mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-10-12 07:58:49 +00:00
Compare commits
66 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d9ee1a7ac4 | |||
| 846de0e1a9 | |||
| 3d9aefaf25 | |||
| 5beb2dbd69 | |||
| d19e3507cc | |||
| 152aad2c8f | |||
| 3ee149d87d | |||
| 0ccc5034c4 | |||
| 95e8772eab | |||
| 9675290e61 | |||
| c7ca921d7f | |||
| 2d4ddc9d95 | |||
| 65d77295e5 | |||
| 662df6f9aa | |||
| e866080867 | |||
| dce39c4c0d | |||
| 7b02f72660 | |||
| 74392eccee | |||
| 184d616701 | |||
| 9b78692124 | |||
| f67b143851 | |||
| f65e3f5fbb | |||
| ac492e094d | |||
| f6823e92bb | |||
| 9437aa25e8 | |||
| 77d64d06b9 | |||
| 1ae1715993 | |||
| c1193fd2cd | |||
| 60a9b95a25 | |||
| 586e66d03a | |||
| 39d88867ae | |||
| f98b7b2b00 | |||
| abf7ec35a6 | |||
| cb3c1f9f78 | |||
| ce890c609c | |||
| 5b933777e9 | |||
| 8d1c349ed1 | |||
| 4026fc1339 | |||
| 4b9f206e50 | |||
| f6b75e7dbd | |||
| 949f25cfa6 | |||
| 0b44528a1c | |||
| ea4f70ec88 | |||
| 768b555f66 | |||
| 96d5537087 | |||
| 28c59d81e7 | |||
| 3d147902d6 | |||
| a6adfaaf27 | |||
| d29a7a6f15 | |||
| 7244a0419a | |||
| c338bca2ba | |||
| 4f59d18bfc | |||
| b87ec84fd5 | |||
| e793a177f6 | |||
| 9573535eba | |||
| 087a783574 | |||
| 3b0f5d82d5 | |||
| 69414bdc52 | |||
| adaf0deb5b | |||
| d919d2ccfa | |||
| 2d15453e51 | |||
| ca4518d068 | |||
| cb454a3c4e | |||
| e852206573 | |||
| 7e79e14a62 | |||
| 4d476a8cbf |
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
'@gitbook/react-openapi': minor
|
||||||
|
'gitbook': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Allow selection of server url
|
||||||
@@ -3,6 +3,7 @@ import { Metadata, Viewport } from 'next';
|
|||||||
import { notFound, redirect } from 'next/navigation';
|
import { notFound, redirect } from 'next/navigation';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { serverUrlCache } from '@/components/DocumentView/OpenAPI/ServerUrlCache';
|
||||||
import { PageAside } from '@/components/PageAside';
|
import { PageAside } from '@/components/PageAside';
|
||||||
import { PageBody, PageCover } from '@/components/PageBody';
|
import { PageBody, PageCover } from '@/components/PageBody';
|
||||||
import { PageHrefContext, absoluteHref, pageHref } from '@/lib/links';
|
import { PageHrefContext, absoluteHref, pageHref } from '@/lib/links';
|
||||||
@@ -26,6 +27,8 @@ export default async function Page(props: {
|
|||||||
}) {
|
}) {
|
||||||
const { params, searchParams } = props;
|
const { params, searchParams } = props;
|
||||||
|
|
||||||
|
serverUrlCache.parse(searchParams);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
content: contentPointer,
|
content: contentPointer,
|
||||||
contentTarget,
|
contentTarget,
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import { LoadingPane } from '@/components/primitives';
|
|||||||
import { fetchOpenAPIBlock } from '@/lib/openapi';
|
import { fetchOpenAPIBlock } from '@/lib/openapi';
|
||||||
import { tcls } from '@/lib/tailwind';
|
import { tcls } from '@/lib/tailwind';
|
||||||
|
|
||||||
|
import OpenAPIClientStateContainer from './OpenAPIClientStateContainer';
|
||||||
|
import { serverUrlCache } from './ServerUrlCache';
|
||||||
import { BlockProps } from '../Block';
|
import { BlockProps } from '../Block';
|
||||||
import { PlainCodeBlock } from '../CodeBlock';
|
import { PlainCodeBlock } from '../CodeBlock';
|
||||||
|
|
||||||
@@ -17,7 +19,7 @@ import './scalar.css';
|
|||||||
* Render an OpenAPI block.
|
* Render an OpenAPI block.
|
||||||
*/
|
*/
|
||||||
export async function OpenAPI(props: BlockProps<DocumentBlockSwagger>) {
|
export async function OpenAPI(props: BlockProps<DocumentBlockSwagger>) {
|
||||||
const { block, style } = props;
|
const { style } = props;
|
||||||
return (
|
return (
|
||||||
<div className={tcls('w-full', 'flex', 'flex-row', style, 'max-w-full')}>
|
<div className={tcls('w-full', 'flex', 'flex-row', style, 'max-w-full')}>
|
||||||
<React.Suspense fallback={<OpenAPIFallback />}>
|
<React.Suspense fallback={<OpenAPIFallback />}>
|
||||||
@@ -45,21 +47,30 @@ async function OpenAPIBody(props: BlockProps<DocumentBlockSwagger>) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// To update the code sample we need to re-render the server component
|
||||||
|
// so reading the cached value from search params
|
||||||
|
const serverUrl = serverUrlCache.get('serverUrl');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OpenAPIOperation
|
<OpenAPIClientStateContainer block={block} servers={data.servers}>
|
||||||
data={data}
|
<OpenAPIOperation
|
||||||
context={{
|
data={data}
|
||||||
icons: {
|
context={{
|
||||||
chevronDown: <Icon icon="chevron-down" />,
|
icons: {
|
||||||
chevronRight: <Icon icon="chevron-right" />,
|
chevronDown: <Icon icon="chevron-down" />,
|
||||||
},
|
chevronRight: <Icon icon="chevron-right" />,
|
||||||
CodeBlock: PlainCodeBlock,
|
edit: <Icon icon="edit" />,
|
||||||
defaultInteractiveOpened: context.mode === 'print',
|
editDone: <Icon icon="check" />,
|
||||||
id: block.meta?.id,
|
},
|
||||||
blockKey: block.key,
|
CodeBlock: PlainCodeBlock,
|
||||||
}}
|
defaultInteractiveOpened: context.mode === 'print',
|
||||||
className="openapi-block"
|
id: block.meta?.id,
|
||||||
/>
|
blockKey: block.key,
|
||||||
|
serverUrl,
|
||||||
|
}}
|
||||||
|
className="openapi-block"
|
||||||
|
/>
|
||||||
|
</OpenAPIClientStateContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { DocumentBlock } from '@gitbook/api';
|
||||||
|
import { OpenAPIClientState } from '@gitbook/react-openapi/client';
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
import { OpenAPIV3 } from 'openapi-types';
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Client component that wraps `OpenAPIClientState` so we can
|
||||||
|
* use some hooks (e.g. useRouter) in the `onUpdate` callback.
|
||||||
|
*/
|
||||||
|
export default function OpenAPIClientStateContainer(props: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
block: DocumentBlock;
|
||||||
|
servers: OpenAPIV3.ServerObject[];
|
||||||
|
}) {
|
||||||
|
const { block, children, servers } = props;
|
||||||
|
const [isPending, startTransition] = React.useTransition();
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OpenAPIClientState
|
||||||
|
isPending={isPending}
|
||||||
|
servers={servers}
|
||||||
|
params={
|
||||||
|
searchParams.get('block') === block.key
|
||||||
|
? Object.fromEntries(searchParams.entries())
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onUpdate={async (params) => {
|
||||||
|
startTransition(() => {
|
||||||
|
const queryParams = new URLSearchParams(params ?? '');
|
||||||
|
router.replace(`?${queryParams}`, { scroll: false });
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</OpenAPIClientState>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { createSearchParamsCache, parseAsString } from 'nuqs/server';
|
||||||
|
|
||||||
|
export const serverUrlCache = createSearchParamsCache({
|
||||||
|
serverUrl: parseAsString,
|
||||||
|
});
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
/** URL */
|
/** URL */
|
||||||
|
|
||||||
.openapi-url {
|
.openapi-url {
|
||||||
@apply font-mono text-sm text-dark/8 dark:text-light/8;
|
@apply flex items-center font-mono text-sm text-dark/8 dark:text-light/8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.openapi-url-var {
|
.openapi-url-var {
|
||||||
@@ -364,3 +364,24 @@
|
|||||||
.openapi-markdown > *:last-child {
|
.openapi-markdown > *:last-child {
|
||||||
@apply mb-0;
|
@apply mb-0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.openapi-select-button,
|
||||||
|
.openapi-edit-button {
|
||||||
|
@apply p-0.5 inline-flex text-dark/6 dark:text-light/6 hover:opacity-8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-edit-button {
|
||||||
|
@apply p-0.5 ml-4 size-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-edit-button > * {
|
||||||
|
@apply size-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-select-button {
|
||||||
|
@apply leading-[1cap] disabled:opacity-5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openapi-pending {
|
||||||
|
@apply opacity-5;
|
||||||
|
}
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ export function PageBody(props: {
|
|||||||
const language = getSpaceLanguage(customization);
|
const language = getSpaceLanguage(customization);
|
||||||
const updatedAt = page.updatedAt ?? page.createdAt;
|
const updatedAt = page.updatedAt ?? page.createdAt;
|
||||||
const shouldHighlightCode = createHighlightingContext();
|
const shouldHighlightCode = createHighlightingContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<main
|
<main
|
||||||
|
|||||||
@@ -6,7 +6,8 @@
|
|||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"development": "./src/index.ts",
|
"development": "./src/index.ts",
|
||||||
"default": "./dist/index.js"
|
"default": "./dist/index.js"
|
||||||
}
|
},
|
||||||
|
"./client": "./src/client.ts"
|
||||||
},
|
},
|
||||||
"version": "0.6.0",
|
"version": "0.6.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import { getServersURL } from './utils';
|
||||||
|
import { OpenAPIV3 } from 'openapi-types';
|
||||||
|
|
||||||
|
type OpenAPIClientStateContextProps = {
|
||||||
|
/**
|
||||||
|
* Whether client state updates are in a pending state,
|
||||||
|
* i.e. is a transition in progress.
|
||||||
|
*/
|
||||||
|
isPending?: boolean;
|
||||||
|
/**
|
||||||
|
* The server url
|
||||||
|
*/
|
||||||
|
serverUrl?: string;
|
||||||
|
/**
|
||||||
|
* The current state
|
||||||
|
*/
|
||||||
|
state?: Record<string, string> | null;
|
||||||
|
/**
|
||||||
|
* Callback for when the client state is updated
|
||||||
|
*/
|
||||||
|
onUpdate: (params: Record<string, string> | null) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const OpenAPIClientStateContext = React.createContext<OpenAPIClientStateContextProps | null>(null);
|
||||||
|
|
||||||
|
export function useOpenAPIClientState() {
|
||||||
|
return React.useContext(OpenAPIClientStateContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Control client state for an OpenAPI operation
|
||||||
|
*/
|
||||||
|
export function OpenAPIClientState(props: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
servers: OpenAPIV3.ServerObject[];
|
||||||
|
isPending?: boolean;
|
||||||
|
params?: Record<string, string>;
|
||||||
|
onUpdate: OpenAPIClientStateContextProps['onUpdate'];
|
||||||
|
}) {
|
||||||
|
const { children, servers, isPending, params, onUpdate } = props;
|
||||||
|
|
||||||
|
const clientState = React.useMemo(() => {
|
||||||
|
if (!params) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return parseClientStateModifiers(servers, params);
|
||||||
|
}, [servers, params]);
|
||||||
|
const serverUrl = getServersURL(servers, clientState ?? undefined);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OpenAPIClientStateContext.Provider
|
||||||
|
value={{
|
||||||
|
isPending,
|
||||||
|
state: clientState,
|
||||||
|
serverUrl,
|
||||||
|
onUpdate,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</OpenAPIClientStateContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseClientStateModifiers(
|
||||||
|
servers: OpenAPIV3.ServerObject[],
|
||||||
|
params: Record<string, string>,
|
||||||
|
) {
|
||||||
|
if (!servers) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const serverQueryParam = params['server'];
|
||||||
|
const serverIndex =
|
||||||
|
serverQueryParam && !isNaN(Number(serverQueryParam))
|
||||||
|
? Math.max(0, Math.min(Number(serverQueryParam), servers.length - 1))
|
||||||
|
: 0;
|
||||||
|
const server = servers[serverIndex];
|
||||||
|
return server
|
||||||
|
? Object.keys(server.variables ?? {}).reduce<Record<string, string>>(
|
||||||
|
(result, key) => {
|
||||||
|
const selection = params[key];
|
||||||
|
if (!isNaN(Number(selection))) {
|
||||||
|
result[key] = selection;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
{ server: `${serverIndex}`, edit: params['edit'] },
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
@@ -4,10 +4,9 @@ import { CodeSampleInput, codeSampleGenerators } from './code-samples';
|
|||||||
import { OpenAPIOperationData, toJSON } from './fetchOpenAPIOperation';
|
import { OpenAPIOperationData, toJSON } from './fetchOpenAPIOperation';
|
||||||
import { generateMediaTypeExample, generateSchemaExample } from './generateSchemaExample';
|
import { generateMediaTypeExample, generateSchemaExample } from './generateSchemaExample';
|
||||||
import { InteractiveSection } from './InteractiveSection';
|
import { InteractiveSection } from './InteractiveSection';
|
||||||
import { getServersURL } from './OpenAPIServerURL';
|
|
||||||
import { ScalarApiButton } from './ScalarApiButton';
|
import { ScalarApiButton } from './ScalarApiButton';
|
||||||
import { OpenAPIContextProps } from './types';
|
import { OpenAPIContextProps } from './types';
|
||||||
import { noReference } from './utils';
|
import { getServersURL, noReference } from './utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display code samples to execute the operation.
|
* Display code samples to execute the operation.
|
||||||
@@ -49,14 +48,11 @@ export function OpenAPICodeSample(props: {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const serverUrl = context.serverUrl ?? getServersURL(data.servers);
|
||||||
const requestBody = noReference(data.operation.requestBody);
|
const requestBody = noReference(data.operation.requestBody);
|
||||||
const requestBodyContent = requestBody ? Object.entries(requestBody.content)[0] : undefined;
|
const requestBodyContent = requestBody ? Object.entries(requestBody.content)[0] : undefined;
|
||||||
|
|
||||||
const input: CodeSampleInput = {
|
const input: CodeSampleInput = {
|
||||||
url:
|
url: serverUrl + data.path + (searchParams.size ? `?${searchParams.toString()}` : ''),
|
||||||
getServersURL(data.servers) +
|
|
||||||
data.path +
|
|
||||||
(searchParams.size ? `?${searchParams.toString()}` : ''),
|
|
||||||
method: data.method,
|
method: data.method,
|
||||||
body: requestBodyContent
|
body: requestBodyContent
|
||||||
? generateMediaTypeExample(requestBodyContent[1], { onlyRequired: true })
|
? generateMediaTypeExample(requestBodyContent[1], { onlyRequired: true })
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export function OpenAPIOperation(props: {
|
|||||||
{operation.description ? (
|
{operation.description ? (
|
||||||
<Markdown className="openapi-description" source={operation.description} />
|
<Markdown className="openapi-description" source={operation.description} />
|
||||||
) : null}
|
) : null}
|
||||||
<div className="openapi-target">
|
<div className="openapi-target flex items-center">
|
||||||
<span
|
<span
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'openapi-method',
|
'openapi-method',
|
||||||
@@ -47,8 +47,11 @@ export function OpenAPIOperation(props: {
|
|||||||
{method.toUpperCase()}
|
{method.toUpperCase()}
|
||||||
</span>
|
</span>
|
||||||
<span className="openapi-url">
|
<span className="openapi-url">
|
||||||
<OpenAPIServerURL servers={servers} />
|
<OpenAPIServerURL
|
||||||
{path}
|
servers={servers}
|
||||||
|
context={clientContext}
|
||||||
|
path={path}
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,66 +1,62 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { OpenAPIV3 } from 'openapi-types';
|
import { OpenAPIV3 } from 'openapi-types';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { OpenAPIServerURLVariable } from './OpenAPIServerURLVariable';
|
import { OpenAPIServerURLVariable } from './OpenAPIServerURLVariable';
|
||||||
|
import { OpenAPIClientContext } from './types';
|
||||||
|
import { ServerURLForm } from './OpenAPIServerURLForm';
|
||||||
|
import { useOpenAPIClientState } from './OpenAPIClientStateContext';
|
||||||
|
import { parseServerURL } from './utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show the url of the server with variables replaced by their default values.
|
* Show the url of the server, where there are variables they are replaced by their default values.
|
||||||
|
* If a stateContext is provided with an `onUpdate` callback then the variables can be edited and
|
||||||
|
* if there are multiple servers available they can be selected.
|
||||||
*/
|
*/
|
||||||
export function OpenAPIServerURL(props: { servers: OpenAPIV3.ServerObject[] }) {
|
export function OpenAPIServerURL(props: {
|
||||||
const { servers } = props;
|
servers: OpenAPIV3.ServerObject[];
|
||||||
const server = servers[0];
|
context: OpenAPIClientContext;
|
||||||
|
path?: string;
|
||||||
|
}) {
|
||||||
|
const { path, servers, context } = props;
|
||||||
|
const stateContext = useOpenAPIClientState();
|
||||||
|
|
||||||
|
const serverIndex = !isNaN(Number(stateContext?.state?.server))
|
||||||
|
? Number(stateContext?.state?.server)
|
||||||
|
: 0;
|
||||||
|
const server = servers[serverIndex];
|
||||||
const parts = parseServerURL(server?.url ?? '');
|
const parts = parseServerURL(server?.url ?? '');
|
||||||
|
|
||||||
|
if (!server) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span>
|
<ServerURLForm context={context} servers={servers} serverIndex={serverIndex}>
|
||||||
{parts.map((part, i) => {
|
<span className={classNames(stateContext?.isPending && 'openapi-pending')}>
|
||||||
if (part.kind === 'text') {
|
{parts.map((part, i) => {
|
||||||
return <span key={i}>{part.text}</span>;
|
if (part.kind === 'text') {
|
||||||
} else {
|
return <span key={i}>{part.text}</span>;
|
||||||
if (!server.variables?.[part.name]) {
|
} else {
|
||||||
return <span key={i}>{`{${part.name}}`}</span>;
|
if (!server.variables?.[part.name]) {
|
||||||
}
|
return <span key={i}>{`{${part.name}}`}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OpenAPIServerURLVariable
|
<OpenAPIServerURLVariable
|
||||||
key={i}
|
key={i}
|
||||||
name={part.name}
|
name={part.name}
|
||||||
variable={server.variables[part.name]}
|
variable={server.variables[part.name]}
|
||||||
/>
|
selectionIndex={Number(stateContext?.state?.[part.name])}
|
||||||
);
|
selectable={Boolean(stateContext?.state?.edit)}
|
||||||
}
|
/>
|
||||||
})}
|
);
|
||||||
</span>
|
}
|
||||||
|
})}
|
||||||
|
{path}
|
||||||
|
</span>
|
||||||
|
</ServerURLForm>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the default URL for the server.
|
|
||||||
*/
|
|
||||||
export function getServersURL(servers: OpenAPIV3.ServerObject[]): string {
|
|
||||||
const server = servers[0];
|
|
||||||
const parts = parseServerURL(server?.url ?? '');
|
|
||||||
|
|
||||||
return parts
|
|
||||||
.map((part) => {
|
|
||||||
if (part.kind === 'text') {
|
|
||||||
return part.text;
|
|
||||||
} else {
|
|
||||||
return server.variables?.[part.name]?.default ?? `{${part.name}}`;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseServerURL(url: string) {
|
|
||||||
const parts = url.split(/{([^}]+)}/g);
|
|
||||||
const result: Array<{ kind: 'variable'; name: string } | { kind: 'text'; text: string }> = [];
|
|
||||||
for (let i = 0; i < parts.length; i++) {
|
|
||||||
if (i % 2 === 0) {
|
|
||||||
result.push({ kind: 'text', text: parts[i] });
|
|
||||||
} else {
|
|
||||||
result.push({ kind: 'variable', name: parts[i] });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import { OpenAPIClientContext } from './types';
|
||||||
|
import { OpenAPIV3 } from 'openapi-types';
|
||||||
|
import { ServerSelector } from './ServerSelector';
|
||||||
|
import { useOpenAPIClientState } from './OpenAPIClientStateContext';
|
||||||
|
import { getServersURL } from './utils';
|
||||||
|
|
||||||
|
export function ServerURLForm(props: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
context: OpenAPIClientContext;
|
||||||
|
servers: OpenAPIV3.ServerObject[];
|
||||||
|
serverIndex: number;
|
||||||
|
}) {
|
||||||
|
const { children, context, servers, serverIndex } = props;
|
||||||
|
const stateContext = useOpenAPIClientState();
|
||||||
|
const server = servers[serverIndex];
|
||||||
|
const formRef = React.useRef<HTMLFormElement>(null);
|
||||||
|
|
||||||
|
function switchServer(index: number) {
|
||||||
|
if (index !== serverIndex) {
|
||||||
|
update({
|
||||||
|
server: index ? `${index}` : '0',
|
||||||
|
...(stateContext?.state?.edit ? { edit: 'true' } : undefined),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateServerVariables(formData: FormData) {
|
||||||
|
const variableKeys = Object.keys(server.variables ?? {});
|
||||||
|
const variables: Record<string, string> = {};
|
||||||
|
for (const pair of formData) {
|
||||||
|
if (variableKeys.includes(pair[0]) && !isNaN(Number(pair[1]))) {
|
||||||
|
variables[pair[0]] = `${pair[1]}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
update({
|
||||||
|
server: formData.has('server') ? `${formData.get('server')}` : '0',
|
||||||
|
...variables,
|
||||||
|
...(stateContext?.state?.edit ? { edit: 'true' } : undefined),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function update(variables?: Record<string, string>) {
|
||||||
|
if (!context.blockKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stateContext?.onUpdate({
|
||||||
|
block: context.blockKey,
|
||||||
|
...variables,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only make the server url editable if there is some onUpdate callback
|
||||||
|
// and if there are variations on the server url (e.g. an array of servers or url variables).
|
||||||
|
const isEditable = stateContext?.onUpdate && (servers.length > 1 || server.variables);
|
||||||
|
const isEditing = isEditable && stateContext?.state?.edit;
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
ref={formRef}
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
updateServerVariables(new FormData(e.currentTarget));
|
||||||
|
}}
|
||||||
|
className="contents"
|
||||||
|
>
|
||||||
|
<fieldset disabled={stateContext?.isPending} className="contents">
|
||||||
|
<input type="hidden" name="block" value={context.blockKey} />
|
||||||
|
{children}
|
||||||
|
{isEditing && servers.length > 1 ? (
|
||||||
|
<ServerSelector
|
||||||
|
lastIndex={servers.length - 1}
|
||||||
|
currentIndex={serverIndex}
|
||||||
|
onChange={switchServer}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{isEditable ? (
|
||||||
|
<button
|
||||||
|
className="openapi-edit-button ml-2"
|
||||||
|
onClick={() => {
|
||||||
|
const state = { ...stateContext?.state };
|
||||||
|
delete state.edit;
|
||||||
|
update({
|
||||||
|
server: `${serverIndex}`,
|
||||||
|
...state,
|
||||||
|
...(isEditing
|
||||||
|
? { serverUrl: getServersURL(servers, state) }
|
||||||
|
: { edit: 'true' }),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
title={isEditing ? undefined : 'Try different server options'}
|
||||||
|
aria-label={isEditing ? 'Clear' : 'Edit'}
|
||||||
|
>
|
||||||
|
{isEditing ? context.icons.editDone : context.icons.edit}
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { OpenAPIV3 } from 'openapi-types';
|
import { OpenAPIV3 } from 'openapi-types';
|
||||||
@@ -10,7 +9,64 @@ import { OpenAPIV3 } from 'openapi-types';
|
|||||||
export function OpenAPIServerURLVariable(props: {
|
export function OpenAPIServerURLVariable(props: {
|
||||||
name: string;
|
name: string;
|
||||||
variable: OpenAPIV3.ServerVariableObject;
|
variable: OpenAPIV3.ServerVariableObject;
|
||||||
|
selectionIndex?: number;
|
||||||
|
selectable: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { variable } = props;
|
const { selectable, selectionIndex, name, variable } = props;
|
||||||
|
|
||||||
|
if (variable.enum && variable.enum.length > 0) {
|
||||||
|
if (!selectable) {
|
||||||
|
return (
|
||||||
|
<span className={classNames('openapi-url-var')}>
|
||||||
|
{!isNaN(Number(selectionIndex))
|
||||||
|
? variable.enum[Number(selectionIndex)]
|
||||||
|
: variable.default}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VariableSelector
|
||||||
|
name={name}
|
||||||
|
variable={variable}
|
||||||
|
value={
|
||||||
|
!isNaN(Number(selectionIndex))
|
||||||
|
? selectionIndex
|
||||||
|
: variable.enum.findIndex((v) => v === variable.default)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return <span className={classNames('openapi-url-var')}>{variable.default}</span>;
|
return <span className={classNames('openapi-url-var')}>{variable.default}</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a select if there is an enum for a Server URL variable
|
||||||
|
*/
|
||||||
|
function VariableSelector(props: {
|
||||||
|
value?: number;
|
||||||
|
name: string;
|
||||||
|
variable: OpenAPIV3.ServerVariableObject;
|
||||||
|
}) {
|
||||||
|
const { value, name, variable } = props;
|
||||||
|
return (
|
||||||
|
<select
|
||||||
|
name={name}
|
||||||
|
onChange={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.currentTarget.form?.requestSubmit();
|
||||||
|
}}
|
||||||
|
className={classNames('openapi-select')}
|
||||||
|
value={value}
|
||||||
|
>
|
||||||
|
{variable.enum?.map((value: string, index: number) => {
|
||||||
|
return (
|
||||||
|
<option key={value} value={index}>
|
||||||
|
{value}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
}) ?? null}
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { OpenAPIOperationData, fromJSON } from './fetchOpenAPIOperation';
|
import { OpenAPIOperationData, fromJSON } from './fetchOpenAPIOperation';
|
||||||
|
import { useOpenAPIClientState } from './OpenAPIClientStateContext';
|
||||||
|
|
||||||
const ApiClientReact = React.lazy(async () => {
|
const ApiClientReact = React.lazy(async () => {
|
||||||
const mod = await import('@scalar/api-client-react');
|
const mod = await import('@scalar/api-client-react');
|
||||||
@@ -58,6 +59,8 @@ export function ScalarApiButton(props: {
|
|||||||
export function ScalarApiClient(props: { children: React.ReactNode }) {
|
export function ScalarApiClient(props: { children: React.ReactNode }) {
|
||||||
const { children } = props;
|
const { children } = props;
|
||||||
|
|
||||||
|
const stateCtx = useOpenAPIClientState();
|
||||||
|
|
||||||
const [active, setActive] = React.useState<null | {
|
const [active, setActive] = React.useState<null | {
|
||||||
operationData: OpenAPIOperationData | null;
|
operationData: OpenAPIOperationData | null;
|
||||||
}>(null);
|
}>(null);
|
||||||
@@ -125,12 +128,12 @@ export function ScalarApiClient(props: { children: React.ReactNode }) {
|
|||||||
headers: request.headers.map((header: Header) => {
|
headers: request.headers.map((header: Header) => {
|
||||||
return { ...header, enabled: true };
|
return { ...header, enabled: true };
|
||||||
}),
|
}),
|
||||||
url: operationData.servers[0]?.url,
|
url: stateCtx?.serverUrl ?? operationData.servers[0]?.url,
|
||||||
body: request.postData?.text,
|
body: request.postData?.text,
|
||||||
};
|
};
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}, [active]);
|
}, [active, stateCtx?.serverUrl]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScalarContext.Provider value={open}>
|
<ScalarContext.Provider value={open}>
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
export function ServerSelector(props: {
|
||||||
|
currentIndex: number;
|
||||||
|
lastIndex: number;
|
||||||
|
onChange: (value: number) => void;
|
||||||
|
}) {
|
||||||
|
const { currentIndex, onChange, lastIndex } = props;
|
||||||
|
const [index, setIndex] = React.useState(currentIndex);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
onChange(index);
|
||||||
|
}, [index]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="inline-flex pl-4 gap-2">
|
||||||
|
<input type="hidden" value={`${index}`} name="server" />
|
||||||
|
<button
|
||||||
|
className="openapi-select-button"
|
||||||
|
disabled={index === 0}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setIndex((index) => index - 1);
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
aria-label="Previous"
|
||||||
|
>
|
||||||
|
◀
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="openapi-select-button"
|
||||||
|
disabled={index >= lastIndex}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setIndex((index) => index + 1);
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
aria-label="Next"
|
||||||
|
>
|
||||||
|
▶
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
'use client';
|
||||||
|
export { OpenAPIClientState } from './OpenAPIClientStateContext';
|
||||||
@@ -8,6 +8,8 @@ export interface OpenAPIClientContext {
|
|||||||
icons: {
|
icons: {
|
||||||
chevronDown: React.ReactNode;
|
chevronDown: React.ReactNode;
|
||||||
chevronRight: React.ReactNode;
|
chevronRight: React.ReactNode;
|
||||||
|
edit: React.ReactNode;
|
||||||
|
editDone: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -15,12 +17,20 @@ export interface OpenAPIClientContext {
|
|||||||
* @default false
|
* @default false
|
||||||
*/
|
*/
|
||||||
defaultInteractiveOpened?: boolean;
|
defaultInteractiveOpened?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The key of the block
|
* The key of the block
|
||||||
*/
|
*/
|
||||||
blockKey?: string;
|
blockKey?: string;
|
||||||
/** Optional id attached to the OpenAPI Operation heading and used as an anchor */
|
|
||||||
|
/**
|
||||||
|
* Optional id attached to the OpenAPI Operation heading and used as an anchor
|
||||||
|
*/
|
||||||
id?: string;
|
id?: string;
|
||||||
|
/**
|
||||||
|
* Optional serverUrl to use with OpenAPI operations
|
||||||
|
*/
|
||||||
|
serverUrl?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OpenAPIFetcher {
|
export interface OpenAPIFetcher {
|
||||||
|
|||||||
@@ -11,3 +11,41 @@ export function noReference<T>(input: T | OpenAPIV3.ReferenceObject): T {
|
|||||||
export function createStateKey(key: string, scope?: string) {
|
export function createStateKey(key: string, scope?: string) {
|
||||||
return scope ? `${scope}_${key}` : key;
|
return scope ? `${scope}_${key}` : key;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the default URL for the server.
|
||||||
|
*/
|
||||||
|
export function getServersURL(
|
||||||
|
servers: OpenAPIV3.ServerObject[],
|
||||||
|
selectors?: Record<string, string>,
|
||||||
|
): string {
|
||||||
|
const serverIndex =
|
||||||
|
selectors && !isNaN(Number(selectors.server)) ? Number(selectors.server) : 0;
|
||||||
|
const server = servers[serverIndex];
|
||||||
|
const parts = parseServerURL(server?.url ?? '');
|
||||||
|
|
||||||
|
return parts
|
||||||
|
.map((part) => {
|
||||||
|
if (part.kind === 'text') {
|
||||||
|
return part.text;
|
||||||
|
} else {
|
||||||
|
return selectors && !isNaN(Number(selectors[part.name]))
|
||||||
|
? server.variables?.[part.name]?.enum?.[Number(selectors[part.name])]
|
||||||
|
: (server.variables?.[part.name]?.default ?? `{${part.name}}`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseServerURL(url: string) {
|
||||||
|
const parts = url.split(/{([^}]+)}/g);
|
||||||
|
const result: Array<{ kind: 'variable'; name: string } | { kind: 'text'; text: string }> = [];
|
||||||
|
for (let i = 0; i < parts.length; i++) {
|
||||||
|
if (i % 2 === 0) {
|
||||||
|
result.push({ kind: 'text', text: parts[i] });
|
||||||
|
} else {
|
||||||
|
result.push({ kind: 'variable', name: parts[i] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user