Update server url based on variable selection

This commit is contained in:
Brett Jephson
2024-09-03 10:11:58 +01:00
parent c1193fd2cd
commit 1ae1715993
10 changed files with 168 additions and 33 deletions
@@ -90,6 +90,7 @@ export default async function Page(props: {
// Display the page feedback in the page footer if the aside is not visible
withPageFeedback && !page.layout.outline
}
searchParams={searchParams}
/>
{page.layout.outline ? (
<PageAside
@@ -46,6 +46,8 @@ export interface DocumentContext {
* https://linear.app/gitbook-x/issue/RND-3588/gitbook-open-code-syntax-highlighting-runs-out-of-memory-after-a
*/
shouldHighlightCode: (spaceId: string | undefined) => boolean;
searchParams?: Record<string, string>;
}
export interface DocumentContextProps {
@@ -1,6 +1,6 @@
import { DocumentBlockSwagger } from '@gitbook/api';
import { Icon } from '@gitbook/icons';
import { OpenAPIOperation } from '@gitbook/react-openapi';
import { OpenAPIOperation, OpenAPIOperationData } from '@gitbook/react-openapi';
import React from 'react';
import { LoadingPane } from '@/components/primitives';
@@ -45,6 +45,10 @@ async function OpenAPIBody(props: BlockProps<DocumentBlockSwagger>) {
return null;
}
const enumSelectors =
context.searchParams && context.searchParams.block === block.key
? parseModifiers(data, context.searchParams)
: undefined;
return (
<OpenAPIOperation
data={data}
@@ -56,6 +60,7 @@ async function OpenAPIBody(props: BlockProps<DocumentBlockSwagger>) {
CodeBlock: PlainCodeBlock,
defaultInteractiveOpened: context.mode === 'print',
id: block.meta?.id,
enumSelectors,
blockKey: block.key,
}}
className="openapi-block"
@@ -91,3 +96,27 @@ function OpenAPIFallback() {
</div>
);
}
function parseModifiers(data: OpenAPIOperationData, params: Record<string, string>) {
if (!data) {
return;
}
const { servers } = params;
const serverIndex =
servers && !isNaN(Number(servers))
? Math.min(0, Math.max(Number(servers), servers.length - 1))
: 0;
const server = data.servers[serverIndex];
if (server && server.variables) {
return Object.keys(server.variables).reduce<Record<string, number>>(
(result, key) => {
const selection = Number(params[key]);
if (!isNaN(selection)) {
result[key] = selection;
}
return result;
},
{ servers: serverIndex },
);
}
}
@@ -34,6 +34,7 @@ export function PageBody(props: {
document: JSONDocument | null;
context: ContentRefContext;
withPageFeedback: boolean;
searchParams: Record<string, string>;
}) {
const {
space,
@@ -44,6 +45,7 @@ export function PageBody(props: {
page,
document,
withPageFeedback,
searchParams,
} = props;
const asFullWidth = document ? hasFullWidthBlock(document) : false;
@@ -54,7 +56,6 @@ export function PageBody(props: {
'siteId' in contentPointer
? { organizationId: contentPointer.organizationId, siteId: contentPointer.siteId }
: undefined;
return (
<>
<main
@@ -97,6 +98,7 @@ export function PageBody(props: {
resolveContentRef: (ref, options) =>
resolveContentRef(ref, context, options),
shouldHighlightCode,
searchParams,
}}
/>
) : (
@@ -54,7 +54,7 @@ export function OpenAPICodeSample(props: {
const input: CodeSampleInput = {
url:
getServersURL(data.servers) +
getServersURL(data.servers, context.enumSelectors) +
data.path +
(searchParams.size ? `?${searchParams.toString()}` : ''),
method: data.method,
@@ -5,7 +5,7 @@ import { OpenAPIOperationData, toJSON } from './fetchOpenAPIOperation';
import { Markdown } from './Markdown';
import { OpenAPICodeSample } from './OpenAPICodeSample';
import { OpenAPIResponseExample } from './OpenAPIResponseExample';
import { OpenAPIServerURL } from './OpenAPIServerURL';
import { getServersURL, OpenAPIServerURL } from './OpenAPIServerURL';
import { OpenAPISpec } from './OpenAPISpec';
import { OpenAPIClientContext, OpenAPIContextProps } from './types';
import { ScalarApiClient } from './ScalarApiButton';
@@ -13,7 +13,7 @@ import { ScalarApiClient } from './ScalarApiButton';
/**
* Display an interactive OpenAPI operation.
*/
export function OpenAPIOperation(props: {
export async function OpenAPIOperation(props: {
className?: string;
data: OpenAPIOperationData;
context: OpenAPIContextProps;
@@ -25,8 +25,10 @@ export function OpenAPIOperation(props: {
defaultInteractiveOpened: context.defaultInteractiveOpened,
icons: context.icons,
blockKey: context.blockKey,
enumSelectors: context.enumSelectors,
};
const config = await getConfiguration(context);
return (
<ScalarApiClient>
<div className={classNames('openapi-operation', className)}>
@@ -47,7 +49,7 @@ export function OpenAPIOperation(props: {
{method.toUpperCase()}
</span>
<span className="openapi-url">
<OpenAPIServerURL servers={servers} />
<OpenAPIServerURL servers={servers} context={clientContext} />
{path}
</span>
</div>
@@ -67,3 +69,17 @@ export function OpenAPIOperation(props: {
</ScalarApiClient>
);
}
async function getConfiguration(context: OpenAPIContextProps) {
const response = await fetch(context.specUrl);
const doc = await response.json();
return {
spec: {
content: {
...doc,
servers: [{ url: getServersURL(doc.servers, context.enumSelectors) }],
},
},
};
}
@@ -1,18 +1,23 @@
import * as React from 'react';
import { OpenAPIV3 } from 'openapi-types';
import { OpenAPIServerURLVariable } from './OpenAPIServerURLVariable';
import { OpenAPIClientContext } from './types';
import { ServerURLForm } from './OpenAPIServerURLForm';
/**
* Show the url of the server with variables replaced by their default values.
*/
export function OpenAPIServerURL(props: { servers: OpenAPIV3.ServerObject[] }) {
const { servers } = props;
const server = servers[0];
export function OpenAPIServerURL(props: {
servers: OpenAPIV3.ServerObject[];
context: OpenAPIClientContext;
}) {
const { servers, context } = props;
const serverIndex = context.enumSelectors?.servers ?? 0;
const server = servers[serverIndex];
const parts = parseServerURL(server?.url ?? '');
return (
<span>
<ServerURLForm context={context} server={server}>
{parts.map((part, i) => {
if (part.kind === 'text') {
return <span key={i}>{part.text}</span>;
@@ -26,18 +31,22 @@ export function OpenAPIServerURL(props: { servers: OpenAPIV3.ServerObject[] }) {
key={i}
name={part.name}
variable={server.variables[part.name]}
enumIndex={context.enumSelectors?.[part.name]}
/>
);
}
})}
</span>
</ServerURLForm>
);
}
/**
* Get the default URL for the server.
*/
export function getServersURL(servers: OpenAPIV3.ServerObject[]): string {
export function getServersURL(
servers: OpenAPIV3.ServerObject[],
selectors?: Record<string, number>,
): string {
const server = servers[0];
const parts = parseServerURL(server?.url ?? '');
@@ -46,7 +55,9 @@ export function getServersURL(servers: OpenAPIV3.ServerObject[]): string {
if (part.kind === 'text') {
return part.text;
} else {
return server.variables?.[part.name]?.default ?? `{${part.name}}`;
return selectors && !isNaN(selectors[part.name])
? server.variables?.[part.name]?.enum?.[selectors[part.name]]
: (server.variables?.[part.name]?.default ?? `{${part.name}}`);
}
})
.join('');
@@ -0,0 +1,43 @@
'use client';
import * as React from 'react';
import { useRouter } from 'next/navigation';
import { OpenAPIClientContext } from './types';
import { OpenAPIV3 } from 'openapi-types';
import { useApiClientModal } from '@scalar/api-client-react';
export function ServerURLForm(props: {
children: React.ReactNode;
context: OpenAPIClientContext;
server: OpenAPIV3.ServerObject;
}) {
const { children, context, server } = props;
const router = useRouter();
const client = useApiClientModal();
const [isPending, startTransition] = React.useTransition();
function updateServerUrl(formData: FormData) {
startTransition(() => {
if (!server.variables) {
return;
}
let params = new URLSearchParams(`block=${context.blockKey}`);
const variableKeys = Object.keys(server.variables);
for (const pair of formData.entries()) {
if (variableKeys.includes(pair[0]) && !isNaN(Number(pair[1]))) {
params.set(pair[0], `${pair[1]}`);
}
}
router.push(`?${params}`, { scroll: false });
});
}
return (
<form action={updateServerUrl} className="contents">
<fieldset disabled={isPending} className="contents">
<input type="hidden" name="block" value={context.blockKey} />
<span>{children}</span>
</fieldset>
</form>
);
}
@@ -1,8 +1,9 @@
'use client';
import * as React from 'react';
import { useRouter } from 'next/navigation';
import classNames from 'classnames';
import { OpenAPIV3 } from 'openapi-types';
import { OpenAPIClientContext } from './types';
/**
* Interactive component to show the value of a server variable and let the user change it.
@@ -10,27 +11,53 @@ import { OpenAPIV3 } from 'openapi-types';
export function OpenAPIServerURLVariable(props: {
name: string;
variable: OpenAPIV3.ServerVariableObject;
enumIndex?: number;
}) {
const { variable } = props;
const { enumIndex, name, variable } = props;
if (variable.enum && variable.enum.length > 0) {
return (<select
className={classNames(
'openapi-section-select',
'openapi-select',
)}
value={variable.default}
>
{
variable.enum?.map((value: string) => {
return (
<option key={value} value={value}>
{value}
</option>
);
}) ?? null}
</select>);
return (
<EnumSelect
name={name}
variable={variable}
value={
!isNaN(Number(enumIndex))
? enumIndex
: variable.enum.findIndex((v) => v === variable.default)
}
/>
);
}
return <span className={classNames('openapi-url-var')}>{variable.default}</span>;
}
/**
* Render a select if there is an enum for a Server URL variable
*/
function EnumSelect(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>
);
}
+4
View File
@@ -21,6 +21,10 @@ export interface OpenAPIClientContext {
blockKey?: string;
/** Optional id attached to the OpenAPI Operation heading and used as an anchor */
id?: string;
blockKey?: string;
enumSelectors?: Record<string, number>;
}
export interface OpenAPIFetcher {