Skip to content

Commit be799b1

Browse files
committed
Update server url based on variable selection
1 parent d056253 commit be799b1

File tree

10 files changed

+172
-35
lines changed

10 files changed

+172
-35
lines changed

packages/gitbook/src/app/(space)/(content)/[[...pathname]]/page.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ export default async function Page(props: {
8989
// Display the page feedback in the page footer if the aside is not visible
9090
withPageFeedback && !page.layout.outline
9191
}
92+
searchParams={searchParams}
9293
/>
9394
{page.layout.outline ? (
9495
<PageAside

packages/gitbook/src/components/DocumentView/DocumentView.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ export interface DocumentContext {
4646
* https://linear.app/gitbook-x/issue/RND-3588/gitbook-open-code-syntax-highlighting-runs-out-of-memory-after-a
4747
*/
4848
shouldHighlightCode: (spaceId: string | undefined) => boolean;
49+
50+
searchParams?: Record<string, string>;
4951
}
5052

5153
export interface DocumentContextProps {

packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx

+32-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { DocumentBlockSwagger } from '@gitbook/api';
22
import { Icon } from '@gitbook/icons';
3-
import { OpenAPIOperation } from '@gitbook/react-openapi';
3+
import { OpenAPIOperation, OpenAPIOperationData } from '@gitbook/react-openapi';
44
import React from 'react';
55

66
import { LoadingPane } from '@/components/primitives';
@@ -34,8 +34,8 @@ export async function OpenAPI(props: BlockProps<DocumentBlockSwagger>) {
3434

3535
async function OpenAPIBody(props: BlockProps<DocumentBlockSwagger>) {
3636
const { block, context } = props;
37-
const { data, specUrl, error } = await fetchOpenAPIBlock(block, context.resolveContentRef);
3837

38+
const { data, specUrl, error } = await fetchOpenAPIBlock(block, context.resolveContentRef);
3939
if (error) {
4040
return (
4141
<div className={tcls('hidden')}>
@@ -50,6 +50,10 @@ async function OpenAPIBody(props: BlockProps<DocumentBlockSwagger>) {
5050
return null;
5151
}
5252

53+
const enumSelectors =
54+
context.searchParams && context.searchParams.block === block.key
55+
? parseModifiers(data, context.searchParams)
56+
: undefined;
5357
return (
5458
<OpenAPIOperation
5559
data={data}
@@ -61,6 +65,8 @@ async function OpenAPIBody(props: BlockProps<DocumentBlockSwagger>) {
6165
CodeBlock: PlainCodeBlock,
6266
defaultInteractiveOpened: context.mode === 'print',
6367
specUrl,
68+
enumSelectors,
69+
blockKey: block.key,
6470
}}
6571
className="openapi-block"
6672
/>
@@ -95,3 +101,27 @@ function OpenAPIFallback() {
95101
</div>
96102
);
97103
}
104+
105+
function parseModifiers(data: OpenAPIOperationData, params: Record<string, string>) {
106+
if (!data) {
107+
return;
108+
}
109+
const { servers } = params;
110+
const serverIndex =
111+
servers && !isNaN(Number(servers))
112+
? Math.min(0, Math.max(Number(servers), servers.length - 1))
113+
: 0;
114+
const server = data.servers[serverIndex];
115+
if (server && server.variables) {
116+
return Object.keys(server.variables).reduce<Record<string, number>>(
117+
(result, key) => {
118+
const selection = Number(params[key]);
119+
if (!isNaN(selection)) {
120+
result[key] = selection;
121+
}
122+
return result;
123+
},
124+
{ servers: serverIndex },
125+
);
126+
}
127+
}

packages/gitbook/src/components/PageBody/PageBody.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export function PageBody(props: {
3333
document: JSONDocument | null;
3434
context: ContentRefContext;
3535
withPageFeedback: boolean;
36+
searchParams: Record<string, string>;
3637
}) {
3738
const {
3839
space,
@@ -43,6 +44,7 @@ export function PageBody(props: {
4344
page,
4445
document,
4546
withPageFeedback,
47+
searchParams,
4648
} = props;
4749

4850
const asFullWidth = document ? hasFullWidthBlock(document) : false;
@@ -53,7 +55,6 @@ export function PageBody(props: {
5355
'siteId' in contentPointer
5456
? { organizationId: contentPointer.organizationId, siteId: contentPointer.siteId }
5557
: undefined;
56-
5758
return (
5859
<>
5960
<main
@@ -95,6 +96,7 @@ export function PageBody(props: {
9596
resolveContentRef: (ref, options) =>
9697
resolveContentRef(ref, context, options),
9798
shouldHighlightCode,
99+
searchParams,
98100
}}
99101
/>
100102
) : (

packages/react-openapi/src/OpenAPICodeSample.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export function OpenAPICodeSample(props: {
2323
const requestBodyContent = requestBody ? Object.entries(requestBody.content)[0] : undefined;
2424

2525
const input: CodeSampleInput = {
26-
url: getServersURL(data.servers) + data.path,
26+
url: getServersURL(data.servers, context.enumSelectors) + data.path,
2727
method: data.method,
2828
body: requestBodyContent
2929
? generateMediaTypeExample(requestBodyContent[1], { onlyRequired: true })

packages/react-openapi/src/OpenAPIOperation.tsx

+21-4
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@ import { OpenAPIOperationData, toJSON } from './fetchOpenAPIOperation';
55
import { Markdown } from './Markdown';
66
import { OpenAPICodeSample } from './OpenAPICodeSample';
77
import { OpenAPIResponseExample } from './OpenAPIResponseExample';
8-
import { OpenAPIServerURL } from './OpenAPIServerURL';
8+
import { getServersURL, OpenAPIServerURL } from './OpenAPIServerURL';
99
import { OpenAPISpec } from './OpenAPISpec';
1010
import { OpenAPIClientContext, OpenAPIContextProps } from './types';
1111
import { ApiClientModalProvider } from '@scalar/api-client-react';
1212

1313
/**
1414
* Display an interactive OpenAPI operation.
1515
*/
16-
export function OpenAPIOperation(props: {
16+
export async function OpenAPIOperation(props: {
1717
className?: string;
1818
data: OpenAPIOperationData;
1919
context: OpenAPIContextProps;
@@ -25,11 +25,14 @@ export function OpenAPIOperation(props: {
2525
defaultInteractiveOpened: context.defaultInteractiveOpened,
2626
specUrl: context.specUrl,
2727
icons: context.icons,
28+
blockKey: context.blockKey,
29+
enumSelectors: context.enumSelectors,
2830
};
2931

32+
const config = await getConfiguration(context);
3033
return (
3134
<ApiClientModalProvider
32-
configuration={{ spec: { url: context.specUrl } }}
35+
configuration={config}
3336
initialRequest={{ path: data.path, method: data.method }}
3437
>
3538
<div className={classNames('openapi-operation', className)}>
@@ -48,7 +51,7 @@ export function OpenAPIOperation(props: {
4851
{method.toUpperCase()}
4952
</span>
5053
<span className="openapi-url">
51-
<OpenAPIServerURL servers={servers} />
54+
<OpenAPIServerURL servers={servers} context={clientContext} />
5255
{path}
5356
</span>
5457
</div>
@@ -68,3 +71,17 @@ export function OpenAPIOperation(props: {
6871
</ApiClientModalProvider>
6972
);
7073
}
74+
75+
async function getConfiguration(context: OpenAPIContextProps) {
76+
const response = await fetch(context.specUrl);
77+
const doc = await response.json();
78+
79+
return {
80+
spec: {
81+
content: {
82+
...doc,
83+
servers: [{ url: getServersURL(doc.servers, context.enumSelectors) }],
84+
},
85+
},
86+
};
87+
}

packages/react-openapi/src/OpenAPIServerURL.tsx

+19-8
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
import * as React from 'react';
22
import { OpenAPIV3 } from 'openapi-types';
33
import { OpenAPIServerURLVariable } from './OpenAPIServerURLVariable';
4+
import { OpenAPIClientContext } from './types';
5+
import { ServerURLForm } from './OpenAPIServerURLForm';
46

57
/**
68
* Show the url of the server with variables replaced by their default values.
79
*/
8-
export function OpenAPIServerURL(props: { servers: OpenAPIV3.ServerObject[] }) {
9-
const { servers } = props;
10-
const server = servers[0];
11-
10+
export function OpenAPIServerURL(props: {
11+
servers: OpenAPIV3.ServerObject[];
12+
context: OpenAPIClientContext;
13+
}) {
14+
const { servers, context } = props;
15+
const serverIndex = context.enumSelectors?.servers ?? 0;
16+
const server = servers[serverIndex];
1217
const parts = parseServerURL(server?.url ?? '');
1318

1419
return (
15-
<span>
20+
<ServerURLForm context={context} server={server}>
1621
{parts.map((part, i) => {
1722
if (part.kind === 'text') {
1823
return <span key={i}>{part.text}</span>;
@@ -26,18 +31,22 @@ export function OpenAPIServerURL(props: { servers: OpenAPIV3.ServerObject[] }) {
2631
key={i}
2732
name={part.name}
2833
variable={server.variables[part.name]}
34+
enumIndex={context.enumSelectors?.[part.name]}
2935
/>
3036
);
3137
}
3238
})}
33-
</span>
39+
</ServerURLForm>
3440
);
3541
}
3642

3743
/**
3844
* Get the default URL for the server.
3945
*/
40-
export function getServersURL(servers: OpenAPIV3.ServerObject[]): string {
46+
export function getServersURL(
47+
servers: OpenAPIV3.ServerObject[],
48+
selectors?: Record<string, number>,
49+
): string {
4150
const server = servers[0];
4251
const parts = parseServerURL(server?.url ?? '');
4352

@@ -46,7 +55,9 @@ export function getServersURL(servers: OpenAPIV3.ServerObject[]): string {
4655
if (part.kind === 'text') {
4756
return part.text;
4857
} else {
49-
return server.variables?.[part.name]?.default ?? `{${part.name}}`;
58+
return selectors && !isNaN(selectors[part.name])
59+
? server.variables?.[part.name]?.enum?.[selectors[part.name]]
60+
: (server.variables?.[part.name]?.default ?? `{${part.name}}`);
5061
}
5162
})
5263
.join('');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
'use client';
2+
3+
import * as React from 'react';
4+
import { useRouter } from 'next/navigation';
5+
import { OpenAPIClientContext } from './types';
6+
import { OpenAPIV3 } from 'openapi-types';
7+
import { useApiClientModal } from '@scalar/api-client-react';
8+
9+
export function ServerURLForm(props: {
10+
children: React.ReactNode;
11+
context: OpenAPIClientContext;
12+
server: OpenAPIV3.ServerObject;
13+
}) {
14+
const { children, context, server } = props;
15+
const router = useRouter();
16+
const client = useApiClientModal();
17+
const [isPending, startTransition] = React.useTransition();
18+
19+
function updateServerUrl(formData: FormData) {
20+
startTransition(() => {
21+
if (!server.variables) {
22+
return;
23+
}
24+
let params = new URLSearchParams(`block=${context.blockKey}`);
25+
const variableKeys = Object.keys(server.variables);
26+
for (const pair of formData.entries()) {
27+
if (variableKeys.includes(pair[0]) && !isNaN(Number(pair[1]))) {
28+
params.set(pair[0], `${pair[1]}`);
29+
}
30+
}
31+
router.push(`?${params}`, { scroll: false });
32+
})
33+
}
34+
35+
return (
36+
<form action={updateServerUrl} className="contents">
37+
<fieldset disabled={isPending} className="contents">
38+
<input type="hidden" name="block" value={context.blockKey} />
39+
<span>{children}</span>
40+
</fieldset>
41+
</form>
42+
);
43+
}
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,63 @@
11
'use client';
2-
32
import * as React from 'react';
3+
import { useRouter } from 'next/navigation';
44
import classNames from 'classnames';
55
import { OpenAPIV3 } from 'openapi-types';
6+
import { OpenAPIClientContext } from './types';
67

78
/**
89
* Interactive component to show the value of a server variable and let the user change it.
910
*/
1011
export function OpenAPIServerURLVariable(props: {
1112
name: string;
1213
variable: OpenAPIV3.ServerVariableObject;
14+
enumIndex?: number;
1315
}) {
14-
const { variable } = props;
16+
const { enumIndex, name, variable } = props;
1517

1618
if (variable.enum && variable.enum.length > 0) {
17-
return (<select
18-
className={classNames(
19-
'openapi-section-select',
20-
'openapi-select',
21-
)}
22-
value={variable.default}
23-
>
24-
{
25-
variable.enum?.map((value: string) => {
26-
return (
27-
<option key={value} value={value}>
28-
{value}
29-
</option>
30-
);
31-
}) ?? null}
32-
</select>);
33-
19+
return (
20+
<EnumSelect
21+
name={name}
22+
variable={variable}
23+
value={
24+
!isNaN(Number(enumIndex))
25+
? enumIndex
26+
: variable.enum.findIndex((v) => v === variable.default)
27+
}
28+
/>
29+
);
3430
}
31+
3532
return <span className={classNames('openapi-url-var')}>{variable.default}</span>;
3633
}
34+
35+
/**
36+
* Render a select if there is an enum for a Server URL variable
37+
*/
38+
function EnumSelect(props: {
39+
value?: number;
40+
name: string;
41+
variable: OpenAPIV3.ServerVariableObject;
42+
}) {
43+
const { value, name, variable } = props;
44+
return (
45+
<select
46+
name={name}
47+
onChange={(e) => {
48+
e.preventDefault();
49+
e.currentTarget.form?.requestSubmit();
50+
}}
51+
className={classNames('openapi-select')}
52+
value={value}
53+
>
54+
{variable.enum?.map((value: string, index: number) => {
55+
return (
56+
<option key={value} value={index}>
57+
{value}
58+
</option>
59+
);
60+
}) ?? null}
61+
</select>
62+
);
63+
}

packages/react-openapi/src/types.ts

+4
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ export interface OpenAPIClientContext {
1818
* @default false
1919
*/
2020
defaultInteractiveOpened?: boolean;
21+
22+
blockKey?: string;
23+
24+
enumSelectors?: Record<string, number>;
2125
}
2226

2327
export interface OpenAPIFetcher {

0 commit comments

Comments
 (0)