From 1ae1715993b1567f11feec8d9ae589b4d09f2f2a Mon Sep 17 00:00:00 2001 From: Brett Jephson Date: Tue, 3 Sep 2024 10:11:58 +0100 Subject: [PATCH] Update server url based on variable selection --- .../(content)/[[...pathname]]/page.tsx | 1 + .../components/DocumentView/DocumentView.tsx | 2 + .../DocumentView/OpenAPI/OpenAPI.tsx | 31 ++++++++- .../src/components/PageBody/PageBody.tsx | 4 +- .../react-openapi/src/OpenAPICodeSample.tsx | 2 +- .../react-openapi/src/OpenAPIOperation.tsx | 22 ++++++- .../react-openapi/src/OpenAPIServerURL.tsx | 27 +++++--- .../src/OpenAPIServerURLForm.tsx | 43 ++++++++++++ .../src/OpenAPIServerURLVariable.tsx | 65 +++++++++++++------ packages/react-openapi/src/types.ts | 4 ++ 10 files changed, 168 insertions(+), 33 deletions(-) create mode 100644 packages/react-openapi/src/OpenAPIServerURLForm.tsx diff --git a/packages/gitbook/src/app/(space)/(content)/[[...pathname]]/page.tsx b/packages/gitbook/src/app/(space)/(content)/[[...pathname]]/page.tsx index b5ab1424e..84a925860 100644 --- a/packages/gitbook/src/app/(space)/(content)/[[...pathname]]/page.tsx +++ b/packages/gitbook/src/app/(space)/(content)/[[...pathname]]/page.tsx @@ -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 ? ( boolean; + + searchParams?: Record; } export interface DocumentContextProps { diff --git a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx index 014a53193..ee96a211e 100644 --- a/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx +++ b/packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx @@ -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) { return null; } + const enumSelectors = + context.searchParams && context.searchParams.block === block.key + ? parseModifiers(data, context.searchParams) + : undefined; return ( ) { CodeBlock: PlainCodeBlock, defaultInteractiveOpened: context.mode === 'print', id: block.meta?.id, + enumSelectors, blockKey: block.key, }} className="openapi-block" @@ -91,3 +96,27 @@ function OpenAPIFallback() { ); } + +function parseModifiers(data: OpenAPIOperationData, params: Record) { + 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>( + (result, key) => { + const selection = Number(params[key]); + if (!isNaN(selection)) { + result[key] = selection; + } + return result; + }, + { servers: serverIndex }, + ); + } +} diff --git a/packages/gitbook/src/components/PageBody/PageBody.tsx b/packages/gitbook/src/components/PageBody/PageBody.tsx index 05440f9d9..98dd20f97 100644 --- a/packages/gitbook/src/components/PageBody/PageBody.tsx +++ b/packages/gitbook/src/components/PageBody/PageBody.tsx @@ -34,6 +34,7 @@ export function PageBody(props: { document: JSONDocument | null; context: ContentRefContext; withPageFeedback: boolean; + searchParams: Record; }) { 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 ( <>
resolveContentRef(ref, context, options), shouldHighlightCode, + searchParams, }} /> ) : ( diff --git a/packages/react-openapi/src/OpenAPICodeSample.tsx b/packages/react-openapi/src/OpenAPICodeSample.tsx index 265d57207..670d2712d 100644 --- a/packages/react-openapi/src/OpenAPICodeSample.tsx +++ b/packages/react-openapi/src/OpenAPICodeSample.tsx @@ -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, diff --git a/packages/react-openapi/src/OpenAPIOperation.tsx b/packages/react-openapi/src/OpenAPIOperation.tsx index 365cede51..a6f495097 100644 --- a/packages/react-openapi/src/OpenAPIOperation.tsx +++ b/packages/react-openapi/src/OpenAPIOperation.tsx @@ -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 (
@@ -47,7 +49,7 @@ export function OpenAPIOperation(props: { {method.toUpperCase()} - + {path}
@@ -67,3 +69,17 @@ export function OpenAPIOperation(props: {
); } + +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) }], + }, + }, + }; +} diff --git a/packages/react-openapi/src/OpenAPIServerURL.tsx b/packages/react-openapi/src/OpenAPIServerURL.tsx index e81fb47ba..79973d621 100644 --- a/packages/react-openapi/src/OpenAPIServerURL.tsx +++ b/packages/react-openapi/src/OpenAPIServerURL.tsx @@ -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 ( - + {parts.map((part, i) => { if (part.kind === 'text') { return {part.text}; @@ -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]} /> ); } })} - + ); } /** * Get the default URL for the server. */ -export function getServersURL(servers: OpenAPIV3.ServerObject[]): string { +export function getServersURL( + servers: OpenAPIV3.ServerObject[], + selectors?: Record, +): 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(''); diff --git a/packages/react-openapi/src/OpenAPIServerURLForm.tsx b/packages/react-openapi/src/OpenAPIServerURLForm.tsx new file mode 100644 index 000000000..565108417 --- /dev/null +++ b/packages/react-openapi/src/OpenAPIServerURLForm.tsx @@ -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 ( +
+
+ + {children} +
+
+ ); +} diff --git a/packages/react-openapi/src/OpenAPIServerURLVariable.tsx b/packages/react-openapi/src/OpenAPIServerURLVariable.tsx index 7bf93c997..5bc9d62bc 100644 --- a/packages/react-openapi/src/OpenAPIServerURLVariable.tsx +++ b/packages/react-openapi/src/OpenAPIServerURLVariable.tsx @@ -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 (); - + return ( + v === variable.default) + } + /> + ); } + return {variable.default}; } + +/** + * 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 ( + + ); +} diff --git a/packages/react-openapi/src/types.ts b/packages/react-openapi/src/types.ts index 156a28ac0..b278f2efd 100644 --- a/packages/react-openapi/src/types.ts +++ b/packages/react-openapi/src/types.ts @@ -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; } export interface OpenAPIFetcher {