-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
1 parent
885eee2
commit 95c1366
Showing
33 changed files
with
1,026 additions
and
827 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
347 changes: 0 additions & 347 deletions
347
packages/ui/app/src/playground/PlaygroundEndpointContent.tsx
This file was deleted.
Oops, something went wrong.
400 changes: 0 additions & 400 deletions
400
packages/ui/app/src/playground/PlaygroundEndpointForm.tsx
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
13 changes: 13 additions & 0 deletions
13
packages/ui/app/src/playground/endpoint/PlaygroundCardSkeleton.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import clsx from "clsx"; | ||
import { PropsWithChildren, ReactElement } from "react"; | ||
|
||
export function PlaygroundCardSkeleton({ | ||
className, | ||
children, | ||
}: PropsWithChildren<{ className?: string }>): ReactElement { | ||
return ( | ||
<div className={clsx("rounded-xl bg-tag-default", className)}> | ||
{children && <div className="contents invisible">{children}</div>} | ||
</div> | ||
); | ||
} |
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
68 changes: 68 additions & 0 deletions
68
packages/ui/app/src/playground/endpoint/PlaygroundEndpointContent.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import { Loadable } from "@fern-ui/loadable"; | ||
import { Dispatch, ReactElement, SetStateAction, useDeferredValue } from "react"; | ||
import { ResolvedEndpointDefinition, ResolvedTypeDefinition } from "../../resolver/types"; | ||
import { PlaygroundAuthorizationFormCard } from "../PlaygroundAuthorizationForm"; | ||
import { PlaygroundEndpointRequestFormState } from "../types"; | ||
import { PlaygroundResponse } from "../types/playgroundResponse"; | ||
import { PlaygroundEndpointContentLayout } from "./PlaygroundEndpointContentLayout"; | ||
import { PlaygroundEndpointForm } from "./PlaygroundEndpointForm"; | ||
import { PlaygroundEndpointFormButtons } from "./PlaygroundEndpointFormButtons"; | ||
import { PlaygroundEndpointRequestCard } from "./PlaygroundEndpointRequestCard"; | ||
import { PlaygroundResponseCard } from "./PlaygroundResponseCard"; | ||
|
||
interface PlaygroundEndpointContentProps { | ||
endpoint: ResolvedEndpointDefinition; | ||
formState: PlaygroundEndpointRequestFormState; | ||
setFormState: Dispatch<SetStateAction<PlaygroundEndpointRequestFormState>>; | ||
resetWithExample: () => void; | ||
resetWithoutExample: () => void; | ||
response: Loadable<PlaygroundResponse>; | ||
sendRequest: () => void; | ||
types: Record<string, ResolvedTypeDefinition>; | ||
} | ||
|
||
export function PlaygroundEndpointContent({ | ||
endpoint, | ||
formState, | ||
setFormState, | ||
resetWithExample, | ||
resetWithoutExample, | ||
response, | ||
sendRequest, | ||
types, | ||
}: PlaygroundEndpointContentProps): ReactElement { | ||
const deferredFormState = useDeferredValue(formState); | ||
|
||
const form = ( | ||
<div className="mx-auto w-full max-w-5xl space-y-6 pt-6 max-sm:pt-0 sm:pb-20"> | ||
{endpoint.auth != null && <PlaygroundAuthorizationFormCard auth={endpoint.auth} disabled={false} />} | ||
|
||
<div className="col-span-2 space-y-8"> | ||
<PlaygroundEndpointForm | ||
endpoint={endpoint} | ||
formState={formState} | ||
setFormState={setFormState} | ||
types={types} | ||
/> | ||
</div> | ||
|
||
<PlaygroundEndpointFormButtons | ||
endpoint={endpoint} | ||
resetWithExample={resetWithExample} | ||
resetWithoutExample={resetWithoutExample} | ||
/> | ||
</div> | ||
); | ||
|
||
const requestCard = <PlaygroundEndpointRequestCard endpoint={endpoint} formState={deferredFormState} />; | ||
const responseCard = <PlaygroundResponseCard response={response} sendRequest={sendRequest} />; | ||
|
||
return ( | ||
<PlaygroundEndpointContentLayout | ||
sendRequest={sendRequest} | ||
form={form} | ||
requestCard={requestCard} | ||
responseCard={responseCard} | ||
/> | ||
); | ||
} |
57 changes: 57 additions & 0 deletions
57
packages/ui/app/src/playground/endpoint/PlaygroundEndpointContentLayout.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import { useResizeObserver } from "@fern-ui/react-commons"; | ||
import { useAtomValue } from "jotai"; | ||
import { ReactElement, ReactNode, useRef, useState } from "react"; | ||
import { IS_MOBILE_SCREEN_ATOM } from "../../atoms"; | ||
import { PlaygroundSendRequestButton } from "../PlaygroundSendRequestButton"; | ||
import { PlaygroundEndpointDesktopLayout } from "./PlaygroundEndpointDesktopLayout"; | ||
import { PlaygroundEndpointMobileLayout } from "./PlaygroundEndpointMobileLayout"; | ||
|
||
interface PlaygroundEndpointContentLayoutProps { | ||
sendRequest: () => void; | ||
form: ReactNode; | ||
requestCard: ReactNode; | ||
responseCard: ReactNode; | ||
} | ||
|
||
export function PlaygroundEndpointContentLayout({ | ||
sendRequest, | ||
form, | ||
requestCard, | ||
responseCard, | ||
}: PlaygroundEndpointContentLayoutProps): ReactElement { | ||
const isMobileScreen = useAtomValue(IS_MOBILE_SCREEN_ATOM); | ||
|
||
const scrollAreaRef = useRef<HTMLDivElement>(null); | ||
const [scrollAreaHeight, setScrollAreaHeight] = useState(0); | ||
|
||
useResizeObserver(scrollAreaRef, ([size]) => { | ||
if (size != null) { | ||
setScrollAreaHeight(size.contentRect.height); | ||
} | ||
}); | ||
|
||
return ( | ||
<div className="flex min-h-0 w-full flex-1 shrink items-stretch divide-x"> | ||
<div | ||
ref={scrollAreaRef} | ||
className="mask-grad-top-6 w-full overflow-x-hidden overflow-y-scroll overscroll-contain" | ||
> | ||
{!isMobileScreen ? ( | ||
<PlaygroundEndpointDesktopLayout | ||
scrollAreaHeight={scrollAreaHeight} | ||
form={form} | ||
requestCard={requestCard} | ||
responseCard={responseCard} | ||
/> | ||
) : ( | ||
<PlaygroundEndpointMobileLayout | ||
form={form} | ||
requestCard={requestCard} | ||
responseCard={responseCard} | ||
sendButton={<PlaygroundSendRequestButton sendRequest={sendRequest} />} | ||
/> | ||
)} | ||
</div> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
32 changes: 32 additions & 0 deletions
32
packages/ui/app/src/playground/endpoint/PlaygroundEndpointDesktopLayout.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import { ReactElement, ReactNode } from "react"; | ||
import { HorizontalSplitPane, VerticalSplitPane } from "../VerticalSplitPane"; | ||
|
||
interface PlaygroundEndpointDesktopLayoutProps { | ||
scrollAreaHeight: number; | ||
form: ReactNode; | ||
requestCard: ReactNode; | ||
responseCard: ReactNode; | ||
} | ||
|
||
export function PlaygroundEndpointDesktopLayout({ | ||
scrollAreaHeight, | ||
form, | ||
requestCard, | ||
responseCard, | ||
}: PlaygroundEndpointDesktopLayoutProps): ReactElement { | ||
return ( | ||
<HorizontalSplitPane rizeBarHeight={scrollAreaHeight} leftClassName="pl-6 pr-1 mt" rightClassName="pl-1"> | ||
{form} | ||
|
||
<VerticalSplitPane | ||
className="sticky inset-0 pr-6" | ||
style={{ height: scrollAreaHeight }} | ||
aboveClassName={"pt-6 pb-1 flex items-stretch justify-stretch"} | ||
belowClassName="pb-6 pt-1 flex items-stretch justify-stretch" | ||
> | ||
{requestCard} | ||
{responseCard} | ||
</VerticalSplitPane> | ||
</HorizontalSplitPane> | ||
); | ||
} |
217 changes: 217 additions & 0 deletions
217
packages/ui/app/src/playground/endpoint/PlaygroundEndpointForm.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,217 @@ | ||
import { titleCase } from "@fern-api/fdr-sdk"; | ||
import { Dispatch, FC, SetStateAction, useCallback } from "react"; | ||
import { | ||
ResolvedEndpointDefinition, | ||
ResolvedTypeDefinition, | ||
dereferenceObjectProperties, | ||
unwrapReference, | ||
visitResolvedHttpRequestBodyShape, | ||
} from "../../resolver/types"; | ||
import { PlaygroundFileUploadForm } from "../form/PlaygroundFileUploadForm"; | ||
import { PlaygroundObjectPropertiesForm } from "../form/PlaygroundObjectPropertyForm"; | ||
import { PlaygroundTypeReferenceForm } from "../form/PlaygroundTypeReferenceForm"; | ||
import { PlaygroundEndpointRequestFormState, PlaygroundFormStateBody } from "../types"; | ||
import { PlaygroundEndpointFormSection } from "./PlaygroundEndpointFormSection"; | ||
import { PlaygroundEndpointMultipartForm } from "./PlaygroundEndpointMultipartForm"; | ||
|
||
interface PlaygroundEndpointFormProps { | ||
endpoint: ResolvedEndpointDefinition; | ||
formState: PlaygroundEndpointRequestFormState | undefined; | ||
setFormState: Dispatch<SetStateAction<PlaygroundEndpointRequestFormState>>; | ||
types: Record<string, ResolvedTypeDefinition>; | ||
ignoreHeaders?: boolean; | ||
} | ||
|
||
export const PlaygroundEndpointForm: FC<PlaygroundEndpointFormProps> = ({ | ||
endpoint, | ||
formState, | ||
setFormState, | ||
types, | ||
ignoreHeaders, | ||
}) => { | ||
const setHeaders = useCallback( | ||
(value: ((old: unknown) => unknown) | unknown) => { | ||
setFormState((state) => ({ | ||
...state, | ||
headers: typeof value === "function" ? value(state.headers) : value, | ||
})); | ||
}, | ||
[setFormState], | ||
); | ||
|
||
const setPathParameters = useCallback( | ||
(value: ((old: unknown) => unknown) | unknown) => { | ||
setFormState((state) => ({ | ||
...state, | ||
pathParameters: typeof value === "function" ? value(state.pathParameters) : value, | ||
})); | ||
}, | ||
[setFormState], | ||
); | ||
|
||
const setQueryParameters = useCallback( | ||
(value: ((old: unknown) => unknown) | unknown) => { | ||
setFormState((state) => ({ | ||
...state, | ||
queryParameters: typeof value === "function" ? value(state.queryParameters) : value, | ||
})); | ||
}, | ||
[setFormState], | ||
); | ||
|
||
const setBody = useCallback( | ||
( | ||
value: | ||
| ((old: PlaygroundFormStateBody | undefined) => PlaygroundFormStateBody | undefined) | ||
| PlaygroundFormStateBody | ||
| undefined, | ||
) => { | ||
setFormState((state) => ({ | ||
...state, | ||
body: typeof value === "function" ? value(state.body) : value, | ||
})); | ||
}, | ||
[setFormState], | ||
); | ||
|
||
const setBodyJson = useCallback( | ||
(value: ((old: unknown) => unknown) | unknown) => { | ||
setBody((old) => { | ||
return { | ||
type: "json", | ||
value: typeof value === "function" ? value(old?.type === "json" ? old.value : undefined) : value, | ||
}; | ||
}); | ||
}, | ||
[setBody], | ||
); | ||
|
||
const setBodyOctetStream = useCallback( | ||
(value: ((old: File | undefined) => File | undefined) | File | undefined) => { | ||
setBody((old) => { | ||
return { | ||
type: "octet-stream", | ||
value: | ||
typeof value === "function" | ||
? value(old?.type === "octet-stream" ? old.value : undefined) | ||
: value, | ||
}; | ||
}); | ||
}, | ||
[setBody], | ||
); | ||
|
||
return ( | ||
<> | ||
{endpoint.headers.length > 0 && ( | ||
<PlaygroundEndpointFormSection ignoreHeaders={ignoreHeaders} title="Headers"> | ||
<PlaygroundObjectPropertiesForm | ||
id="header" | ||
properties={endpoint.headers} | ||
onChange={setHeaders} | ||
value={formState?.headers} | ||
types={types} | ||
/> | ||
</PlaygroundEndpointFormSection> | ||
)} | ||
|
||
{endpoint.pathParameters.length > 0 && ( | ||
<PlaygroundEndpointFormSection ignoreHeaders={ignoreHeaders} title="Path Parameters"> | ||
<PlaygroundObjectPropertiesForm | ||
id="path" | ||
properties={endpoint.pathParameters} | ||
onChange={setPathParameters} | ||
value={formState?.pathParameters} | ||
types={types} | ||
/> | ||
</PlaygroundEndpointFormSection> | ||
)} | ||
|
||
{endpoint.queryParameters.length > 0 && ( | ||
<PlaygroundEndpointFormSection ignoreHeaders={ignoreHeaders} title="Query Parameters"> | ||
<PlaygroundObjectPropertiesForm | ||
id="query" | ||
properties={endpoint.queryParameters} | ||
onChange={setQueryParameters} | ||
value={formState?.queryParameters} | ||
types={types} | ||
/> | ||
</PlaygroundEndpointFormSection> | ||
)} | ||
|
||
{endpoint.requestBody != null && | ||
visitResolvedHttpRequestBodyShape(endpoint.requestBody.shape, { | ||
formData: (formData) => ( | ||
<PlaygroundEndpointFormSection ignoreHeaders={ignoreHeaders} title={titleCase(formData.name)}> | ||
<PlaygroundEndpointMultipartForm | ||
endpoint={endpoint} | ||
formState={formState} | ||
formData={formData} | ||
types={types} | ||
setBody={setBody} | ||
/> | ||
</PlaygroundEndpointFormSection> | ||
), | ||
bytes: (bytes) => ( | ||
<PlaygroundEndpointFormSection ignoreHeaders={ignoreHeaders} title="Body"> | ||
<PlaygroundFileUploadForm | ||
id="body" | ||
propertyKey="body" | ||
isOptional={bytes.isOptional} | ||
type="file" | ||
onValueChange={(files) => setBodyOctetStream(files?.[0])} | ||
value={ | ||
formState?.body?.type === "octet-stream" && formState.body.value != null | ||
? [formState.body.value] | ||
: undefined | ||
} | ||
/> | ||
</PlaygroundEndpointFormSection> | ||
), | ||
typeShape: (shape) => { | ||
shape = unwrapReference(shape, types); | ||
|
||
if (shape.type === "object") { | ||
return ( | ||
<PlaygroundEndpointFormSection ignoreHeaders={ignoreHeaders} title="Body Parameters"> | ||
<PlaygroundObjectPropertiesForm | ||
id="body" | ||
properties={dereferenceObjectProperties(shape, types)} | ||
onChange={setBodyJson} | ||
value={formState?.body?.value} | ||
types={types} | ||
/> | ||
</PlaygroundEndpointFormSection> | ||
); | ||
} else if (shape.type === "optional") { | ||
return ( | ||
<PlaygroundEndpointFormSection ignoreHeaders={ignoreHeaders} title="Optional Body"> | ||
<PlaygroundTypeReferenceForm | ||
id="body" | ||
shape={shape.shape} | ||
onChange={setBodyJson} | ||
value={formState?.body?.value} | ||
onlyRequired | ||
types={types} | ||
/> | ||
</PlaygroundEndpointFormSection> | ||
); | ||
} | ||
|
||
return ( | ||
<PlaygroundEndpointFormSection ignoreHeaders={ignoreHeaders} title="Body"> | ||
<PlaygroundTypeReferenceForm | ||
id="body" | ||
shape={shape} | ||
onChange={setBodyJson} | ||
value={formState?.body?.value} | ||
onlyRequired | ||
types={types} | ||
/> | ||
</PlaygroundEndpointFormSection> | ||
); | ||
}, | ||
})} | ||
</> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
27 changes: 27 additions & 0 deletions
27
packages/ui/app/src/playground/endpoint/PlaygroundEndpointFormSection.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { FernCard } from "@fern-ui/components"; | ||
import { PropsWithChildren, ReactElement, ReactNode } from "react"; | ||
|
||
interface PlaygroundEndpointFormSection { | ||
title?: ReactNode; | ||
ignoreHeaders: boolean | undefined; | ||
} | ||
|
||
export function PlaygroundEndpointFormSection({ | ||
title, | ||
ignoreHeaders, | ||
children, | ||
}: PropsWithChildren<PlaygroundEndpointFormSection>): ReactElement | null { | ||
if (children == null) { | ||
return null; | ||
} | ||
return ( | ||
<section> | ||
{!ignoreHeaders && title && ( | ||
<div className="mb-4 px-4"> | ||
{typeof title === "string" ? <h5 className="t-muted m-0">{title}</h5> : title} | ||
</div> | ||
)} | ||
<FernCard className="rounded-xl p-4 shadow-sm">{children}</FernCard> | ||
</section> | ||
); | ||
} |
13 changes: 13 additions & 0 deletions
13
packages/ui/app/src/playground/endpoint/PlaygroundEndpointFormSectionSkeleton.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { ReactElement } from "react"; | ||
import { PlaygroundCardSkeleton } from "./PlaygroundCardSkeleton"; | ||
|
||
export function PlaygroundEndpointFormSectionSkeleton(): ReactElement { | ||
return ( | ||
<section> | ||
<PlaygroundCardSkeleton className="w-fit mb-4"> | ||
<h5 className="inline">Parameters</h5> | ||
</PlaygroundCardSkeleton> | ||
<PlaygroundCardSkeleton className="h-32" /> | ||
</section> | ||
); | ||
} |
50 changes: 50 additions & 0 deletions
50
packages/ui/app/src/playground/endpoint/PlaygroundEndpointMobileLayout.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import { FernTabs } from "@fern-ui/components"; | ||
import { ReactElement, ReactNode, useState } from "react"; | ||
|
||
interface PlaygroundEndpointMobileLayoutProps { | ||
form: ReactNode; | ||
requestCard: ReactNode; | ||
responseCard: ReactNode; | ||
sendButton: ReactNode; | ||
} | ||
|
||
export function PlaygroundEndpointMobileLayout({ | ||
form, | ||
requestCard, | ||
responseCard, | ||
sendButton, | ||
}: PlaygroundEndpointMobileLayoutProps): ReactElement { | ||
const [tabValue, setTabValue] = useState<string>("0"); | ||
return ( | ||
<FernTabs | ||
className="px-4" | ||
defaultValue="0" | ||
value={tabValue} | ||
onValueChange={setTabValue} | ||
tabs={[ | ||
{ | ||
title: "Request", | ||
content: ( | ||
<div className="space-y-4 pb-6"> | ||
{form} | ||
<div className="border-default flex justify-end border-b pb-4"> | ||
{sendButton} | ||
{/* <PlaygroundSendRequestButton | ||
sendRequest={() => { | ||
sendRequest(); | ||
setTabValue("1"); | ||
}} | ||
sendRequestIcon={ | ||
<SendSolid className="transition-transform group-hover:translate-x-0.5" /> | ||
} | ||
/> */} | ||
</div> | ||
{requestCard} | ||
</div> | ||
), | ||
}, | ||
{ title: "Response", content: responseCard }, | ||
]} | ||
/> | ||
); | ||
} |
156 changes: 156 additions & 0 deletions
156
packages/ui/app/src/playground/endpoint/PlaygroundEndpointMultipartForm.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
import { visitDiscriminatedUnion } from "@fern-api/fdr-sdk"; | ||
import { ReactElement, useCallback } from "react"; | ||
import { ResolvedEndpointDefinition, ResolvedFormData, ResolvedTypeDefinition } from "../../resolver/types"; | ||
import { PlaygroundFileUploadForm } from "../form/PlaygroundFileUploadForm"; | ||
import { PlaygroundObjectPropertyForm } from "../form/PlaygroundObjectPropertyForm"; | ||
import { PlaygroundEndpointRequestFormState, PlaygroundFormDataEntryValue, PlaygroundFormStateBody } from "../types"; | ||
|
||
interface PlaygroundEndpointMultipartFormProps { | ||
endpoint: ResolvedEndpointDefinition; | ||
formState: PlaygroundEndpointRequestFormState | undefined; | ||
formData: ResolvedFormData; | ||
types: Record<string, ResolvedTypeDefinition>; | ||
setBody: ( | ||
value: | ||
| PlaygroundFormStateBody | ||
| ((old: PlaygroundFormStateBody | undefined) => PlaygroundFormStateBody | undefined) | ||
| undefined, | ||
) => void; | ||
} | ||
|
||
export function PlaygroundEndpointMultipartForm({ | ||
endpoint, | ||
formState, | ||
formData, | ||
types, | ||
setBody, | ||
}: PlaygroundEndpointMultipartFormProps): ReactElement { | ||
const formDataFormValue = formState?.body?.type === "form-data" ? formState?.body.value : {}; | ||
|
||
const setBodyFormData = useCallback( | ||
( | ||
value: | ||
| ((old: Record<string, PlaygroundFormDataEntryValue>) => Record<string, PlaygroundFormDataEntryValue>) | ||
| Record<string, PlaygroundFormDataEntryValue>, | ||
) => { | ||
setBody((old) => { | ||
return { | ||
type: "form-data", | ||
value: typeof value === "function" ? value(old?.type === "form-data" ? old.value : {}) : value, | ||
}; | ||
}); | ||
}, | ||
[setBody], | ||
); | ||
|
||
const setFormDataEntry = useCallback( | ||
( | ||
key: string, | ||
value: | ||
| PlaygroundFormDataEntryValue | ||
| undefined | ||
| ((old: PlaygroundFormDataEntryValue | undefined) => PlaygroundFormDataEntryValue | undefined), | ||
) => { | ||
setBodyFormData((old) => { | ||
const newValue = typeof value === "function" ? value(old[key] ?? undefined) : value; | ||
if (newValue == null) { | ||
// delete the key | ||
const { [key]: _, ...rest } = old; | ||
return rest; | ||
} | ||
return { ...old, [key]: newValue }; | ||
}); | ||
}, | ||
[setBodyFormData], | ||
); | ||
|
||
const handleFormDataFileChange = useCallback( | ||
(key: string, files: ReadonlyArray<File> | undefined) => { | ||
const type = | ||
endpoint.requestBody?.shape.type === "formData" | ||
? endpoint.requestBody?.shape.properties.find((p) => p.key === key)?.type | ||
: undefined; | ||
if (files == null || files.length === 0) { | ||
setFormDataEntry(key, undefined); | ||
return; | ||
} else { | ||
setFormDataEntry( | ||
key, | ||
type === "fileArray" ? { type: "fileArray", value: files } : { type: "file", value: files[0] }, | ||
); | ||
} | ||
}, | ||
[endpoint.requestBody, setFormDataEntry], | ||
); | ||
|
||
const handleFormDataJsonChange = useCallback( | ||
(key: string, value: unknown) => { | ||
setFormDataEntry( | ||
key, | ||
value == null | ||
? undefined | ||
: typeof value === "function" | ||
? (oldValue) => ({ type: "json", value: value(oldValue?.value) }) | ||
: { type: "json", value }, | ||
); | ||
}, | ||
[setFormDataEntry], | ||
); | ||
|
||
return ( | ||
<ul className="list-none space-y-8"> | ||
{formData.properties.map((property) => | ||
visitDiscriminatedUnion(property, "type")._visit({ | ||
file: (file) => { | ||
const currentValue = formDataFormValue[property.key]; | ||
return ( | ||
<li key={property.key}> | ||
<PlaygroundFileUploadForm | ||
id={`body.${property.key}`} | ||
propertyKey={property.key} | ||
type={file.type} | ||
isOptional={file.isOptional} | ||
onValueChange={(files) => handleFormDataFileChange(property.key, files)} | ||
value={ | ||
currentValue?.type === "file" | ||
? currentValue.value != null | ||
? [currentValue.value] | ||
: undefined | ||
: undefined | ||
} | ||
/> | ||
</li> | ||
); | ||
}, | ||
fileArray: (fileArray) => { | ||
const currentValue = formDataFormValue[property.key]; | ||
return ( | ||
<li key={property.key}> | ||
<PlaygroundFileUploadForm | ||
id={`body.${property.key}`} | ||
propertyKey={property.key} | ||
type={fileArray.type} | ||
isOptional={fileArray.isOptional} | ||
onValueChange={(files) => handleFormDataFileChange(property.key, files)} | ||
value={currentValue?.type === "fileArray" ? currentValue.value : undefined} | ||
/> | ||
</li> | ||
); | ||
}, | ||
bodyProperty: (bodyProperty) => ( | ||
<li key={property.key}> | ||
<PlaygroundObjectPropertyForm | ||
id="body" | ||
property={bodyProperty} | ||
onChange={handleFormDataJsonChange} | ||
value={formDataFormValue[property.key]?.value} | ||
types={types} | ||
/> | ||
</li> | ||
), | ||
_other: () => null, | ||
}), | ||
)} | ||
</ul> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
81 changes: 81 additions & 0 deletions
81
packages/ui/app/src/playground/endpoint/PlaygroundEndpointRequestCard.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import { CopyToClipboardButton, FernButton, FernButtonGroup, FernCard } from "@fern-ui/components"; | ||
import { useAtom, useSetAtom } from "jotai"; | ||
import { ReactElement } from "react"; | ||
import { | ||
PLAYGROUND_AUTH_STATE_ATOM, | ||
PLAYGROUND_AUTH_STATE_OAUTH_ATOM, | ||
PLAYGROUND_REQUEST_TYPE_ATOM, | ||
store, | ||
useFeatureFlags, | ||
} from "../../atoms"; | ||
import { useStandardProxyEnvironment } from "../../hooks/useStandardProxyEnvironment"; | ||
import { ResolvedEndpointDefinition } from "../../resolver/types"; | ||
import { PlaygroundRequestPreview } from "../PlaygroundRequestPreview"; | ||
import { PlaygroundCodeSnippetResolverBuilder } from "../code-snippets/resolver"; | ||
import { PlaygroundEndpointRequestFormState } from "../types"; | ||
|
||
interface PlaygroundEndpointRequestCardProps { | ||
endpoint: ResolvedEndpointDefinition; | ||
formState: PlaygroundEndpointRequestFormState; | ||
} | ||
|
||
export function PlaygroundEndpointRequestCard({ | ||
endpoint, | ||
formState, | ||
}: PlaygroundEndpointRequestCardProps): ReactElement { | ||
const { isSnippetTemplatesEnabled, isFileForgeHackEnabled } = useFeatureFlags(); | ||
const [requestType, setRequestType] = useAtom(PLAYGROUND_REQUEST_TYPE_ATOM); | ||
const setOAuthValue = useSetAtom(PLAYGROUND_AUTH_STATE_OAUTH_ATOM); | ||
const proxyEnvironment = useStandardProxyEnvironment(); | ||
return ( | ||
<FernCard className="flex min-w-0 flex-1 shrink flex-col overflow-hidden rounded-xl shadow-sm"> | ||
<div className="border-default flex h-10 w-full shrink-0 items-center justify-between border-b px-3 py-2"> | ||
<span className="t-muted text-xs uppercase">Request</span> | ||
|
||
<FernButtonGroup> | ||
<FernButton | ||
onClick={() => setRequestType("curl")} | ||
size="small" | ||
variant="minimal" | ||
intent={requestType === "curl" ? "primary" : "none"} | ||
active={requestType === "curl"} | ||
> | ||
cURL | ||
</FernButton> | ||
<FernButton | ||
onClick={() => setRequestType("typescript")} | ||
size="small" | ||
variant="minimal" | ||
intent={requestType === "typescript" ? "primary" : "none"} | ||
active={requestType === "typescript"} | ||
> | ||
TypeScript | ||
</FernButton> | ||
<FernButton | ||
onClick={() => setRequestType("python")} | ||
size="small" | ||
variant="minimal" | ||
intent={requestType === "python" ? "primary" : "none"} | ||
active={requestType === "python"} | ||
> | ||
Python | ||
</FernButton> | ||
</FernButtonGroup> | ||
|
||
<CopyToClipboardButton | ||
content={() => { | ||
const authState = store.get(PLAYGROUND_AUTH_STATE_ATOM); | ||
const resolver = new PlaygroundCodeSnippetResolverBuilder( | ||
endpoint, | ||
isSnippetTemplatesEnabled, | ||
isFileForgeHackEnabled, | ||
).create(authState, formState, proxyEnvironment, setOAuthValue); | ||
return resolver.resolve(requestType); | ||
}} | ||
className="-mr-2" | ||
/> | ||
</div> | ||
<PlaygroundRequestPreview endpoint={endpoint} formState={formState} requestType={requestType} /> | ||
</FernCard> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
54 changes: 54 additions & 0 deletions
54
packages/ui/app/src/playground/endpoint/PlaygroundEndpointSkeleton.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import { FernButton } from "@fern-ui/components"; | ||
import * as Dialog from "@radix-ui/react-dialog"; | ||
import { Xmark } from "iconoir-react"; | ||
import { ReactElement } from "react"; | ||
import { noop } from "ts-essentials"; | ||
import { HttpMethodTag } from "../../components/HttpMethodTag"; | ||
import { PlaygroundSendRequestButton } from "../PlaygroundSendRequestButton"; | ||
import { PlaygroundCardSkeleton } from "./PlaygroundCardSkeleton"; | ||
import { PlaygroundEndpointContentLayout } from "./PlaygroundEndpointContentLayout"; | ||
import { PlaygroundEndpointFormSectionSkeleton } from "./PlaygroundEndpointFormSectionSkeleton"; | ||
|
||
function PlaygroundEndpointPath() { | ||
return ( | ||
<div className="playground-endpoint"> | ||
<div className="flex h-10 min-w-0 flex-1 shrink gap-2 rounded-lg bg-tag-default px-4 py-2 max-sm:h-8 max-sm:px-2 max-sm:py-1 sm:rounded-[20px] items-center"> | ||
<HttpMethodTag method="POST" className="playground-endpoint-method" skeleton /> | ||
</div> | ||
|
||
<div className="max-sm:hidden"> | ||
<PlaygroundSendRequestButton /> | ||
</div> | ||
|
||
<Dialog.Close asChild className="max-sm:hidden"> | ||
<FernButton icon={<Xmark />} size="large" rounded variant="outlined" /> | ||
</Dialog.Close> | ||
</div> | ||
); | ||
} | ||
|
||
export function PlaygroundEndpointSkeleton(): ReactElement { | ||
const form = ( | ||
<div className="mx-auto w-full max-w-5xl space-y-6 pt-6 max-sm:pt-0 sm:pb-20"> | ||
<div className="col-span-2 space-y-8"> | ||
<PlaygroundEndpointFormSectionSkeleton /> | ||
<PlaygroundEndpointFormSectionSkeleton /> | ||
</div> | ||
</div> | ||
); | ||
return ( | ||
<div className="flex min-h-0 flex-1 shrink flex-col size-full"> | ||
<div className="flex-0"> | ||
<PlaygroundEndpointPath /> | ||
</div> | ||
<div className="flex min-h-0 flex-1 shrink"> | ||
<PlaygroundEndpointContentLayout | ||
sendRequest={noop} | ||
form={form} | ||
requestCard={<PlaygroundCardSkeleton className="flex-1" />} | ||
responseCard={<PlaygroundCardSkeleton className="flex-1" />} | ||
/> | ||
</div> | ||
</div> | ||
); | ||
} |
154 changes: 154 additions & 0 deletions
154
packages/ui/app/src/playground/endpoint/PlaygroundResponseCard.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
import { | ||
CopyToClipboardButton, | ||
FernAudioPlayer, | ||
FernButton, | ||
FernCard, | ||
FernTooltip, | ||
FernTooltipProvider, | ||
} from "@fern-ui/components"; | ||
import { Loadable, visitLoadable } from "@fern-ui/loadable"; | ||
import clsx from "clsx"; | ||
import { Download } from "iconoir-react"; | ||
import { isEmpty, round } from "lodash-es"; | ||
import { ReactElement } from "react"; | ||
import { useFeatureFlags } from "../../atoms"; | ||
import { FernErrorTag } from "../../components/FernErrorBoundary"; | ||
import { PlaygroundResponsePreview } from "../PlaygroundResponsePreview"; | ||
import { PlaygroundSendRequestButton } from "../PlaygroundSendRequestButton"; | ||
import { PlaygroundResponse } from "../types/playgroundResponse"; | ||
import { ProxyResponse } from "../types/proxy"; | ||
|
||
interface PlaygroundResponseCard { | ||
response: Loadable<PlaygroundResponse>; | ||
sendRequest: () => void; | ||
} | ||
|
||
export function PlaygroundResponseCard({ response, sendRequest }: PlaygroundResponseCard): ReactElement { | ||
const { isBinaryOctetStreamAudioPlayer } = useFeatureFlags(); | ||
return ( | ||
<FernCard className="flex min-w-0 flex-1 shrink flex-col overflow-hidden rounded-xl shadow-sm"> | ||
<div className="border-default flex h-10 w-full shrink-0 items-center justify-between border-b px-3 py-2"> | ||
<span className="t-muted text-xs uppercase">Response</span> | ||
|
||
{response.type === "loaded" && ( | ||
<div className="flex items-center gap-2 text-xs"> | ||
<span | ||
className={clsx("font-mono flex items-center py-1 px-1.5 rounded-md h-5", { | ||
["bg-method-get/10 text-method-get dark:bg-method-get-dark/10 dark:text-method-get-dark"]: | ||
response.value.response.status >= 200 && response.value.response.status < 300, | ||
["bg-method-delete/10 text-method-delete dark:bg-method-delete-dark/10 dark:text-method-delete-dark"]: | ||
response.value.response.status > 300, | ||
})} | ||
> | ||
status: {response.value.response.status} | ||
</span> | ||
<span className={"flex h-5 items-center rounded-md bg-tag-default px-1.5 py-1 font-mono"}> | ||
time: {round(response.value.time, 2)}ms | ||
</span> | ||
{response.value.type === "json" && !isEmpty(response.value.size) && ( | ||
<span className={"flex h-5 items-center rounded-md bg-tag-default px-1.5 py-1 font-mono"}> | ||
size: {response.value.size}b | ||
</span> | ||
)} | ||
</div> | ||
)} | ||
|
||
{visitLoadable(response, { | ||
loading: () => <div />, | ||
loaded: (response) => | ||
response.type === "file" ? ( | ||
<FernTooltipProvider> | ||
<FernTooltip content="Download file"> | ||
<FernButton | ||
icon={<Download />} | ||
size="small" | ||
variant="minimal" | ||
onClick={() => { | ||
const a = document.createElement("a"); | ||
a.href = response.response.body; | ||
a.download = createFilename(response.response, response.contentType); | ||
a.click(); | ||
}} | ||
/> | ||
</FernTooltip> | ||
</FernTooltipProvider> | ||
) : ( | ||
<CopyToClipboardButton | ||
content={() => | ||
response.type === "json" | ||
? JSON.stringify(response.response.body, null, 2) | ||
: response.type === "stream" | ||
? response.response.body | ||
: "" | ||
} | ||
className="-mr-2" | ||
/> | ||
), | ||
failed: () => ( | ||
<span className="flex items-center rounded-[4px] bg-tag-danger p-1 font-mono text-xs uppercase leading-none text-intent-danger"> | ||
Failed | ||
</span> | ||
), | ||
})} | ||
</div> | ||
{visitLoadable(response, { | ||
loading: () => | ||
response.type === "notStartedLoading" ? ( | ||
<div className="flex flex-1 items-center justify-center"> | ||
<PlaygroundSendRequestButton sendRequest={sendRequest} /> | ||
</div> | ||
) : ( | ||
<div className="flex flex-1 items-center justify-center">Loading...</div> | ||
), | ||
loaded: (response) => | ||
response.type !== "file" ? ( | ||
<PlaygroundResponsePreview response={response} /> | ||
) : response.contentType.startsWith("audio/") || | ||
(isBinaryOctetStreamAudioPlayer && response.contentType === "binary/octet-stream") ? ( | ||
<FernAudioPlayer | ||
src={response.response.body} | ||
className="flex h-full items-center justify-center p-4" | ||
/> | ||
) : response.contentType.includes("application/pdf") ? ( | ||
<iframe | ||
src={response.response.body} | ||
className="size-full" | ||
title="PDF preview" | ||
allowFullScreen | ||
/> | ||
) : ( | ||
<FernErrorTag | ||
component="PlaygroundEndpointContent" | ||
error={`File preview not supported for ${response.contentType}`} | ||
className="flex h-full items-center justify-center" | ||
showError | ||
/> | ||
), | ||
failed: (e) => ( | ||
<FernErrorTag | ||
component="PlaygroundEndpointContent" | ||
error={e} | ||
className="flex h-full items-center justify-center" | ||
showError={true} | ||
/> | ||
), | ||
})} | ||
</FernCard> | ||
); | ||
} | ||
|
||
function createFilename(body: ProxyResponse.SerializableFileBody, contentType: string): string { | ||
const headers = new Headers(body.headers); | ||
const contentDisposition = headers.get("Content-Disposition"); | ||
|
||
if (contentDisposition != null) { | ||
const filename = contentDisposition.split("filename=")[1]; | ||
if (filename != null) { | ||
return filename; | ||
} | ||
} | ||
|
||
// TODO: use a more deterministic way to generate filenames | ||
const extension = contentType.split("/")[1]; | ||
return `${crypto.randomUUID()}.${extension}`; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
@import "./endpoint/PlaygroundEndpoint"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters