@kingsleyweb/bun-ui 0.0.0-stage → 3.0.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +80 -2
- package/app/docs/App.tsx +72 -0
- package/app/docs/api/client.ts +303 -0
- package/app/docs/api/contract.ts +24 -0
- package/app/docs/api/docs.ts +38 -0
- package/app/docs/api/errors.ts +144 -0
- package/app/docs/boot.tsx +58 -0
- package/app/docs/components/Badge.tsx +65 -0
- package/app/docs/components/Button.tsx +37 -0
- package/app/docs/components/Card.tsx +41 -0
- package/app/docs/components/ConfirmDialog.tsx +173 -0
- package/app/docs/components/CopyButton.tsx +68 -0
- package/app/docs/components/Dialog.tsx +230 -0
- package/app/docs/components/EmptyState.tsx +34 -0
- package/app/docs/components/ErrorView.tsx +70 -0
- package/app/docs/components/Field.tsx +93 -0
- package/app/docs/components/JsonEditor.tsx +151 -0
- package/app/docs/components/JsonView.tsx +338 -0
- package/app/docs/components/KeyValue.tsx +63 -0
- package/app/docs/components/ProblemBanner.tsx +116 -0
- package/app/docs/components/ScreenErrorBoundary.tsx +141 -0
- package/app/docs/components/Spinner.tsx +33 -0
- package/app/docs/components/Table.tsx +33 -0
- package/app/docs/components/Tabs.tsx +226 -0
- package/app/docs/components/classNames.ts +4 -0
- package/app/docs/components/components.css +691 -0
- package/app/docs/components/copy.ts +89 -0
- package/app/docs/components/fieldContext.ts +29 -0
- package/app/docs/components/inputValues.ts +142 -0
- package/app/docs/components/inputs.tsx +387 -0
- package/app/docs/components/jsonParse.ts +338 -0
- package/app/docs/components/listKeys.ts +30 -0
- package/app/docs/config.ts +86 -0
- package/app/docs/context.ts +33 -0
- package/app/docs/embed.ts +74 -0
- package/app/docs/env.d.ts +2 -0
- package/app/docs/format.ts +9 -0
- package/app/docs/hooks/useUrlTab.ts +16 -0
- package/app/docs/main.tsx +11 -0
- package/app/docs/queryClient.ts +54 -0
- package/app/docs/router.tsx +194 -0
- package/app/docs/routing.ts +194 -0
- package/app/docs/screens/docs/document.ts +27 -0
- package/app/docs/screens/docs/http/OperationView.tsx +407 -0
- package/app/docs/screens/docs/http/Sidebar.tsx +190 -0
- package/app/docs/screens/docs/http/TryItPanel.tsx +603 -0
- package/app/docs/screens/docs/http/http.css +405 -0
- package/app/docs/screens/docs/http/index.tsx +395 -0
- package/app/docs/screens/docs/http/model.ts +604 -0
- package/app/docs/screens/docs/http/tryIt.ts +455 -0
- package/app/docs/screens/docs/permissions.ts +6 -0
- package/app/docs/screens/docs/schema/Prose.tsx +43 -0
- package/app/docs/screens/docs/schema/SchemaTree.tsx +319 -0
- package/app/docs/screens/docs/schema/index.ts +30 -0
- package/app/docs/screens/docs/schema/resolve.ts +466 -0
- package/app/docs/screens/docs/schema/schema.css +137 -0
- package/app/docs/screens/docs/schema/skeleton.ts +92 -0
- package/app/docs/screens/docs/viewer.tsx +154 -0
- package/app/docs/screens/docs/ws/InlineText.tsx +25 -0
- package/app/docs/screens/docs/ws/PayloadSchema.tsx +39 -0
- package/app/docs/screens/docs/ws/format.ts +82 -0
- package/app/docs/screens/docs/ws/index.tsx +286 -0
- package/app/docs/screens/docs/ws/items.tsx +786 -0
- package/app/docs/screens/docs/ws/model.ts +1178 -0
- package/app/docs/screens/docs/ws/panels.tsx +440 -0
- package/app/docs/screens/docs/ws/ws.css +231 -0
- package/app/docs/shortcuts.ts +94 -0
- package/app/docs/styles/base.css +847 -0
- package/app/docs/styles/tokens.css +143 -0
- package/app/docs/theme.ts +56 -0
- package/app/kit/unauthorized.ts +222 -0
- package/dist/docs/assets/main-0rchxtv5.js +18 -0
- package/dist/docs/assets/main-0rchxtv5.js.map +99 -0
- package/dist/docs/assets/main-cqgyvs86.css +1 -0
- package/dist/docs/manifest.json +26 -0
- package/dist/jobs/assets/AddFlowDialog-h6xkgx9b.js +6 -0
- package/dist/jobs/assets/AddFlowDialog-h6xkgx9b.js.map +11 -0
- package/dist/jobs/assets/AddFlowDialog-n7gtc84v.css +1 -0
- package/dist/jobs/assets/added-cbwsxv3e.js +4 -0
- package/dist/jobs/assets/added-cbwsxv3e.js.map +11 -0
- package/dist/jobs/assets/bodies-yyw5hmrp.js +6 -0
- package/dist/jobs/assets/bodies-yyw5hmrp.js.map +10 -0
- package/dist/jobs/assets/channel-tgkvqm8y.js +4 -0
- package/dist/jobs/assets/channel-tgkvqm8y.js.map +10 -0
- package/dist/jobs/assets/channelChecks-s4yz4yhw.js +4 -0
- package/dist/jobs/assets/channelChecks-s4yz4yhw.js.map +10 -0
- package/dist/jobs/assets/index-02427smp.css +1 -0
- package/dist/jobs/assets/index-09z2cjft.js +4 -0
- package/dist/jobs/assets/index-09z2cjft.js.map +16 -0
- package/dist/jobs/assets/index-18bn0gvy.css +1 -0
- package/dist/jobs/assets/index-47pxmyxn.js +4 -0
- package/dist/jobs/assets/index-47pxmyxn.js.map +10 -0
- package/dist/jobs/assets/index-4e55z986.js +4 -0
- package/dist/jobs/assets/index-4e55z986.js.map +11 -0
- package/dist/jobs/assets/index-5c933424.js +4 -0
- package/dist/jobs/assets/index-5c933424.js.map +18 -0
- package/dist/jobs/assets/index-5kbyqspm.css +1 -0
- package/dist/jobs/assets/index-5ng37hk6.css +1 -0
- package/dist/jobs/assets/index-5w8w7fvc.css +1 -0
- package/dist/jobs/assets/index-71qs5qyx.css +1 -0
- package/dist/jobs/assets/index-btpg4r8b.css +1 -0
- package/dist/jobs/assets/index-d5r5n0mm.js +4 -0
- package/dist/jobs/assets/index-d5r5n0mm.js.map +23 -0
- package/dist/jobs/assets/index-e0yy2skq.css +1 -0
- package/dist/jobs/assets/index-hs60b25v.js +10 -0
- package/dist/jobs/assets/index-hs60b25v.js.map +62 -0
- package/dist/jobs/assets/index-k2smrr70.js +4 -0
- package/dist/jobs/assets/index-k2smrr70.js.map +9 -0
- package/dist/jobs/assets/index-q24jw3w9.js +4 -0
- package/dist/jobs/assets/index-q24jw3w9.js.map +32 -0
- package/dist/jobs/assets/index-qfb80xen.css +1 -0
- package/dist/jobs/assets/index-rtynx6wj.js +4 -0
- package/dist/jobs/assets/index-rtynx6wj.js.map +10 -0
- package/dist/jobs/assets/live-6k7s6358.js +4 -0
- package/dist/jobs/assets/live-6k7s6358.js.map +10 -0
- package/dist/jobs/assets/main-0jmeh6mq.js +6 -0
- package/dist/jobs/assets/main-0jmeh6mq.js.map +12 -0
- package/dist/jobs/assets/main-3yj6bv90.js +5 -0
- package/dist/jobs/assets/main-3yj6bv90.js.map +10 -0
- package/dist/jobs/assets/main-4g69fn3x.js +5 -0
- package/dist/jobs/assets/main-4g69fn3x.js.map +10 -0
- package/dist/jobs/assets/main-4gte3eqq.js +5 -0
- package/dist/jobs/assets/main-4gte3eqq.js.map +10 -0
- package/dist/jobs/assets/main-4j1fcevh.js +5 -0
- package/dist/jobs/assets/main-4j1fcevh.js.map +10 -0
- package/dist/jobs/assets/main-54qwkz28.css +1 -0
- package/dist/jobs/assets/main-5grjvsy7.js +5 -0
- package/dist/jobs/assets/main-5grjvsy7.js.map +11 -0
- package/dist/jobs/assets/main-5zbx60gq.js +5 -0
- package/dist/jobs/assets/main-5zbx60gq.js.map +11 -0
- package/dist/jobs/assets/main-9prgjhg4.js +5 -0
- package/dist/jobs/assets/main-9prgjhg4.js.map +10 -0
- package/dist/jobs/assets/main-a250ffk7.js +17 -0
- package/dist/jobs/assets/main-a250ffk7.js.map +115 -0
- package/dist/jobs/assets/main-d6ge7w08.js +5 -0
- package/dist/jobs/assets/main-d6ge7w08.js.map +10 -0
- package/dist/jobs/assets/main-gngzht43.js +5 -0
- package/dist/jobs/assets/main-gngzht43.js.map +10 -0
- package/dist/jobs/assets/main-hg0hrg8j.js +5 -0
- package/dist/jobs/assets/main-hg0hrg8j.js.map +10 -0
- package/dist/jobs/assets/main-hm1ng3yg.js +5 -0
- package/dist/jobs/assets/main-hm1ng3yg.js.map +12 -0
- package/dist/jobs/assets/main-jkvd92z9.js +5 -0
- package/dist/jobs/assets/main-jkvd92z9.js.map +10 -0
- package/dist/jobs/assets/main-jv5xqd13.js +5 -0
- package/dist/jobs/assets/main-jv5xqd13.js.map +21 -0
- package/dist/jobs/assets/main-jxre7w93.js +5 -0
- package/dist/jobs/assets/main-jxre7w93.js.map +23 -0
- package/dist/jobs/assets/main-kwmqkjfx.js +5 -0
- package/dist/jobs/assets/main-kwmqkjfx.js.map +11 -0
- package/dist/jobs/assets/main-n9cf6bxm.js +5 -0
- package/dist/jobs/assets/main-n9cf6bxm.js.map +11 -0
- package/dist/jobs/assets/main-pegp0ygb.js +5 -0
- package/dist/jobs/assets/main-pegp0ygb.js.map +10 -0
- package/dist/jobs/assets/main-qr573853.js +5 -0
- package/dist/jobs/assets/main-qr573853.js.map +12 -0
- package/dist/jobs/assets/main-rq97rdr4.js +4 -0
- package/dist/jobs/assets/main-rq97rdr4.js.map +22 -0
- package/dist/jobs/assets/main-szjdmydc.js +5 -0
- package/dist/jobs/assets/main-szjdmydc.js.map +13 -0
- package/dist/jobs/assets/main-tnbhrm9n.js +7 -0
- package/dist/jobs/assets/main-tnbhrm9n.js.map +11 -0
- package/dist/jobs/assets/main-ts19j4n0.js +5 -0
- package/dist/jobs/assets/main-ts19j4n0.js.map +25 -0
- package/dist/jobs/assets/main-w93x1dzs.js +5 -0
- package/dist/jobs/assets/main-w93x1dzs.js.map +10 -0
- package/dist/jobs/assets/sections-0ksdxs8m.css +1 -0
- package/dist/jobs/assets/sections-0y7w4fb4.js +4 -0
- package/dist/jobs/assets/sections-0y7w4fb4.js.map +10 -0
- package/dist/jobs/assets/sse-mn0t0fh7.js +5 -0
- package/dist/jobs/assets/sse-mn0t0fh7.js.map +11 -0
- package/dist/jobs/assets/sseChecks-t7tcm7ra.js +4 -0
- package/dist/jobs/assets/sseChecks-t7tcm7ra.js.map +10 -0
- package/dist/jobs/assets/validation-ysjfy072.js +4 -0
- package/dist/jobs/assets/validation-ysjfy072.js.map +9 -0
- package/dist/jobs/manifest.json +511 -0
- package/dts/docs/assets.d.ts +144 -0
- package/dts/docs/assets.d.ts.map +1 -0
- package/dts/docs/browser/config.d.ts +61 -0
- package/dts/docs/browser/config.d.ts.map +1 -0
- package/dts/docs/browser/index.d.ts +12 -0
- package/dts/docs/browser/index.d.ts.map +1 -0
- package/dts/docs/docsUi.d.ts +42 -0
- package/dts/docs/docsUi.d.ts.map +1 -0
- package/dts/docs/hosted.d.ts +22 -0
- package/dts/docs/hosted.d.ts.map +1 -0
- package/dts/docs/index.d.ts +18 -0
- package/dts/docs/index.d.ts.map +1 -0
- package/dts/docs/shell.d.ts +57 -0
- package/dts/docs/shell.d.ts.map +1 -0
- package/dts/docs/types.d.ts +137 -0
- package/dts/docs/types.d.ts.map +1 -0
- package/dts/index.d.ts +21 -0
- package/dts/index.d.ts.map +1 -0
- package/dts/jobs/assets.d.ts +157 -0
- package/dts/jobs/assets.d.ts.map +1 -0
- package/dts/jobs/browser/config.d.ts +67 -0
- package/dts/jobs/browser/config.d.ts.map +1 -0
- package/dts/jobs/browser/index.d.ts +10 -0
- package/dts/jobs/browser/index.d.ts.map +1 -0
- package/dts/jobs/browser/unauthorized.d.ts +9 -0
- package/dts/jobs/browser/unauthorized.d.ts.map +1 -0
- package/dts/jobs/index.d.ts +24 -0
- package/dts/jobs/index.d.ts.map +1 -0
- package/dts/jobs/jobsUi.d.ts +63 -0
- package/dts/jobs/jobsUi.d.ts.map +1 -0
- package/dts/jobs/shell.d.ts +77 -0
- package/dts/jobs/shell.d.ts.map +1 -0
- package/dts/jobs/types.d.ts +146 -0
- package/dts/jobs/types.d.ts.map +1 -0
- package/dts/kit/browser/shared.d.ts +10 -0
- package/dts/kit/browser/shared.d.ts.map +1 -0
- package/dts/kit/browser/unauthorized.d.ts +74 -0
- package/dts/kit/browser/unauthorized.d.ts.map +1 -0
- package/dts/kit/unauthorized.d.ts +47 -0
- package/dts/kit/unauthorized.d.ts.map +1 -0
- package/guides/docs.md +170 -0
- package/guides/jobs.md +1120 -0
- package/lib/docs/assets.ts +427 -0
- package/lib/docs/browser/config.ts +66 -0
- package/lib/docs/browser/index.ts +24 -0
- package/lib/docs/docsUi.ts +623 -0
- package/lib/docs/hosted.ts +31 -0
- package/lib/docs/index.ts +37 -0
- package/lib/docs/shell.ts +156 -0
- package/lib/docs/types.ts +149 -0
- package/lib/index.ts +27 -0
- package/lib/jobs/assets.ts +448 -0
- package/lib/jobs/browser/config.ts +65 -0
- package/lib/jobs/browser/index.ts +9 -0
- package/lib/jobs/browser/unauthorized.ts +11 -0
- package/lib/jobs/index.ts +41 -0
- package/lib/jobs/jobsUi.ts +709 -0
- package/lib/jobs/shell.ts +205 -0
- package/lib/jobs/types.ts +162 -0
- package/lib/kit/browser/shared.ts +11 -0
- package/lib/kit/browser/unauthorized.ts +114 -0
- package/lib/kit/unauthorized.ts +222 -0
- package/package.json +198 -4
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import type { PermissionProbe } from "../permissions";
|
|
2
|
+
import type { SchemaRoot } from "../schema/resolve";
|
|
3
|
+
import type { DocOperation, DocParameter, DocResponse } from "./model";
|
|
4
|
+
import { Badge } from "../../../components/Badge";
|
|
5
|
+
import { CopyButton } from "../../../components/CopyButton";
|
|
6
|
+
import { useDocsUiConfig } from "../../../context";
|
|
7
|
+
import { Prose } from "../schema/Prose";
|
|
8
|
+
import { SchemaTree } from "../schema/SchemaTree";
|
|
9
|
+
import { methodTone } from "./model";
|
|
10
|
+
import { TryItPanel } from "./TryItPanel";
|
|
11
|
+
|
|
12
|
+
/** Props of {@link OperationView}. */
|
|
13
|
+
export interface OperationViewProps {
|
|
14
|
+
/** The operation shown. */
|
|
15
|
+
operation: DocOperation;
|
|
16
|
+
/** The document. */
|
|
17
|
+
root: SchemaRoot;
|
|
18
|
+
/** Opens a component schema (`#/components/schemas/X`) in the side panel. */
|
|
19
|
+
onRef: (ref: string, name: string) => void;
|
|
20
|
+
/** Answers "does the caller hold this permission" (the have/lack badge). Absent: the permission renders plainly. */
|
|
21
|
+
probe?: PermissionProbe;
|
|
22
|
+
/** Whether the API is read-only (a mutation's panel and marker say so). Defaults to `false`. */
|
|
23
|
+
readOnly?: boolean;
|
|
24
|
+
/** After a successful mutation (the host refreshes what it shows). */
|
|
25
|
+
onChanged?: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A method badge. */
|
|
29
|
+
export function MethodBadge({
|
|
30
|
+
method,
|
|
31
|
+
}: {
|
|
32
|
+
/** Upper-case method. */ method: string;
|
|
33
|
+
}) {
|
|
34
|
+
return (
|
|
35
|
+
<Badge
|
|
36
|
+
tone={methodTone(method)}
|
|
37
|
+
className="http-method"
|
|
38
|
+
>
|
|
39
|
+
{method}
|
|
40
|
+
</Badge>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** How an array query parameter is sent, from `style`/`explode`. */
|
|
45
|
+
function styleNote(parameter: DocParameter): string | undefined {
|
|
46
|
+
if (parameter.in !== "query" || parameter.style === undefined) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
if (parameter.style === "form" && parameter.explode !== false) {
|
|
50
|
+
return "Repeat the key for each value (state=a&state=b).";
|
|
51
|
+
}
|
|
52
|
+
return `style: ${parameter.style}${parameter.explode === undefined ? "" : `, explode: ${String(parameter.explode)}`}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The parameters, grouped by where they go. */
|
|
56
|
+
function Parameters({ operation, root, onRef }: OperationViewProps) {
|
|
57
|
+
if (operation.parameters.length === 0) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
return (
|
|
61
|
+
<section
|
|
62
|
+
className="http-section"
|
|
63
|
+
aria-labelledby={`params-${operation.id}`}
|
|
64
|
+
>
|
|
65
|
+
<h3 id={`params-${operation.id}`}>Parameters</h3>
|
|
66
|
+
<ul
|
|
67
|
+
className="http-params"
|
|
68
|
+
data-testid="op-parameters"
|
|
69
|
+
>
|
|
70
|
+
{operation.parameters.map((parameter) => {
|
|
71
|
+
const note = styleNote(parameter);
|
|
72
|
+
return (
|
|
73
|
+
<li
|
|
74
|
+
key={`${parameter.in}:${parameter.name}`}
|
|
75
|
+
className="http-param"
|
|
76
|
+
data-param={parameter.name}
|
|
77
|
+
>
|
|
78
|
+
<div className="http-param-head">
|
|
79
|
+
<code className="http-param-name">{parameter.name}</code>
|
|
80
|
+
<Badge tone="neutral">{parameter.in}</Badge>
|
|
81
|
+
{parameter.required ? (
|
|
82
|
+
<span className="schema-required">required</span>
|
|
83
|
+
) : (
|
|
84
|
+
<span className="http-muted">optional</span>
|
|
85
|
+
)}
|
|
86
|
+
{parameter.deprecated && (
|
|
87
|
+
<Badge tone="warning">deprecated</Badge>
|
|
88
|
+
)}
|
|
89
|
+
</div>
|
|
90
|
+
{parameter.description && (
|
|
91
|
+
<div className="http-param-description">
|
|
92
|
+
<Prose text={parameter.description} />
|
|
93
|
+
</div>
|
|
94
|
+
)}
|
|
95
|
+
{note && <p className="http-muted http-param-style">{note}</p>}
|
|
96
|
+
<SchemaTree
|
|
97
|
+
schema={parameter.schema}
|
|
98
|
+
root={root}
|
|
99
|
+
depth={0}
|
|
100
|
+
onRef={onRef}
|
|
101
|
+
label={`Schema of ${parameter.name}`}
|
|
102
|
+
/>
|
|
103
|
+
</li>
|
|
104
|
+
);
|
|
105
|
+
})}
|
|
106
|
+
</ul>
|
|
107
|
+
</section>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The request body. */
|
|
112
|
+
function RequestBody({ operation, root, onRef }: OperationViewProps) {
|
|
113
|
+
const body = operation.requestBody;
|
|
114
|
+
if (!body) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
return (
|
|
118
|
+
<section
|
|
119
|
+
className="http-section"
|
|
120
|
+
aria-labelledby={`body-${operation.id}`}
|
|
121
|
+
data-testid="op-body"
|
|
122
|
+
>
|
|
123
|
+
<h3 id={`body-${operation.id}`}>
|
|
124
|
+
Request body{" "}
|
|
125
|
+
<span className="http-muted">
|
|
126
|
+
{body.required ? "(required)" : "(optional)"}
|
|
127
|
+
</span>
|
|
128
|
+
</h3>
|
|
129
|
+
{body.description && <Prose text={body.description} />}
|
|
130
|
+
{body.content.map((media) => (
|
|
131
|
+
<div
|
|
132
|
+
key={media.contentType}
|
|
133
|
+
className="http-media"
|
|
134
|
+
>
|
|
135
|
+
<code className="http-media-type">{media.contentType}</code>
|
|
136
|
+
{media.schema !== undefined && (
|
|
137
|
+
<SchemaTree
|
|
138
|
+
schema={media.schema}
|
|
139
|
+
root={root}
|
|
140
|
+
depth={2}
|
|
141
|
+
onRef={onRef}
|
|
142
|
+
label={`Request body schema (${media.contentType})`}
|
|
143
|
+
/>
|
|
144
|
+
)}
|
|
145
|
+
</div>
|
|
146
|
+
))}
|
|
147
|
+
</section>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** A status badge's tone. */
|
|
152
|
+
function statusTone(
|
|
153
|
+
status: string,
|
|
154
|
+
): "success" | "warning" | "danger" | "neutral" {
|
|
155
|
+
if (status.startsWith("2")) {
|
|
156
|
+
return "success";
|
|
157
|
+
}
|
|
158
|
+
if (status.startsWith("4")) {
|
|
159
|
+
return "warning";
|
|
160
|
+
}
|
|
161
|
+
if (status.startsWith("5")) {
|
|
162
|
+
return "danger";
|
|
163
|
+
}
|
|
164
|
+
return "neutral";
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** One response. */
|
|
168
|
+
function ResponseItem({
|
|
169
|
+
response,
|
|
170
|
+
root,
|
|
171
|
+
onRef,
|
|
172
|
+
}: {
|
|
173
|
+
/** The response. */
|
|
174
|
+
response: DocResponse;
|
|
175
|
+
/** The document. */
|
|
176
|
+
root: SchemaRoot;
|
|
177
|
+
/** Opens a component. */
|
|
178
|
+
onRef: OperationViewProps["onRef"];
|
|
179
|
+
}) {
|
|
180
|
+
const success = response.status.startsWith("2");
|
|
181
|
+
return (
|
|
182
|
+
<li
|
|
183
|
+
className="http-response"
|
|
184
|
+
data-status={response.status}
|
|
185
|
+
>
|
|
186
|
+
<div className="http-response-head">
|
|
187
|
+
<Badge tone={statusTone(response.status)}>{response.status}</Badge>
|
|
188
|
+
<span>{response.description}</span>
|
|
189
|
+
</div>
|
|
190
|
+
{response.codes.length > 0 && (
|
|
191
|
+
<ul
|
|
192
|
+
className="http-codes"
|
|
193
|
+
aria-label={`Problem codes for ${response.status}`}
|
|
194
|
+
>
|
|
195
|
+
{response.codes.map((code) => (
|
|
196
|
+
<li key={code}>
|
|
197
|
+
<code>{code}</code>
|
|
198
|
+
</li>
|
|
199
|
+
))}
|
|
200
|
+
</ul>
|
|
201
|
+
)}
|
|
202
|
+
{response.content.map((media) => (
|
|
203
|
+
<div
|
|
204
|
+
key={media.contentType}
|
|
205
|
+
className="http-media"
|
|
206
|
+
>
|
|
207
|
+
<code className="http-media-type">{media.contentType}</code>
|
|
208
|
+
{media.schema !== undefined && (
|
|
209
|
+
<SchemaTree
|
|
210
|
+
schema={media.schema}
|
|
211
|
+
root={root}
|
|
212
|
+
depth={success ? 1 : 0}
|
|
213
|
+
onRef={onRef}
|
|
214
|
+
label={`Response ${response.status} schema (${media.contentType})`}
|
|
215
|
+
/>
|
|
216
|
+
)}
|
|
217
|
+
</div>
|
|
218
|
+
))}
|
|
219
|
+
</li>
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* One operation: method and path, summary and description, the permission
|
|
225
|
+
* it needs, whether it is a mutation, its CSRF rules and driver requirements,
|
|
226
|
+
* its parameters, body and responses (schemas as trees), and its try-it panel.
|
|
227
|
+
*/
|
|
228
|
+
export function OperationView({
|
|
229
|
+
operation,
|
|
230
|
+
root,
|
|
231
|
+
onRef,
|
|
232
|
+
probe,
|
|
233
|
+
readOnly = false,
|
|
234
|
+
onChanged,
|
|
235
|
+
}: OperationViewProps) {
|
|
236
|
+
const config = useDocsUiConfig();
|
|
237
|
+
const held =
|
|
238
|
+
operation.action === undefined || probe === undefined
|
|
239
|
+
? undefined
|
|
240
|
+
: probe(operation.action);
|
|
241
|
+
const csrf = operation.csrf;
|
|
242
|
+
return (
|
|
243
|
+
<article
|
|
244
|
+
className="http-operation"
|
|
245
|
+
aria-labelledby={`op-${operation.id}`}
|
|
246
|
+
data-testid="http-operation"
|
|
247
|
+
>
|
|
248
|
+
<header className="http-operation-head">
|
|
249
|
+
<div className="http-operation-line">
|
|
250
|
+
<MethodBadge method={operation.method} />
|
|
251
|
+
<code
|
|
252
|
+
className="http-operation-path"
|
|
253
|
+
data-testid="op-path"
|
|
254
|
+
>
|
|
255
|
+
{operation.path}
|
|
256
|
+
</code>
|
|
257
|
+
{operation.deprecated && <Badge tone="warning">deprecated</Badge>}
|
|
258
|
+
</div>
|
|
259
|
+
<h2
|
|
260
|
+
id={`op-${operation.id}`}
|
|
261
|
+
className="http-operation-title"
|
|
262
|
+
>
|
|
263
|
+
{operation.summary ?? operation.id}
|
|
264
|
+
</h2>
|
|
265
|
+
<p className="http-operation-id">
|
|
266
|
+
<span className="http-muted">operationId</span>{" "}
|
|
267
|
+
<code>{operation.id}</code>
|
|
268
|
+
<CopyButton
|
|
269
|
+
text={operation.id}
|
|
270
|
+
ariaLabel="Copy the operation id"
|
|
271
|
+
/>
|
|
272
|
+
</p>
|
|
273
|
+
</header>
|
|
274
|
+
|
|
275
|
+
<div
|
|
276
|
+
className="http-markers"
|
|
277
|
+
aria-label="Access"
|
|
278
|
+
role="group"
|
|
279
|
+
>
|
|
280
|
+
{operation.action !== undefined &&
|
|
281
|
+
(probe === undefined ? (
|
|
282
|
+
<Badge
|
|
283
|
+
tone="neutral"
|
|
284
|
+
title="The permission the API's authorize hook is asked about."
|
|
285
|
+
>
|
|
286
|
+
<span data-testid="op-permission">
|
|
287
|
+
Needs <code>{operation.action}</code>
|
|
288
|
+
</span>
|
|
289
|
+
</Badge>
|
|
290
|
+
) : (
|
|
291
|
+
<Badge
|
|
292
|
+
tone={held ? "success" : "danger"}
|
|
293
|
+
title="From the caller's permissions. authorize may still decide per target."
|
|
294
|
+
>
|
|
295
|
+
<span
|
|
296
|
+
data-testid="op-permission"
|
|
297
|
+
data-allowed={String(held ?? false)}
|
|
298
|
+
>
|
|
299
|
+
{held ? "You have " : "You lack "}
|
|
300
|
+
<code>{operation.action}</code>
|
|
301
|
+
</span>
|
|
302
|
+
</Badge>
|
|
303
|
+
))}
|
|
304
|
+
<Badge
|
|
305
|
+
tone={operation.mutation ? "warning" : "neutral"}
|
|
306
|
+
title={
|
|
307
|
+
operation.mutation
|
|
308
|
+
? "Changes state: CSRF checks apply, and a read-only API refuses it."
|
|
309
|
+
: "Reads only."
|
|
310
|
+
}
|
|
311
|
+
>
|
|
312
|
+
<span data-testid="op-mutation">
|
|
313
|
+
{operation.mutation ? "Mutation" : "Read"}
|
|
314
|
+
</span>
|
|
315
|
+
</Badge>
|
|
316
|
+
{/* Only for a stale or foreign document: a read-only API documents no mutation (see tryItGate). */}
|
|
317
|
+
{operation.mutation && readOnly && (
|
|
318
|
+
<Badge tone="danger">Refused: the API is read-only</Badge>
|
|
319
|
+
)}
|
|
320
|
+
{operation.requires.length > 0 && (
|
|
321
|
+
<span
|
|
322
|
+
className="http-requires"
|
|
323
|
+
data-testid="op-requires"
|
|
324
|
+
>
|
|
325
|
+
<span className="http-muted">Needs driver support:</span>{" "}
|
|
326
|
+
{operation.requires.map((method) => (
|
|
327
|
+
<code key={method}>{method}</code>
|
|
328
|
+
))}
|
|
329
|
+
</span>
|
|
330
|
+
)}
|
|
331
|
+
</div>
|
|
332
|
+
|
|
333
|
+
{csrf && (
|
|
334
|
+
<div
|
|
335
|
+
className="http-csrf"
|
|
336
|
+
role="note"
|
|
337
|
+
data-testid="op-csrf"
|
|
338
|
+
>
|
|
339
|
+
{csrf.header !== null && (
|
|
340
|
+
<p>
|
|
341
|
+
Every mutation must carry the <code>{csrf.header}</code> header,
|
|
342
|
+
with any non-empty value.{" "}
|
|
343
|
+
{config.csrfHeader === csrf.header
|
|
344
|
+
? "The app sends it."
|
|
345
|
+
: "This UI is not configured to send it."}
|
|
346
|
+
</p>
|
|
347
|
+
)}
|
|
348
|
+
{csrf.requireJson && (
|
|
349
|
+
<p>
|
|
350
|
+
Send <code>Content-Type: application/json</code>, even with no
|
|
351
|
+
body; any other media type is refused with 415.
|
|
352
|
+
</p>
|
|
353
|
+
)}
|
|
354
|
+
{csrf.header === null && !csrf.requireJson && (
|
|
355
|
+
<p>Cross-site requests are refused (Origin / Sec-Fetch-Site).</p>
|
|
356
|
+
)}
|
|
357
|
+
</div>
|
|
358
|
+
)}
|
|
359
|
+
|
|
360
|
+
{operation.description && (
|
|
361
|
+
<div className="http-description">
|
|
362
|
+
<Prose text={operation.description} />
|
|
363
|
+
</div>
|
|
364
|
+
)}
|
|
365
|
+
|
|
366
|
+
<Parameters
|
|
367
|
+
operation={operation}
|
|
368
|
+
root={root}
|
|
369
|
+
onRef={onRef}
|
|
370
|
+
/>
|
|
371
|
+
<RequestBody
|
|
372
|
+
operation={operation}
|
|
373
|
+
root={root}
|
|
374
|
+
onRef={onRef}
|
|
375
|
+
/>
|
|
376
|
+
|
|
377
|
+
<section
|
|
378
|
+
className="http-section"
|
|
379
|
+
aria-labelledby={`responses-${operation.id}`}
|
|
380
|
+
>
|
|
381
|
+
<h3 id={`responses-${operation.id}`}>Responses</h3>
|
|
382
|
+
<ul
|
|
383
|
+
className="http-responses"
|
|
384
|
+
data-testid="op-responses"
|
|
385
|
+
>
|
|
386
|
+
{operation.responses.map((response) => (
|
|
387
|
+
<ResponseItem
|
|
388
|
+
key={response.status}
|
|
389
|
+
response={response}
|
|
390
|
+
root={root}
|
|
391
|
+
onRef={onRef}
|
|
392
|
+
/>
|
|
393
|
+
))}
|
|
394
|
+
</ul>
|
|
395
|
+
</section>
|
|
396
|
+
|
|
397
|
+
<TryItPanel
|
|
398
|
+
key={operation.id}
|
|
399
|
+
operation={operation}
|
|
400
|
+
root={root}
|
|
401
|
+
readOnly={readOnly}
|
|
402
|
+
probe={probe}
|
|
403
|
+
onChanged={onChanged}
|
|
404
|
+
/>
|
|
405
|
+
</article>
|
|
406
|
+
);
|
|
407
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import type { KeyboardEvent } from "react";
|
|
2
|
+
import type { DocSection, DocTagGroup } from "./model";
|
|
3
|
+
import { useRef } from "react";
|
|
4
|
+
import { keyedSections } from "./model";
|
|
5
|
+
import { TextInput } from "../../../components/inputs";
|
|
6
|
+
import { arrowTarget } from "../../../components/listKeys";
|
|
7
|
+
import { SEARCH_SHORTCUT } from "../../../shortcuts";
|
|
8
|
+
import { Link } from "../../../router";
|
|
9
|
+
import { MethodBadge } from "./OperationView";
|
|
10
|
+
|
|
11
|
+
/** Props of {@link Sidebar}. */
|
|
12
|
+
export interface SidebarProps {
|
|
13
|
+
/** The groups to list, already filtered by the search. */
|
|
14
|
+
groups: readonly DocTagGroup[];
|
|
15
|
+
/**
|
|
16
|
+
* The document's sections over those groups (`x-tagGroups`), already
|
|
17
|
+
* filtered: each heads its tags, one level deeper. Default: none, and the
|
|
18
|
+
* groups are listed at the top level.
|
|
19
|
+
*/
|
|
20
|
+
sections?: readonly DocSection[];
|
|
21
|
+
/** The search text (`?q=`). */
|
|
22
|
+
query: string;
|
|
23
|
+
/** Sets the search text. */
|
|
24
|
+
onQuery: (query: string) => void;
|
|
25
|
+
/** The link to an operation, keeping the search. */
|
|
26
|
+
hrefOf: (operationId: string) => string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The links of a sidebar, in order. */
|
|
30
|
+
function links(root: HTMLElement | null): HTMLAnchorElement[] {
|
|
31
|
+
return root
|
|
32
|
+
? Array.from(root.querySelectorAll<HTMLAnchorElement>("a.http-op-link"))
|
|
33
|
+
: [];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** One tag's heading and its operations' links. */
|
|
37
|
+
function TagBlock({
|
|
38
|
+
group,
|
|
39
|
+
headingId,
|
|
40
|
+
level,
|
|
41
|
+
hrefOf,
|
|
42
|
+
}: {
|
|
43
|
+
/** The tag and its operations. */
|
|
44
|
+
group: DocTagGroup;
|
|
45
|
+
/** The heading's id, unique in the sidebar (a tag can be in two sections). */
|
|
46
|
+
headingId: string;
|
|
47
|
+
/** The heading's level: 2 at the top, 3 inside a section. */
|
|
48
|
+
level: 2 | 3;
|
|
49
|
+
/** The link to an operation. */
|
|
50
|
+
hrefOf: (operationId: string) => string;
|
|
51
|
+
}) {
|
|
52
|
+
const Heading = level === 2 ? "h2" : "h3";
|
|
53
|
+
return (
|
|
54
|
+
<section
|
|
55
|
+
className="http-tag"
|
|
56
|
+
aria-labelledby={headingId}
|
|
57
|
+
>
|
|
58
|
+
<Heading
|
|
59
|
+
id={headingId}
|
|
60
|
+
className="http-tag-name"
|
|
61
|
+
title={group.description}
|
|
62
|
+
>
|
|
63
|
+
{group.name}
|
|
64
|
+
</Heading>
|
|
65
|
+
<ul className="http-tag-ops">
|
|
66
|
+
{group.operations.map((operation) => (
|
|
67
|
+
<li key={operation.id}>
|
|
68
|
+
<Link
|
|
69
|
+
to={hrefOf(operation.id)}
|
|
70
|
+
className="http-op-link"
|
|
71
|
+
activeMatch="exact"
|
|
72
|
+
data-operation={operation.id}
|
|
73
|
+
>
|
|
74
|
+
<MethodBadge method={operation.method} />
|
|
75
|
+
<span className="http-op-text">
|
|
76
|
+
<span className="http-op-path">{operation.path}</span>
|
|
77
|
+
{operation.summary && (
|
|
78
|
+
<span className="http-op-summary">{operation.summary}</span>
|
|
79
|
+
)}
|
|
80
|
+
</span>
|
|
81
|
+
</Link>
|
|
82
|
+
</li>
|
|
83
|
+
))}
|
|
84
|
+
</ul>
|
|
85
|
+
</section>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The operations by tag (by section, then tag, when the document declares
|
|
91
|
+
* `x-tagGroups`), with method badges, and a search over path,
|
|
92
|
+
* operation id and summary. Arrow keys move between operations (Home/End to
|
|
93
|
+
* the ends), and ArrowDown from the search goes to the first match.
|
|
94
|
+
*/
|
|
95
|
+
export function Sidebar({
|
|
96
|
+
groups,
|
|
97
|
+
sections,
|
|
98
|
+
query,
|
|
99
|
+
onQuery,
|
|
100
|
+
hrefOf,
|
|
101
|
+
}: SidebarProps) {
|
|
102
|
+
const listRef = useRef<HTMLDivElement>(null);
|
|
103
|
+
const count = groups.reduce((sum, group) => sum + group.operations.length, 0);
|
|
104
|
+
|
|
105
|
+
function onListKey(event: KeyboardEvent<HTMLDivElement>) {
|
|
106
|
+
const target = arrowTarget(
|
|
107
|
+
event.key,
|
|
108
|
+
links(listRef.current),
|
|
109
|
+
document.activeElement,
|
|
110
|
+
);
|
|
111
|
+
if (target) {
|
|
112
|
+
event.preventDefault();
|
|
113
|
+
target.focus();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<nav
|
|
119
|
+
className="http-sidebar"
|
|
120
|
+
aria-label="Operations"
|
|
121
|
+
>
|
|
122
|
+
<TextInput
|
|
123
|
+
type="search"
|
|
124
|
+
value={query}
|
|
125
|
+
aria-label="Search operations"
|
|
126
|
+
placeholder="Search path, id, summary"
|
|
127
|
+
{...SEARCH_SHORTCUT}
|
|
128
|
+
onChange={(value) => onQuery(value)}
|
|
129
|
+
onKeyDown={(event) => {
|
|
130
|
+
if (event.key === "ArrowDown") {
|
|
131
|
+
const first = links(listRef.current)[0];
|
|
132
|
+
if (first) {
|
|
133
|
+
event.preventDefault();
|
|
134
|
+
first.focus();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}}
|
|
138
|
+
/>
|
|
139
|
+
<p
|
|
140
|
+
className="http-muted http-sidebar-count"
|
|
141
|
+
aria-live="polite"
|
|
142
|
+
>
|
|
143
|
+
{count === 1 ? "1 operation" : `${count} operations`}
|
|
144
|
+
</p>
|
|
145
|
+
{/* Arrow keys move between the links; each link stays a tab stop too. */}
|
|
146
|
+
<div
|
|
147
|
+
ref={listRef}
|
|
148
|
+
className="http-sidebar-list"
|
|
149
|
+
onKeyDown={onListKey}
|
|
150
|
+
>
|
|
151
|
+
{sections === undefined
|
|
152
|
+
? groups.map((group) => (
|
|
153
|
+
<TagBlock
|
|
154
|
+
key={group.name}
|
|
155
|
+
group={group}
|
|
156
|
+
headingId={`tag-${group.name}`}
|
|
157
|
+
level={2}
|
|
158
|
+
hrefOf={hrefOf}
|
|
159
|
+
/>
|
|
160
|
+
))
|
|
161
|
+
: keyedSections(sections).map(({ key, index, section }) => (
|
|
162
|
+
<section
|
|
163
|
+
key={key}
|
|
164
|
+
className="http-section"
|
|
165
|
+
aria-labelledby={`section-${index}`}
|
|
166
|
+
>
|
|
167
|
+
<h2
|
|
168
|
+
id={`section-${index}`}
|
|
169
|
+
className="http-section-name"
|
|
170
|
+
>
|
|
171
|
+
{section.name}
|
|
172
|
+
</h2>
|
|
173
|
+
{section.tags.map((group) => (
|
|
174
|
+
<TagBlock
|
|
175
|
+
key={group.name}
|
|
176
|
+
group={group}
|
|
177
|
+
headingId={`tag-${index}-${group.name}`}
|
|
178
|
+
level={3}
|
|
179
|
+
hrefOf={hrefOf}
|
|
180
|
+
/>
|
|
181
|
+
))}
|
|
182
|
+
</section>
|
|
183
|
+
))}
|
|
184
|
+
{groups.length === 0 && (
|
|
185
|
+
<p className="http-muted">No operation matches.</p>
|
|
186
|
+
)}
|
|
187
|
+
</div>
|
|
188
|
+
</nav>
|
|
189
|
+
);
|
|
190
|
+
}
|