1+ import { usePrevious } from '@react-hookz/web'
12import { HTTPSnippet } from '@readme/httpsnippet'
23import fetchHar from 'fetch-har'
34import { type ComponentPropsWithoutRef , useContext , useState } from 'react'
45
5- import { ExperimentalButton , ExperimentalCodeBlock , ExperimentalInput , objectKeys } from '@edgeandnode/gds'
6+ import {
7+ ExperimentalButton ,
8+ ExperimentalCodeBlock ,
9+ ExperimentalInput ,
10+ ExperimentalStatus ,
11+ objectEntries ,
12+ objectFromEntries ,
13+ objectKeys ,
14+ } from '@edgeandnode/gds'
615import { Play } from '@edgeandnode/gds/icons'
716
817import { useI18n } from '@/i18n'
@@ -24,6 +33,7 @@ export default function TemplateOpenApiAside(props: ComponentPropsWithoutRef<'di
2433 return < TemplateOpenApiAsideContent { ...props } />
2534}
2635
36+ // TODO: Allow showing the aside on mobile?
2737function TemplateOpenApiAsideContent ( props : ComponentPropsWithoutRef < 'div' > ) {
2838 const openApiContext = useContext ( OpenApiContext ) !
2939 const { operation, values } = openApiContext
@@ -114,70 +124,97 @@ function TemplateOpenApiAsideContent(props: ComponentPropsWithoutRef<'div'>) {
114124 javascript : 'JavaScript' ,
115125 python : 'Python' ,
116126 go : 'Go' ,
117- // TODO: Add `php` and `java` after GDS update
118- }
127+ php : 'PHP' ,
128+ java : 'Java' ,
129+ } as const
130+
131+ const snippets = objectFromEntries (
132+ objectKeys ( snippetLanguages ) . map ( ( language ) => {
133+ let code = ( snippet . convert ( language ) || [ ] ) [ 0 ] ?? ''
134+ // Unencode placeholders potentially present in the URL
135+ for ( const placeholder of [
136+ ...Object . values ( PLACEHOLDERS ) ,
137+ ...operation . pathParameters . map ( ( parameter ) => `{${ parameter . name } }` ) ,
138+ ] ) {
139+ const encodedPlaceholder = encodeURIComponent ( placeholder )
140+ code = code . replaceAll ( encodedPlaceholder , placeholder )
141+ }
142+ return [ language , code ] as const
143+ } ) ,
144+ )
145+
146+ const previousShellSnippet = usePrevious ( snippets . shell )
147+ const snippetsChanged = previousShellSnippet !== undefined && previousShellSnippet !== snippets . shell
119148
120149 const [ isSendingRequest , setIsSendingRequest ] = useState ( false )
121- const [ responseStatus , setResponseStatus ] = useState < number | null > ( null )
122- const [ responseText , setResponseText ] = useState < string | null > ( null )
150+ const [ response , setResponse ] = useState < { status : number ; text : string } | null > ( null )
123151
124152 const sendRequest = async ( ) => {
125153 setIsSendingRequest ( true )
126154 try {
127155 // fetch-har's types are bad
128156 // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
129157 const response = await fetchHar ( { log : { entries : snippet . entries } } as any )
130- setResponseStatus ( response . status )
131- let text = await response . text ( )
158+ let responseText = await response . text ( )
132159 try {
133- const data = JSON . parse ( text )
134- text = JSON . stringify ( data , null , 2 )
160+ responseText = JSON . parse ( responseText )
135161 } catch { }
136- setResponseText ( text )
162+ responseText = JSON . stringify ( responseText , null , 2 )
163+ setResponse ( { status : response . status , text : responseText } )
137164 } catch ( error ) {
138165 console . error ( { error } )
139- setResponseStatus ( null )
140- setResponseText ( null )
166+ setResponse ( null )
141167 }
168+ setSelectedResponseTabIndex ( 0 )
142169 setIsSendingRequest ( false )
143170 }
144171
145- const actualResponseLabel =
146- responseStatus && responseText ? `${ t ( 'global.openApi.responses.actualResponse' ) } (${ responseStatus } )` : undefined
147172 const responseTabs = [
148- ...( actualResponseLabel
173+ ...( response
149174 ? [
150175 {
151- label : actualResponseLabel ,
176+ label : {
177+ label : `${ response . status } (${ t ( 'global.openApi.responses.liveResponse' ) } )` ,
178+ iconBefore : < ExperimentalStatus variant = { response . status === 200 ? 'success' : 'error' } /> ,
179+ } ,
152180 content : (
153181 < ExperimentalCodeBlock
154- key = "actual-response"
155- label = { operation . potentialResponses . length === 0 ? actualResponseLabel : undefined }
182+ key = "live-response"
183+ label = {
184+ operation . potentialResponses . length === 0
185+ ? `${ response . status } (${ t ( 'global.openApi.responses.liveResponse' ) } )`
186+ : undefined
187+ }
156188 language = "json"
189+ lineNumbers = { response . text . split ( '\n' ) . length > 1 }
157190 >
158- { responseText ! }
191+ { response . text }
159192 </ ExperimentalCodeBlock >
160193 ) ,
161194 } ,
162195 ]
163196 : [ ] ) ,
164197 ...operation . potentialResponses . map ( ( potentialResponse ) => {
165- const label = `${ t ( 'global.openApi.responses.exampleResponse' ) } (${ potentialResponse . status } )`
198+ const label = `${ potentialResponse . status } ${ t ( 'global.openApi.responses.example' ) } `
199+ const exampleResponseText = JSON . stringify ( potentialResponse . example , null , 2 )
166200 return {
167- label : operation . potentialResponses . length === 1 && actualResponseLabel ? label : potentialResponse . status ,
201+ label,
168202 content : (
169203 < ExperimentalCodeBlock
170204 key = { potentialResponse . status }
171- label = { operation . potentialResponses . length === 1 && ! actualResponseLabel ? label : undefined }
205+ label = { operation . potentialResponses . length === 1 && ! response ? label : undefined }
172206 language = "json"
207+ lineNumbers = { exampleResponseText . split ( '\n' ) . length > 1 }
173208 >
174- { JSON . stringify ( potentialResponse . example , null , 2 ) }
209+ { exampleResponseText }
175210 </ ExperimentalCodeBlock >
176211 ) ,
177212 }
178213 } ) ,
179214 ]
180215
216+ const [ selectedResponseTabIndex , setSelectedResponseTabIndex ] = useState ( 0 )
217+
181218 return (
182219 < div key = { operation . path } { ...props } >
183220 < ExperimentalInput
@@ -202,35 +239,42 @@ function TemplateOpenApiAsideContent(props: ComponentPropsWithoutRef<'div'>) {
202239 }
203240 className = "mb-6"
204241 />
205- < ExperimentalCodeBlock . Tabs tabs = { Object . values ( snippetLanguages ) } >
206- { objectKeys ( snippetLanguages ) . map ( ( language ) => {
207- let code = ( snippet . convert ( language ) || [ ] ) [ 0 ] ?? ''
208- // Unencode placeholders potentially present in the URL
209- for ( const placeholder of [
210- ...Object . values ( PLACEHOLDERS ) ,
211- ...operation . pathParameters . map ( ( parameter ) => `{${ parameter . name } }` ) ,
212- ] ) {
213- const encodedPlaceholder = encodeURIComponent ( placeholder )
214- code = code . replaceAll ( encodedPlaceholder , placeholder )
215- }
216- return (
217- < ExperimentalCodeBlock
218- key = { language }
219- language = { language }
220- className = "[tab-size:4]" // TODO: Remove after GDS update
221- >
242+ < div >
243+ < ExperimentalCodeBlock . Tabs tabs = { Object . values ( snippetLanguages ) } >
244+ { objectEntries ( snippets ) . map ( ( [ language , code ] ) => (
245+ < ExperimentalCodeBlock key = { language } language = { language } >
222246 { code }
223247 </ ExperimentalCodeBlock >
224- )
225- } ) }
226- </ ExperimentalCodeBlock . Tabs >
227- { responseTabs . length === 1 ? (
228- < div className = "mt-6" > { responseTabs [ 0 ] ! . content } </ div >
229- ) : (
230- < ExperimentalCodeBlock . Tabs tabs = { responseTabs . map ( ( responseTab ) => responseTab . label ) } className = "mt-6" >
231- { responseTabs . map ( ( responseTab ) => responseTab . content ) }
248+ ) ) }
232249 </ ExperimentalCodeBlock . Tabs >
233- ) }
250+ { snippetsChanged ? (
251+ < div
252+ key = { snippets . shell }
253+ className = "animate-opacity-to-0 absolute inset-0 rounded-8 border border-solar-600 pointer-events-none animate-fill-forwards"
254+ />
255+ ) : null }
256+ </ div >
257+ { responseTabs . length > 0 ? (
258+ < div className = "mt-6" >
259+ { responseTabs . length === 1 ? (
260+ responseTabs [ 0 ] ! . content
261+ ) : (
262+ < ExperimentalCodeBlock . Tabs
263+ tabs = { responseTabs . map ( ( responseTab ) => responseTab . label ) }
264+ selectedIndex = { selectedResponseTabIndex }
265+ onChange = { setSelectedResponseTabIndex }
266+ >
267+ { responseTabs . map ( ( responseTab ) => responseTab . content ) }
268+ </ ExperimentalCodeBlock . Tabs >
269+ ) }
270+ { response ? (
271+ < div
272+ key = { response . text }
273+ className = "animate-opacity-to-0 absolute inset-0 rounded-8 border border-solar-600 pointer-events-none animate-fill-forwards"
274+ />
275+ ) : null }
276+ </ div >
277+ ) : null }
234278 </ div >
235279 )
236280}
0 commit comments