mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-21 01:53:26 +00:00
Update server url based on variable selection
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user