Skip to content

Commit 8de5c04

Browse files
committed
Rework OpenAPI Authorizations + scopes
1 parent 4534ea8 commit 8de5c04

File tree

6 files changed

+175
-116
lines changed

6 files changed

+175
-116
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export function getOpenAPIContext(args: {
4141
plus: <Icon icon="plus" />,
4242
copy: <Icon icon="copy" />,
4343
check: <Icon icon="check" />,
44+
lock: <Icon icon="lock" />,
4445
},
4546
renderCodeBlock: (codeProps) => <PlainCodeBlock {...codeProps} />,
4647
renderDocument: (documentProps) => (

packages/gitbook/src/components/DocumentView/OpenAPI/style.css

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,7 @@
334334
}
335335

336336
.openapi-securities-scopes ul {
337-
@apply !my-0;
337+
@apply !my-0 !list-none !pl-0;
338338
}
339339

340340
.openapi-securities-url {
@@ -398,15 +398,13 @@
398398
@apply text-left prose-sm text-sm leading-tight text-tint select-text prose-strong:font-semibold prose-strong:text-inherit;
399399
}
400400

401-
.openapi-disclosure-group-trigger[aria-expanded="false"] {
402-
.openapi-response-description.openapi-markdown {
403-
@apply truncate;
404-
@apply [&>*:not(:first-child)]:hidden *:truncate *:!p-0 *:!m-0;
405-
}
401+
.openapi-disclosure-group-trigger[aria-expanded="false"] .openapi-response-description.openapi-markdown {
402+
@apply truncate;
403+
@apply [&>*:not(:first-child)]:hidden *:truncate *:!p-0 *:!m-0;
404+
}
406405

407-
.openapi-response-tab-content {
408-
@apply basis-[60%]
409-
}
406+
.openapi-disclosure-group-trigger[aria-expanded="false"] .openapi-response-tab-content {
407+
@apply basis-[60%];
410408
}
411409

412410
.openapi-response-body {
@@ -528,7 +526,7 @@
528526
.openapi-panel,
529527
.openapi-codesample,
530528
.openapi-response-examples {
531-
@apply border shrink min-h-40 overflow-hidden rounded-md straight-corners:rounded-none circular-corners:rounded-xl bg-tint-subtle border-tint-subtle shadow-sm;
529+
@apply border shrink min-h-40 overflow-hidden rounded-lg straight-corners:rounded-none circular-corners:rounded-xl bg-tint-subtle border-tint-subtle shadow-sm;
532530
}
533531

534532
.openapi-response-examples-panel {
@@ -851,7 +849,7 @@ body:has(.openapi-select-popover) {
851849
.openapi-schema-alternatives .openapi-disclosure,
852850
.openapi-schemas-disclosure .openapi-schema.openapi-disclosure
853851
) {
854-
@apply rounded-xl straight-corners:rounded-none;
852+
@apply rounded-md circular-corners:rounded-xl straight-corners:rounded-none;
855853
}
856854

857855
.openapi-disclosure .openapi-schemas-disclosure .openapi-schema.openapi-disclosure {
@@ -866,10 +864,10 @@ body:has(.openapi-select-popover) {
866864
@apply ring-1 shadow-sm;
867865
}
868866

869-
.openapi-disclosure[data-expanded="true"]:not(.openapi-schemas-disclosure):not(:first-child) {
867+
.openapi-disclosure[data-expanded="true"]:not(.openapi-schemas-disclosure,.openapi-required-scopes):not(:first-child) {
870868
@apply mt-2;
871869
}
872-
.openapi-disclosure[data-expanded="true"]:not(.openapi-schemas-disclosure):not(:last-child) {
870+
.openapi-disclosure[data-expanded="true"]:not(.openapi-schemas-disclosure,.openapi-required-scopes):not(:last-child) {
873871
@apply mb-2;
874872
}
875873

@@ -1015,4 +1013,20 @@ body:has(.openapi-select-popover) {
10151013

10161014
.openapi-path-copy-button-icon svg {
10171015
@apply text-tint size-4;
1016+
}
1017+
1018+
.openapi-required-scopes {
1019+
@apply ring-1 ring-tint-subtle text-base font-medium mx-0;
1020+
}
1021+
1022+
.openapi-required-scopes .openapi-disclosure-trigger-label {
1023+
@apply top-1/2 -translate-y-1/2;
1024+
}
1025+
1026+
.openapi-required-scopes .openapi-required-scopes-header {
1027+
@apply flex items-center gap-3;
1028+
}
1029+
1030+
.openapi-required-scopes .openapi-required-scopes-header svg {
1031+
@apply size-3.5 text-tint-subtle rotate-none;
10181032
}

packages/react-openapi/src/OpenAPIDisclosure.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,15 @@ export function OpenAPIDisclosure(props: {
3434
>
3535
{header}
3636
<div className="openapi-disclosure-trigger-label">
37-
<span>{typeof label === 'function' ? label(isExpanded) : label}</span>
37+
{label ? (
38+
<span>{typeof label === 'function' ? label(isExpanded) : label}</span>
39+
) : null}
3840
{icon}
3941
</div>
4042
</Button>
41-
<DisclosurePanel className="openapi-disclosure-panel">
42-
{isExpanded ? children : null}
43-
</DisclosurePanel>
43+
{isExpanded ? (
44+
<DisclosurePanel className="openapi-disclosure-panel">{children}</DisclosurePanel>
45+
) : null}
4446
</Disclosure>
4547
);
4648
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
'use client';
2+
3+
import { OpenAPICopyButton } from './OpenAPICopyButton';
4+
import { OpenAPIDisclosure } from './OpenAPIDisclosure';
5+
import { useSelectState } from './OpenAPISelect';
6+
import type { OpenAPIClientContext } from './context';
7+
import { t } from './translate';
8+
import type { OpenAPISecurityScope } from './types';
9+
import type { OperationSecurityInfo } from './utils';
10+
11+
/**
12+
* Present securities authorization that can be used for this operation.
13+
*/
14+
export function OpenAPIRequiredScopes(props: {
15+
securities: OperationSecurityInfo[];
16+
context: OpenAPIClientContext;
17+
stateKey: string;
18+
}) {
19+
const { securities, stateKey, context } = props;
20+
const { key: selectedKey } = useSelectState(stateKey, securities[0]?.key);
21+
const selectedSecurity = securities.find((security) => security.key === selectedKey);
22+
23+
if (!selectedSecurity) {
24+
return null;
25+
}
26+
27+
const scopes = selectedSecurity.schemes.flatMap((scheme) => {
28+
if (scheme.type === 'oauth2') {
29+
return Object.entries(scheme.flows ?? {}).flatMap(([_, flow]) =>
30+
Object.entries(flow.scopes ?? {})
31+
);
32+
}
33+
34+
return scheme.scopes ?? [];
35+
});
36+
37+
return (
38+
<OpenAPIDisclosure
39+
className="openapi-required-scopes"
40+
header={
41+
<div className="openapi-required-scopes-header">
42+
{context.icons.lock}
43+
<span>{t(context.translation, 'required_scopes')}</span>
44+
</div>
45+
}
46+
icon={context.icons.plus}
47+
label=""
48+
>
49+
<OpenAPISchemaScopes scopes={scopes} context={context} />
50+
</OpenAPIDisclosure>
51+
);
52+
}
53+
54+
function OpenAPISchemaScopes(props: {
55+
scopes: OpenAPISecurityScope[];
56+
context: OpenAPIClientContext;
57+
}) {
58+
const { scopes, context } = props;
59+
60+
return (
61+
<div className="openapi-securities-scopes openapi-markdown">
62+
<ul>
63+
{scopes.map((scope) => (
64+
<OpenAPIScopeItem key={scope[0]} scope={scope} context={context} />
65+
))}
66+
</ul>
67+
</div>
68+
);
69+
}
70+
71+
/**
72+
* Display a scope item. Either a key-value pair or a single string.
73+
*/
74+
function OpenAPIScopeItem(props: {
75+
scope: OpenAPISecurityScope;
76+
context: OpenAPIClientContext;
77+
}) {
78+
const { scope, context } = props;
79+
80+
return (
81+
<li>
82+
<OpenAPIScopeItemKey name={scope[0]} context={context} />
83+
{scope[1] ? `: ${scope[1]}` : null}
84+
</li>
85+
);
86+
}
87+
88+
/**
89+
* Displays the scope name within a copyable button.
90+
*/
91+
function OpenAPIScopeItemKey(props: {
92+
name: string;
93+
context: OpenAPIClientContext;
94+
}) {
95+
const { name, context } = props;
96+
97+
return (
98+
<OpenAPICopyButton value={name} context={context} withTooltip>
99+
<code>{name}</code>
100+
</OpenAPICopyButton>
101+
);
102+
}

packages/react-openapi/src/OpenAPISecurities.tsx

Lines changed: 38 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ import { Fragment } from 'react';
33
import { InteractiveSection } from './InteractiveSection';
44
import { Markdown } from './Markdown';
55
import { OpenAPICopyButton } from './OpenAPICopyButton';
6+
import { OpenAPIRequiredScopes } from './OpenAPIRequiredScopes';
67
import { OpenAPISchemaName } from './OpenAPISchemaName';
78
import type { OpenAPIClientContext } from './context';
89
import { t } from './translate';
9-
import type { OpenAPICustomSecurityScheme, OpenAPISecurityScope } from './types';
10+
import type { OpenAPICustomSecurityScheme } from './types';
1011
import type { OpenAPIOperationData } from './types';
1112
import { createStateKey, extractOperationSecurityInfo, resolveDescription } from './utils';
1213

@@ -25,48 +26,44 @@ export function OpenAPISecurities(props: {
2526
}
2627

2728
const tabsData = extractOperationSecurityInfo({ securityRequirement, securities });
29+
const stateKey = createStateKey('securities', context.blockKey);
2830

2931
return (
30-
<InteractiveSection
31-
header={t(context.translation, 'authorizations')}
32-
stateKey={createStateKey('securities', context.blockKey)}
33-
toggeable
34-
defaultOpened={false}
35-
toggleIcon={context.icons.chevronRight}
36-
selectIcon={context.icons.chevronDown}
37-
className="openapi-securities"
38-
tabs={tabsData.map(({ key, label, schemes }) => ({
39-
key,
40-
label,
41-
body: (
42-
<div className="openapi-schema">
43-
{schemes.map((security, index) => {
44-
const description = resolveDescription(security);
45-
return (
46-
<div
47-
key={`${key}-${index}`}
48-
className="openapi-schema-presentation"
49-
>
50-
{getLabelForType(security, context)}
51-
{description ? (
52-
<Markdown
53-
source={description}
54-
className="openapi-securities-description"
55-
/>
56-
) : null}
57-
{security.scopes?.length ? (
58-
<OpenAPISchemaScopes
59-
scopes={security.scopes}
60-
context={context}
61-
/>
62-
) : null}
63-
</div>
64-
);
65-
})}
66-
</div>
67-
),
68-
}))}
69-
/>
32+
<>
33+
<OpenAPIRequiredScopes context={context} stateKey={stateKey} securities={tabsData} />
34+
<InteractiveSection
35+
header={t(context.translation, 'authorizations')}
36+
stateKey={stateKey}
37+
toggleIcon={context.icons.chevronRight}
38+
selectIcon={context.icons.chevronDown}
39+
className="openapi-securities"
40+
tabs={tabsData.map(({ key, label, schemes }) => ({
41+
key,
42+
label,
43+
body: (
44+
<div className="openapi-schema">
45+
{schemes.map((security, index) => {
46+
const description = resolveDescription(security);
47+
return (
48+
<div
49+
key={`${key}-${index}`}
50+
className="openapi-schema-presentation"
51+
>
52+
{getLabelForType(security, context)}
53+
{description ? (
54+
<Markdown
55+
source={description}
56+
className="openapi-securities-description"
57+
/>
58+
) : null}
59+
</div>
60+
);
61+
})}
62+
</div>
63+
),
64+
}))}
65+
/>
66+
</>
7067
);
7168
}
7269

@@ -175,9 +172,6 @@ function OpenAPISchemaOAuth2Item(props: {
175172
return null;
176173
}
177174

178-
// If the security scheme has scopes, we don't need to display the scopes from the flow
179-
const scopes = !security.scopes?.length && flow.scopes ? Object.entries(flow.scopes) : [];
180-
181175
return (
182176
<div>
183177
<OpenAPISchemaName
@@ -227,62 +221,7 @@ function OpenAPISchemaOAuth2Item(props: {
227221
</OpenAPICopyButton>
228222
</span>
229223
) : null}
230-
{scopes.length ? <OpenAPISchemaScopes scopes={scopes} context={context} /> : null}
231224
</div>
232225
</div>
233226
);
234227
}
235-
236-
/**
237-
* Render a list of available scopes.
238-
*/
239-
function OpenAPISchemaScopes(props: {
240-
scopes: OpenAPISecurityScope[];
241-
context: OpenAPIClientContext;
242-
}) {
243-
const { scopes, context } = props;
244-
245-
return (
246-
<div className="openapi-securities-scopes openapi-markdown">
247-
<span>{t(context.translation, 'required_scopes')}: </span>
248-
<ul>
249-
{scopes.map((scope) => (
250-
<OpenAPIScopeItem key={scope[0]} scope={scope} context={context} />
251-
))}
252-
</ul>
253-
</div>
254-
);
255-
}
256-
257-
/**
258-
* Display a scope item. Either a key-value pair or a single string.
259-
*/
260-
function OpenAPIScopeItem(props: {
261-
scope: OpenAPISecurityScope;
262-
context: OpenAPIClientContext;
263-
}) {
264-
const { scope, context } = props;
265-
266-
return (
267-
<li>
268-
<OpenAPIScopeItemKey name={scope[0]} context={context} />
269-
{scope[1] ? `: ${scope[1]}` : null}
270-
</li>
271-
);
272-
}
273-
274-
/**
275-
* Displays the scope name within a copyable button.
276-
*/
277-
function OpenAPIScopeItemKey(props: {
278-
name: string;
279-
context: OpenAPIClientContext;
280-
}) {
281-
const { name, context } = props;
282-
283-
return (
284-
<OpenAPICopyButton value={name} context={context} withTooltip>
285-
<code>{name}</code>
286-
</OpenAPICopyButton>
287-
);
288-
}

packages/react-openapi/src/context.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export interface OpenAPIClientContext {
1515
plus: React.ReactNode;
1616
copy: React.ReactNode;
1717
check: React.ReactNode;
18+
lock: React.ReactNode;
1819
};
1920

2021
/**

0 commit comments

Comments
 (0)