mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-16 15:45:13 +00:00
fix: rollback scalar (#2454)
Co-authored-by: Brett Jephson <brett@gitbook.io>
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
'@gitbook/react-openapi': minor
|
||||||
|
'gitbook': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Rollback of scalar modal while fixing perf issue
|
||||||
@@ -28,5 +28,8 @@
|
|||||||
},
|
},
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
"@vercel/next@4.3.6": "patches/@vercel%2Fnext@4.3.6.patch"
|
"@vercel/next@4.3.6": "patches/@vercel%2Fnext@4.3.6.patch"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@scalar/api-client-react": "0.3.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export async function OpenAPI(props: BlockProps<DocumentBlockSwagger>) {
|
|||||||
|
|
||||||
async function OpenAPIBody(props: BlockProps<DocumentBlockSwagger>) {
|
async function OpenAPIBody(props: BlockProps<DocumentBlockSwagger>) {
|
||||||
const { block, context } = props;
|
const { block, context } = props;
|
||||||
const { data, specUrl, error } = await fetchOpenAPIBlock(block, context.resolveContentRef);
|
const { data, error } = await fetchOpenAPIBlock(block, context.resolveContentRef);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
@@ -41,7 +41,7 @@ async function OpenAPIBody(props: BlockProps<DocumentBlockSwagger>) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data || !specUrl) {
|
if (!data) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,7 +55,6 @@ async function OpenAPIBody(props: BlockProps<DocumentBlockSwagger>) {
|
|||||||
},
|
},
|
||||||
CodeBlock: PlainCodeBlock,
|
CodeBlock: PlainCodeBlock,
|
||||||
defaultInteractiveOpened: context.mode === 'print',
|
defaultInteractiveOpened: context.mode === 'print',
|
||||||
specUrl,
|
|
||||||
id: block.meta?.id,
|
id: block.meta?.id,
|
||||||
}}
|
}}
|
||||||
className="openapi-block"
|
className="openapi-block"
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
@import '@scalar/api-client-react/style.css';
|
|
||||||
|
|
||||||
.light .scalar-modal-layout,
|
.light .scalar-modal-layout,
|
||||||
.light .scalar-app,
|
|
||||||
.light .scalar {
|
.light .scalar {
|
||||||
--scalar-color-1: color-mix(
|
--scalar-color-1: color-mix(
|
||||||
in srgb,
|
in srgb,
|
||||||
@@ -53,13 +50,8 @@
|
|||||||
--scalar-button-1: rgb(49 53 56);
|
--scalar-button-1: rgb(49 53 56);
|
||||||
--scalar-button-1-color: #fff;
|
--scalar-button-1-color: #fff;
|
||||||
--scalar-button-1-hover: rgb(28 31 33);
|
--scalar-button-1-hover: rgb(28 31 33);
|
||||||
|
|
||||||
--scalar-shadow-1: 0 1px 3px 0 rgba(0, 0, 0, 0.11);
|
|
||||||
--scalar-shadow-2: rgba(0, 0, 0, 0.08) 0px 13px 20px 0px, rgba(0, 0, 0, 0.08) 0px 3px 8px 0px,
|
|
||||||
#eeeeed 0px 0 0 1px;
|
|
||||||
}
|
}
|
||||||
.dark .scalar-modal-layout,
|
.dark .scalar-modal-layout,
|
||||||
.dark .scalar-app,
|
|
||||||
.dark .scalar {
|
.dark .scalar {
|
||||||
--scalar-color-1: color-mix(
|
--scalar-color-1: color-mix(
|
||||||
in srgb,
|
in srgb,
|
||||||
@@ -110,13 +102,8 @@
|
|||||||
--scalar-button-1: #f6f6f6;
|
--scalar-button-1: #f6f6f6;
|
||||||
--scalar-button-1-color: #000;
|
--scalar-button-1-color: #000;
|
||||||
--scalar-button-1-hover: #e7e7e7;
|
--scalar-button-1-hover: #e7e7e7;
|
||||||
|
|
||||||
--scalar-shadow-1: 0 1px 3px 0 rgb(0, 0, 0, 0.1);
|
|
||||||
--scalar-shadow-2: rgba(15, 15, 15, 0.2) 0px 3px 6px, rgba(15, 15, 15, 0.4) 0px 9px 24px,
|
|
||||||
0 0 0 1px rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
}
|
||||||
.scalar-modal-layout,
|
.scalar-modal-layout,
|
||||||
.scalar-app,
|
|
||||||
.scalar {
|
.scalar {
|
||||||
--scalar-font: initial;
|
--scalar-font: initial;
|
||||||
--scalar-font-code: var(--font-mono);
|
--scalar-font-code: var(--font-mono);
|
||||||
@@ -178,7 +165,7 @@
|
|||||||
.scalar-api-client__close:hover {
|
.scalar-api-client__close:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.scalar .scalar-app-layout {
|
.scalar .scalar-app {
|
||||||
background: var(--scalar-background-3);
|
background: var(--scalar-background-3);
|
||||||
height: calc(100dvh - 100px);
|
height: calc(100dvh - 100px);
|
||||||
max-width: 1280px;
|
max-width: 1280px;
|
||||||
@@ -215,6 +202,23 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
animation: scalardrawerexitfadein 0.35s forwards;
|
animation: scalardrawerexitfadein 0.35s forwards;
|
||||||
}
|
}
|
||||||
|
.scalar .scalar-app-exit:before {
|
||||||
|
content: '\00d7';
|
||||||
|
font-family: sans-serif;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
font-size: 30px;
|
||||||
|
font-weight: 100;
|
||||||
|
line-height: 50px;
|
||||||
|
right: 12px;
|
||||||
|
text-align: center;
|
||||||
|
color: white;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
.scalar .scalar-app-exit:hover:before {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
@keyframes scalardrawerexitfadein {
|
@keyframes scalardrawerexitfadein {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@@ -301,6 +305,11 @@
|
|||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
|
@supports (-moz-appearance: none) {
|
||||||
|
.scalar .custom-scroll {
|
||||||
|
padding-right: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
.scalar .custom-scroll:hover {
|
.scalar .custom-scroll:hover {
|
||||||
scrollbar-color: rgba(0, 0, 0, 0.24) transparent;
|
scrollbar-color: rgba(0, 0, 0, 0.24) transparent;
|
||||||
}
|
}
|
||||||
@@ -338,18 +347,3 @@
|
|||||||
padding-right: 12px;
|
padding-right: 12px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.dark .scalar .client-wrapper-bg-color {
|
|
||||||
background: linear-gradient(
|
|
||||||
color-mix(in srgb, var(--tw-bg-base) 6%, transparent) 1%,
|
|
||||||
color-mix(in srgb, var(--scalar-background-1) 30%, black) 9%
|
|
||||||
);
|
|
||||||
}
|
|
||||||
.light .scalar .client-wrapper-bg-color {
|
|
||||||
background-color: var(--scalar-background-2) !important;
|
|
||||||
}
|
|
||||||
.scalar .gitbook-show {
|
|
||||||
display: block !important;
|
|
||||||
}
|
|
||||||
.scalar .gitbook-hidden {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -19,12 +19,12 @@ export async function fetchOpenAPIBlock(
|
|||||||
block: DocumentBlockSwagger,
|
block: DocumentBlockSwagger,
|
||||||
resolveContentRef: (ref: ContentRef) => Promise<ResolvedContentRef | null>,
|
resolveContentRef: (ref: ContentRef) => Promise<ResolvedContentRef | null>,
|
||||||
): Promise<
|
): Promise<
|
||||||
| { data: OpenAPIOperationData | null; specUrl: string | null; error?: undefined }
|
| { data: OpenAPIOperationData | null; error?: undefined }
|
||||||
| { error: OpenAPIFetchError; data?: undefined; specUrl?: undefined }
|
| { error: OpenAPIFetchError; data?: undefined }
|
||||||
> {
|
> {
|
||||||
const resolved = block.data.ref ? await resolveContentRef(block.data.ref) : null;
|
const resolved = block.data.ref ? await resolveContentRef(block.data.ref) : null;
|
||||||
if (!resolved || !block.data.path || !block.data.method) {
|
if (!resolved || !block.data.path || !block.data.method) {
|
||||||
return { data: null, specUrl: null };
|
return { data: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -37,7 +37,7 @@ export async function fetchOpenAPIBlock(
|
|||||||
fetcher,
|
fetcher,
|
||||||
);
|
);
|
||||||
|
|
||||||
return { data, specUrl: resolved.href };
|
return { data };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof OpenAPIFetchError) {
|
if (error instanceof OpenAPIFetchError) {
|
||||||
return { error };
|
return { error };
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
},
|
},
|
||||||
"version": "0.6.0",
|
"version": "0.6.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@scalar/api-client-react": "1.0.20",
|
"@scalar/api-client-react": "0.3.7",
|
||||||
|
"@scalar/oas-utils": "0.1.6",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"flatted": "^3.2.9",
|
"flatted": "^3.2.9",
|
||||||
"openapi-types": "^12.1.3",
|
"openapi-types": "^12.1.3",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
import { CodeSampleInput, codeSampleGenerators } from './code-samples';
|
import { CodeSampleInput, codeSampleGenerators } from './code-samples';
|
||||||
import { OpenAPIOperationData } from './fetchOpenAPIOperation';
|
import { OpenAPIOperationData, toJSON } from './fetchOpenAPIOperation';
|
||||||
import { generateMediaTypeExample } from './generateSchemaExample';
|
import { generateMediaTypeExample } from './generateSchemaExample';
|
||||||
import { InteractiveSection } from './InteractiveSection';
|
import { InteractiveSection } from './InteractiveSection';
|
||||||
import { getServersURL } from './OpenAPIServerURL';
|
import { getServersURL } from './OpenAPIServerURL';
|
||||||
@@ -70,6 +70,11 @@ export function OpenAPICodeSample(props: {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchOperationData() {
|
||||||
|
'use server';
|
||||||
|
return toJSON(data);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InteractiveSection
|
<InteractiveSection
|
||||||
header="Request"
|
header="Request"
|
||||||
@@ -77,7 +82,7 @@ export function OpenAPICodeSample(props: {
|
|||||||
tabs={samples}
|
tabs={samples}
|
||||||
overlay={
|
overlay={
|
||||||
data['x-hideTryItPanel'] || data.operation['x-hideTryItPanel'] ? null : (
|
data['x-hideTryItPanel'] || data.operation['x-hideTryItPanel'] ? null : (
|
||||||
<ScalarApiButton />
|
<ScalarApiButton fetchOperationData={fetchOperationData} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { OpenAPIResponseExample } from './OpenAPIResponseExample';
|
|||||||
import { OpenAPIServerURL } from './OpenAPIServerURL';
|
import { OpenAPIServerURL } from './OpenAPIServerURL';
|
||||||
import { OpenAPISpec } from './OpenAPISpec';
|
import { OpenAPISpec } from './OpenAPISpec';
|
||||||
import { OpenAPIClientContext, OpenAPIContextProps } from './types';
|
import { OpenAPIClientContext, OpenAPIContextProps } from './types';
|
||||||
import { ApiClientModalProvider } from '@scalar/api-client-react';
|
import { ScalarApiClient } from './ScalarApiButton';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display an interactive OpenAPI operation.
|
* Display an interactive OpenAPI operation.
|
||||||
@@ -23,15 +23,11 @@ export function OpenAPIOperation(props: {
|
|||||||
|
|
||||||
const clientContext: OpenAPIClientContext = {
|
const clientContext: OpenAPIClientContext = {
|
||||||
defaultInteractiveOpened: context.defaultInteractiveOpened,
|
defaultInteractiveOpened: context.defaultInteractiveOpened,
|
||||||
specUrl: context.specUrl,
|
|
||||||
icons: context.icons,
|
icons: context.icons,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ApiClientModalProvider
|
<ScalarApiClient>
|
||||||
configuration={{ spec: { url: context.specUrl } }}
|
|
||||||
initialRequest={{ path: data.path, method: data.method }}
|
|
||||||
>
|
|
||||||
<div className={classNames('openapi-operation', className)}>
|
<div className={classNames('openapi-operation', className)}>
|
||||||
<div className="openapi-intro">
|
<div className="openapi-intro">
|
||||||
<h2 className="openapi-summary" id={context.id}>
|
<h2 className="openapi-summary" id={context.id}>
|
||||||
@@ -67,6 +63,6 @@ export function OpenAPIOperation(props: {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ApiClientModalProvider>
|
</ScalarApiClient>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,44 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
import {
|
||||||
import { useApiClientModal } from '@scalar/api-client-react';
|
Cookie,
|
||||||
|
getHarRequest,
|
||||||
|
getParametersFromOperation,
|
||||||
|
type TransformedOperation,
|
||||||
|
getRequestFromOperation,
|
||||||
|
Query,
|
||||||
|
Header,
|
||||||
|
RequestBody,
|
||||||
|
} from '@scalar/oas-utils';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { OpenAPIOperationData, fromJSON } from './fetchOpenAPIOperation';
|
||||||
|
|
||||||
|
const ApiClientReact = React.lazy(async () => {
|
||||||
|
const mod = await import('@scalar/api-client-react');
|
||||||
|
return { default: mod.ApiClientReact };
|
||||||
|
});
|
||||||
|
|
||||||
|
const ScalarContext = React.createContext<
|
||||||
|
(fetchOperationData: () => Promise<OpenAPIOperationData>) => void
|
||||||
|
>(() => {});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Button which launches the Scalar API Client
|
* Button which launches the Scalar API Client
|
||||||
*/
|
*/
|
||||||
export function ScalarApiButton() {
|
export function ScalarApiButton(props: {
|
||||||
const client = useApiClientModal();
|
fetchOperationData: () => Promise<OpenAPIOperationData>;
|
||||||
|
}) {
|
||||||
|
const { fetchOperationData } = props;
|
||||||
|
const open = React.useContext(ScalarContext);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="scalar scalar-activate">
|
<div className="scalar scalar-activate">
|
||||||
<button className="scalar-activate-button" onClick={() => client?.open()}>
|
<button
|
||||||
|
className="scalar-activate-button"
|
||||||
|
onClick={() => {
|
||||||
|
open(fetchOperationData);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="12" fill="none">
|
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="12" fill="none">
|
||||||
<path
|
<path
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
@@ -24,3 +51,111 @@ export function ScalarApiButton() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap the rendering with a context to open the scalar modal.
|
||||||
|
*/
|
||||||
|
export function ScalarApiClient(props: { children: React.ReactNode }) {
|
||||||
|
const { children } = props;
|
||||||
|
|
||||||
|
const [active, setActive] = React.useState<null | {
|
||||||
|
operationData: OpenAPIOperationData | null;
|
||||||
|
}>(null);
|
||||||
|
|
||||||
|
const proxy = '/~scalar/proxy';
|
||||||
|
|
||||||
|
const open = React.useCallback(
|
||||||
|
async (fetchOperationData: () => Promise<OpenAPIOperationData>) => {
|
||||||
|
setActive({ operationData: null });
|
||||||
|
const operationData = fromJSON(await fetchOperationData());
|
||||||
|
setActive({ operationData });
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onClose = React.useCallback(() => {
|
||||||
|
setActive(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const request = React.useMemo(() => {
|
||||||
|
const operationData = active?.operationData;
|
||||||
|
|
||||||
|
if (!operationData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const operationId =
|
||||||
|
operationData.operation.operationId ?? operationData.method + operationData.path;
|
||||||
|
|
||||||
|
const operation = {
|
||||||
|
...operationData,
|
||||||
|
httpVerb: operationData.method,
|
||||||
|
pathParameters: operationData.operation.parameters,
|
||||||
|
} as TransformedOperation;
|
||||||
|
|
||||||
|
const variables = getParametersFromOperation(operation, 'path', false);
|
||||||
|
|
||||||
|
const request = getHarRequest(
|
||||||
|
{
|
||||||
|
url: operationData.path,
|
||||||
|
},
|
||||||
|
getRequestFromOperation(
|
||||||
|
{
|
||||||
|
...operation,
|
||||||
|
information: {
|
||||||
|
requestBody: operationData.operation.requestBody as RequestBody,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ requiredOnly: false },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
id: operationId,
|
||||||
|
type: operationData.method,
|
||||||
|
path: operationData.path,
|
||||||
|
variables,
|
||||||
|
cookies: request.cookies.map((cookie: Cookie) => {
|
||||||
|
return { ...cookie, enabled: true };
|
||||||
|
}),
|
||||||
|
query: request.queryString.map((queryString: Query) => {
|
||||||
|
const query: typeof queryString & { required?: boolean } = queryString;
|
||||||
|
return { ...queryString, enabled: query.required ?? true };
|
||||||
|
}),
|
||||||
|
headers: request.headers.map((header: Header) => {
|
||||||
|
return { ...header, enabled: true };
|
||||||
|
}),
|
||||||
|
url: operationData.servers[0]?.url,
|
||||||
|
body: request.postData?.text,
|
||||||
|
};
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}, [active]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScalarContext.Provider value={open}>
|
||||||
|
{children}
|
||||||
|
{active ? (
|
||||||
|
<div className="scalar">
|
||||||
|
<div className="scalar-container">
|
||||||
|
<div className="scalar-app">
|
||||||
|
<React.Suspense fallback={<ScalarLoading />}>
|
||||||
|
<ApiClientReact
|
||||||
|
close={onClose}
|
||||||
|
proxy={proxy}
|
||||||
|
isOpen={true}
|
||||||
|
request={request}
|
||||||
|
/>
|
||||||
|
</React.Suspense>
|
||||||
|
</div>
|
||||||
|
<div onClick={() => onClose()} className="scalar-app-exit"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</ScalarContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ScalarLoading() {
|
||||||
|
return <div className="scalar-app-loading">Loading...</div>;
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,9 +10,6 @@ export interface OpenAPIClientContext {
|
|||||||
chevronRight: React.ReactNode;
|
chevronRight: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Spec url foor the Scalar Api Client */
|
|
||||||
specUrl: string;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Force all sections to be opened by default.
|
* Force all sections to be opened by default.
|
||||||
* @default false
|
* @default false
|
||||||
|
|||||||
Reference in New Issue
Block a user