UNPKG

fumadocs-openapi

Version:

Generate MDX docs for your OpenAPI spec

459 lines (456 loc) 16.4 kB
'use client'; import { joinURL, resolveRequestData, resolveServerUrl, withBase } from "../utils/url.js"; import { useStorageKey } from "../ui/client/storage-key.js"; import { useApiContext } from "../ui/contexts/api.js"; import { cn } from "../utils/cn.js"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/components/select.js"; import { labelVariants } from "../ui/components/input.js"; import { SchemaProvider, useResolvedSchema } from "./schema.js"; import { FieldInput, FieldSet, JsonInput, ObjectInput } from "./components/inputs.js"; import { getStatusInfo } from "./status-info.js"; import { MethodLabel } from "../ui/components/method-label.js"; import { useQuery } from "../utils/use-query.js"; import { encodeRequestData } from "../requests/media/encode.js"; import ServerSelect from "./components/server-select.js"; import { useExampleRequests } from "../ui/operation/usage-tabs/client.js"; import { Fragment, lazy, useEffect, useEffectEvent, useMemo, useState } from "react"; import { FormProvider, get, set, useController, useForm, useFormContext } from "react-hook-form"; import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime"; import { ChevronDown, LoaderCircle, X } from "lucide-react"; import { buttonVariants } from "fumadocs-ui/components/ui/button"; import { DynamicCodeBlock } from "fumadocs-ui/components/dynamic-codeblock"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "fumadocs-ui/components/ui/collapsible"; //#region src/playground/client.tsx const OauthDialog = lazy(() => import("./components/oauth-dialog.js").then((mod) => ({ default: mod.OauthDialog }))); const OauthDialogTrigger = lazy(() => import("./components/oauth-dialog.js").then((mod) => ({ default: mod.OauthDialogTrigger }))); function PlaygroundClient({ route, method = "GET", securities, parameters = [], body, references, proxyUrl, writeOnly, readOnly, ...rest }) { const { example: exampleId, examples, setExampleData } = useExampleRequests(); const storageKeys = useStorageKey(); const fieldInfoMap = useMemo(() => /* @__PURE__ */ new Map(), []); const { mediaAdapters, serverRef, client: { playground: { components: { ResultDisplay = DefaultResultDisplay } = {}, requestTimeout = 10, transformAuthInputs } = {} } } = useApiContext(); const [securityId, setSecurityId] = useState(0); const { inputs, mapInputs, initAuthValues } = useAuthInputs(securities[securityId], transformAuthInputs); const defaultValues = useMemo(() => { const requestData = examples.find((example) => example.id === exampleId)?.data; return { path: requestData?.path ?? {}, query: requestData?.query ?? {}, header: requestData?.header ?? {}, body: requestData?.body ?? {}, cookie: requestData?.cookie ?? {} }; }, [examples, exampleId]); const form = useForm({ defaultValues }); const testQuery = useQuery(async (input) => { const targetServer = serverRef.current; const fetcher = await import("./fetcher.js").then((mod) => mod.createBrowserFetcher(mediaAdapters, requestTimeout)); input._encoded ??= encodeRequestData({ ...mapInputs(input), method, bodyMediaType: body?.mediaType }, mediaAdapters, parameters); return fetcher.fetch(joinURL(withBase(targetServer ? resolveServerUrl(targetServer.url, targetServer.variables) : "/", window.location.origin), resolveRequestData(route, input._encoded)), { proxyUrl, ...input._encoded }); }); const onUpdateDebounced = useEffectEvent((values) => { for (const item of inputs) { const value = get(values, item.fieldName); if (value) localStorage.setItem(storageKeys.AuthField(item), JSON.stringify(value)); } const data = { ...mapInputs(values), method, bodyMediaType: body?.mediaType }; values._encoded ??= encodeRequestData(data, mediaAdapters, parameters); setExampleData(data, values._encoded); }); useEffect(() => { let timer = null; const subscription = form.subscribe({ formState: { values: true }, callback({ values }) { delete values._encoded; if (timer) window.clearTimeout(timer); timer = window.setTimeout(() => onUpdateDebounced(values), timer ? 400 : 0); } }); return () => subscription(); }, []); useEffect(() => { form.reset(initAuthValues(defaultValues)); return () => fieldInfoMap.clear(); }, [defaultValues]); useEffect(() => { form.reset((values) => initAuthValues(values)); return () => { form.reset((values) => { for (const item of inputs) set(values, item.fieldName, void 0); return values; }); }; }, [inputs]); const onSubmit = form.handleSubmit((value) => { testQuery.start(mapInputs(value)); }); return /* @__PURE__ */ jsx(FormProvider, { ...form, children: /* @__PURE__ */ jsx(SchemaProvider, { fieldInfoMap, references, writeOnly, readOnly, children: /* @__PURE__ */ jsxs("form", { ...rest, className: cn("not-prose flex flex-col rounded-xl border shadow-md overflow-hidden bg-fd-card text-fd-card-foreground", rest.className), onSubmit, children: [ /* @__PURE__ */ jsx(ServerSelect, {}), /* @__PURE__ */ jsxs("div", { className: "flex flex-row items-center gap-2 text-sm p-3 not-last:pb-0", children: [ /* @__PURE__ */ jsx(MethodLabel, { children: method }), /* @__PURE__ */ jsx(Route, { route, className: "flex-1" }), /* @__PURE__ */ jsx("button", { type: "submit", className: cn(buttonVariants({ color: "primary", size: "sm" }), "w-14 py-1.5"), disabled: testQuery.isLoading, children: testQuery.isLoading ? /* @__PURE__ */ jsx(LoaderCircle, { className: "size-4 animate-spin" }) : "Send" }) ] }), securities.length > 0 && /* @__PURE__ */ jsx(SecurityTabs, { securities, securityId, setSecurityId, children: inputs.map((input) => /* @__PURE__ */ jsx(Fragment, { children: input.children }, input.fieldName)) }), /* @__PURE__ */ jsx(FormBody, { body, parameters }), testQuery.data ? /* @__PURE__ */ jsx(ResultDisplay, { data: testQuery.data, reset: testQuery.reset }) : null ] }) }) }); } function SecurityTabs({ securities, setSecurityId, securityId, children }) { const [open, setOpen] = useState(false); const form = useFormContext(); const result = /* @__PURE__ */ jsxs(CollapsiblePanel, { title: "Authorization", children: [/* @__PURE__ */ jsxs(Select, { value: securityId.toString(), onValueChange: (v) => setSecurityId(Number(v)), children: [/* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, {}) }), /* @__PURE__ */ jsx(SelectContent, { children: securities.map((security, i) => /* @__PURE__ */ jsx(SelectItem, { value: i.toString(), children: security.map((item) => /* @__PURE__ */ jsxs("div", { className: "max-w-[600px]", children: [/* @__PURE__ */ jsx("p", { className: "font-mono font-medium", children: item.id }), /* @__PURE__ */ jsx("p", { className: "text-fd-muted-foreground whitespace-pre-wrap", children: item.description })] }, item.id)) }, i)) })] }), children] }); for (let i = 0; i < securities.length; i++) { const security = securities[i]; for (const item of security) if (item.type === "oauth2") return /* @__PURE__ */ jsx(OauthDialog, { scheme: item, scopes: item.scopes, open, setOpen: (v) => { setOpen(v); if (v) setSecurityId(i); }, setToken: (token) => form.setValue("header.Authorization", token), children: result }); } return result; } const ParamTypes = [ "path", "header", "cookie", "query" ]; function FormBody({ parameters = [], body }) { const { renderParameterField, renderBodyField } = useApiContext().client.playground ?? {}; return /* @__PURE__ */ jsxs(Fragment$1, { children: [useMemo(() => { return ParamTypes.map((type) => { const items = parameters.filter((v) => v.in === type); if (items.length === 0) return; return /* @__PURE__ */ jsx(CollapsiblePanel, { title: { header: "Header", cookie: "Cookies", query: "Query", path: "Path" }[type], children: items.map((field) => { const fieldName = `${type}.${field.name}`; if (renderParameterField) return renderParameterField(fieldName, field); const contentTypes = field.content && Object.keys(field.content); const schema = field.content && contentTypes && contentTypes.length > 0 ? field.content[contentTypes[0]].schema : field.schema; return /* @__PURE__ */ jsx(FieldSet, { name: field.name, fieldName, field: schema }, fieldName); }) }, type); }); }, [parameters, renderParameterField]), body && /* @__PURE__ */ jsx(CollapsiblePanel, { title: "Body", children: renderBodyField ? renderBodyField("body", body) : /* @__PURE__ */ jsx(BodyInput, { field: body.schema }) })] }); } function BodyInput({ field: _field }) { const field = useResolvedSchema(_field); const [isJson, setIsJson] = useState(false); if (field.format === "binary") return /* @__PURE__ */ jsx(FieldSet, { field, fieldName: "body" }); if (isJson) return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("button", { className: cn(buttonVariants({ color: "secondary", size: "sm", className: "w-fit font-mono p-2" })), onClick: () => setIsJson(false), type: "button", children: "Close JSON Editor" }), /* @__PURE__ */ jsx(JsonInput, { fieldName: "body" })] }); return /* @__PURE__ */ jsx(FieldSet, { field, fieldName: "body", collapsible: false, name: /* @__PURE__ */ jsx("button", { type: "button", className: cn(buttonVariants({ color: "secondary", size: "sm", className: "p-2" })), onClick: () => setIsJson(true), children: "Open JSON Editor" }) }); } function useAuthInputs(securities, transform) { const storageKeys = useStorageKey(); const inputs = useMemo(() => { const result = []; if (!securities) return result; for (const security of securities) if (security.type === "http" && security.scheme === "basic") { const fieldName = `header.Authorization`; result.push({ fieldName, original: security, defaultValue: { username: "", password: "" }, mapOutput(out) { if (out && typeof out === "object") return `Basic ${btoa(`${"username" in out ? out.username : ""}:${"password" in out ? out.password : ""}`)}`; return out; }, children: /* @__PURE__ */ jsx(ObjectInput, { field: { type: "object", properties: { username: { type: "string" }, password: { type: "string" } }, required: ["username", "password"] }, fieldName }) }); } else if (security.type === "oauth2") { const fieldName = "header.Authorization"; result.push({ fieldName, original: security, defaultValue: "Bearer ", children: /* @__PURE__ */ jsxs("fieldset", { className: "flex flex-col gap-2", children: [/* @__PURE__ */ jsx("label", { htmlFor: fieldName, className: cn(labelVariants()), children: "Access Token" }), /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [/* @__PURE__ */ jsx(FieldInput, { fieldName, isRequired: true, field: { type: "string" }, className: "flex-1" }), /* @__PURE__ */ jsx(OauthDialogTrigger, { type: "button", className: cn(buttonVariants({ size: "sm", color: "secondary" })), children: "Authorize" })] })] }) }); } else if (security.type === "http") { const fieldName = "header.Authorization"; result.push({ fieldName, original: security, defaultValue: "Bearer ", children: /* @__PURE__ */ jsx(FieldSet, { name: "Authorization (header)", fieldName, isRequired: true, field: { type: "string" } }) }); } else if (security.type === "apiKey") { const fieldName = `${security.in}.${security.name}`; result.push({ fieldName, defaultValue: "", original: security, children: /* @__PURE__ */ jsx(FieldSet, { fieldName, name: `${security.name} (${security.in})`, isRequired: true, field: { type: "string" } }) }); } else { const fieldName = "header.Authorization"; result.push({ fieldName, defaultValue: "", original: security, children: /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(FieldSet, { name: "Authorization (header)", isRequired: true, fieldName, field: { type: "string" } }), /* @__PURE__ */ jsx("p", { className: "text-fd-muted-foreground text-xs", children: "OpenID Connect is not supported at the moment, you can still set an access token here." })] }) }); } return transform ? transform(result) : result; }, [securities, transform]); const mapInputs = (values) => { const cloned = structuredClone(values); for (const item of inputs) { if (!item.mapOutput) continue; set(cloned, item.fieldName, item.mapOutput(get(cloned, item.fieldName))); } return cloned; }; const initAuthValues = (values) => { for (const item of inputs) { const stored = localStorage.getItem(storageKeys.AuthField(item)); if (stored) { const parsed = JSON.parse(stored); if (typeof parsed === typeof item.defaultValue) { set(values, item.fieldName, parsed); continue; } } set(values, item.fieldName, item.defaultValue); } return values; }; return { inputs, mapInputs, initAuthValues }; } function Route({ route, ...props }) { return /* @__PURE__ */ jsx("div", { ...props, className: cn("flex flex-row items-center gap-0.5 overflow-auto text-nowrap", props.className), children: route.split("/").map((part, index) => /* @__PURE__ */ jsxs(Fragment, { children: [index > 0 && /* @__PURE__ */ jsx("span", { className: "text-fd-muted-foreground", children: "/" }), part.startsWith("{") && part.endsWith("}") ? /* @__PURE__ */ jsx("code", { className: "bg-fd-primary/10 text-fd-primary", children: part }) : /* @__PURE__ */ jsx("code", { className: "text-fd-foreground", children: part })] }, index)) }); } function DefaultResultDisplay({ data, reset }) { const statusInfo = useMemo(() => getStatusInfo(data.status), [data.status]); const { shikiOptions } = useApiContext(); return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 p-3", children: [ /* @__PURE__ */ jsxs("div", { className: "flex justify-between items-center", children: [/* @__PURE__ */ jsxs("div", { className: "inline-flex items-center gap-1.5 text-sm font-medium text-fd-foreground", children: [/* @__PURE__ */ jsx(statusInfo.icon, { className: cn("size-4", statusInfo.color) }), statusInfo.description] }), /* @__PURE__ */ jsx("button", { type: "button", className: cn(buttonVariants({ size: "icon-xs" }), "p-0 text-fd-muted-foreground hover:text-fd-accent-foreground [&_svg]:size-3.5"), onClick: () => reset(), "aria-label": "Dismiss response", children: /* @__PURE__ */ jsx(X, {}) })] }), /* @__PURE__ */ jsx("p", { className: "text-sm text-fd-muted-foreground", children: data.status }), data.data !== void 0 && /* @__PURE__ */ jsx(DynamicCodeBlock, { lang: typeof data.data === "string" && data.data.length > 5e4 ? "text" : data.type, code: typeof data.data === "string" ? data.data : JSON.stringify(data.data, null, 2), options: shikiOptions }) ] }); } function CollapsiblePanel({ title, children, ...props }) { return /* @__PURE__ */ jsxs(Collapsible, { ...props, className: "border-b last:border-b-0", children: [/* @__PURE__ */ jsxs(CollapsibleTrigger, { className: "group w-full flex items-center gap-2 p-3 text-sm font-medium", children: [title, /* @__PURE__ */ jsx(ChevronDown, { className: "ms-auto size-3.5 text-fd-muted-foreground group-data-[state=open]:rotate-180" })] }), /* @__PURE__ */ jsx(CollapsibleContent, { children: /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-3 p-3 pt-1", children }) })] }); } const Custom = { useController(props) { return useController(props); } }; //#endregion export { Custom, PlaygroundClient as default }; //# sourceMappingURL=client.js.map