fumadocs-openapi
Version:
Generate MDX docs for your OpenAPI spec
459 lines (456 loc) • 16.4 kB
JavaScript
'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