@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,603 @@
|
|
|
1
|
+
import type { RawResponse } from "../../../api/client";
|
|
2
|
+
import type { JsonEditorState } from "../../../components/jsonParse";
|
|
3
|
+
import type { PermissionProbe } from "../permissions";
|
|
4
|
+
import type { SchemaRoot } from "../schema/resolve";
|
|
5
|
+
import type { DocOperation, DocParameter } from "./model";
|
|
6
|
+
import type { FieldValue, InputKind, TryItRequest, TryItValues } from "./tryIt";
|
|
7
|
+
import { useMemo, useState } from "react";
|
|
8
|
+
import { isApiError } from "../../../api/errors";
|
|
9
|
+
import { Badge } from "../../../components/Badge";
|
|
10
|
+
import { Button } from "../../../components/Button";
|
|
11
|
+
import { ConfirmDialog } from "../../../components/ConfirmDialog";
|
|
12
|
+
import { CopyButton } from "../../../components/CopyButton";
|
|
13
|
+
import { Field } from "../../../components/Field";
|
|
14
|
+
import { Checkbox, Select, TextInput } from "../../../components/inputs";
|
|
15
|
+
import { JsonEditor } from "../../../components/JsonEditor";
|
|
16
|
+
import { jsonEditorState } from "../../../components/jsonParse";
|
|
17
|
+
import { JsonView } from "../../../components/JsonView";
|
|
18
|
+
import { ProblemBanner } from "../../../components/ProblemBanner";
|
|
19
|
+
import { Tabs } from "../../../components/Tabs";
|
|
20
|
+
import { useDocsClient, useDocsUiConfig } from "../../../context";
|
|
21
|
+
import {
|
|
22
|
+
bodySchema,
|
|
23
|
+
buildRequest,
|
|
24
|
+
curlSnippet,
|
|
25
|
+
documentedNote,
|
|
26
|
+
fetchSnippet,
|
|
27
|
+
formParameters,
|
|
28
|
+
initialBodyText,
|
|
29
|
+
inputKind,
|
|
30
|
+
isDestructive,
|
|
31
|
+
paramKey,
|
|
32
|
+
tryItGate,
|
|
33
|
+
} from "./tryIt";
|
|
34
|
+
|
|
35
|
+
/** What one send produced. */
|
|
36
|
+
type TryItResult =
|
|
37
|
+
| {
|
|
38
|
+
/** A response arrived, whatever its status. */
|
|
39
|
+
ok: true;
|
|
40
|
+
/** The response: real status, visible headers, body and timing. */
|
|
41
|
+
response: RawResponse;
|
|
42
|
+
}
|
|
43
|
+
| {
|
|
44
|
+
/** No response: a network failure (an `ApiError` of kind `network`) or anything else thrown. */
|
|
45
|
+
ok: false;
|
|
46
|
+
/** What was thrown. */
|
|
47
|
+
error: unknown;
|
|
48
|
+
/** Milliseconds from send to failure. */
|
|
49
|
+
ms: number;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** Props of {@link TryItPanel}. */
|
|
53
|
+
export interface TryItPanelProps {
|
|
54
|
+
/** The operation to send. */
|
|
55
|
+
operation: DocOperation;
|
|
56
|
+
/** The document, for `$ref`s in parameter and body schemas. */
|
|
57
|
+
root: SchemaRoot;
|
|
58
|
+
/** Whether the API is read-only (the panel is off for mutations). Defaults to `false`. */
|
|
59
|
+
readOnly?: boolean;
|
|
60
|
+
/** Answers "does the caller hold this permission" (the panel is off when it is lacked). Absent: every operation may be tried. */
|
|
61
|
+
probe?: PermissionProbe;
|
|
62
|
+
/** After a successful mutation (the host refreshes what it shows). */
|
|
63
|
+
onChanged?: () => void;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Hints per input kind. */
|
|
67
|
+
function kindHint(
|
|
68
|
+
kind: InputKind,
|
|
69
|
+
parameter: DocParameter,
|
|
70
|
+
): string | undefined {
|
|
71
|
+
switch (kind.kind) {
|
|
72
|
+
case "list":
|
|
73
|
+
return "Comma-separated; each value is sent as a repeated key (name=a&name=b).";
|
|
74
|
+
case "enum-list":
|
|
75
|
+
return "Each ticked value is sent as a repeated key.";
|
|
76
|
+
case "number": {
|
|
77
|
+
const bounds = [
|
|
78
|
+
kind.min !== undefined ? `min ${kind.min}` : undefined,
|
|
79
|
+
kind.max !== undefined ? `max ${kind.max}` : undefined,
|
|
80
|
+
].filter(Boolean);
|
|
81
|
+
return [kind.integer ? "An integer" : "A number", ...bounds].join(", ");
|
|
82
|
+
}
|
|
83
|
+
default:
|
|
84
|
+
return parameter.in === "path"
|
|
85
|
+
? "Percent-encoded as one path segment."
|
|
86
|
+
: undefined;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** One parameter's control. */
|
|
91
|
+
function ParameterInput({
|
|
92
|
+
parameter,
|
|
93
|
+
kind,
|
|
94
|
+
value,
|
|
95
|
+
onChange,
|
|
96
|
+
disabled,
|
|
97
|
+
}: {
|
|
98
|
+
/** The parameter. */
|
|
99
|
+
parameter: DocParameter;
|
|
100
|
+
/** Its input kind. */
|
|
101
|
+
kind: InputKind;
|
|
102
|
+
/** Its value. */
|
|
103
|
+
value: FieldValue | undefined;
|
|
104
|
+
/** Sets it. */
|
|
105
|
+
onChange: (value: FieldValue) => void;
|
|
106
|
+
/** Whether the panel is off. */
|
|
107
|
+
disabled: boolean;
|
|
108
|
+
}) {
|
|
109
|
+
const text = typeof value === "string" ? value : "";
|
|
110
|
+
switch (kind.kind) {
|
|
111
|
+
case "boolean":
|
|
112
|
+
case "enum": {
|
|
113
|
+
const options =
|
|
114
|
+
kind.kind === "boolean" ? ["true", "false"] : kind.options;
|
|
115
|
+
return (
|
|
116
|
+
<Select<string>
|
|
117
|
+
value={text}
|
|
118
|
+
disabled={disabled}
|
|
119
|
+
options={[
|
|
120
|
+
{ value: "", label: parameter.required ? "Choose…" : "(not sent)" },
|
|
121
|
+
...options.map((option) => ({ value: option })),
|
|
122
|
+
]}
|
|
123
|
+
onChange={onChange}
|
|
124
|
+
/>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
case "enum-list": {
|
|
128
|
+
const picked = Array.isArray(value) ? value : [];
|
|
129
|
+
return (
|
|
130
|
+
<div
|
|
131
|
+
className="http-tryit-checks"
|
|
132
|
+
role="group"
|
|
133
|
+
aria-label={parameter.name}
|
|
134
|
+
>
|
|
135
|
+
{kind.options.map((option) => (
|
|
136
|
+
<Checkbox
|
|
137
|
+
key={option}
|
|
138
|
+
label={option}
|
|
139
|
+
disabled={disabled}
|
|
140
|
+
checked={picked.includes(option)}
|
|
141
|
+
onChange={(checked) =>
|
|
142
|
+
onChange(
|
|
143
|
+
checked
|
|
144
|
+
? [...picked, option]
|
|
145
|
+
: picked.filter((item) => item !== option),
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
/>
|
|
149
|
+
))}
|
|
150
|
+
</div>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
default:
|
|
154
|
+
return (
|
|
155
|
+
<TextInput
|
|
156
|
+
value={text}
|
|
157
|
+
disabled={disabled}
|
|
158
|
+
inputMode={kind.kind === "number" ? "decimal" : undefined}
|
|
159
|
+
placeholder={kind.kind === "list" ? "a, b, c" : undefined}
|
|
160
|
+
onChange={(next) => onChange(next)}
|
|
161
|
+
/>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** The response headers the browser let the page see. */
|
|
167
|
+
function HeadersView({
|
|
168
|
+
headers,
|
|
169
|
+
}: {
|
|
170
|
+
/** Lower-cased names to values, in order. */
|
|
171
|
+
headers: Record<string, string>;
|
|
172
|
+
}) {
|
|
173
|
+
const entries = Object.entries(headers);
|
|
174
|
+
return (
|
|
175
|
+
<details
|
|
176
|
+
className="http-tryit-headers"
|
|
177
|
+
data-testid="tryit-headers"
|
|
178
|
+
>
|
|
179
|
+
<summary>{`Headers (${entries.length})`}</summary>
|
|
180
|
+
{entries.length === 0 ? (
|
|
181
|
+
<p className="http-muted">No headers visible to the page.</p>
|
|
182
|
+
) : (
|
|
183
|
+
<dl>
|
|
184
|
+
{entries.map(([name, value]) => (
|
|
185
|
+
<div
|
|
186
|
+
key={name}
|
|
187
|
+
data-header={name}
|
|
188
|
+
>
|
|
189
|
+
<dt>
|
|
190
|
+
<code>{name}</code>
|
|
191
|
+
</dt>
|
|
192
|
+
<dd>
|
|
193
|
+
<code>{value}</code>
|
|
194
|
+
</dd>
|
|
195
|
+
</div>
|
|
196
|
+
))}
|
|
197
|
+
</dl>
|
|
198
|
+
)}
|
|
199
|
+
</details>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** A response body: a JSON tree, text, or "No body." */
|
|
204
|
+
function BodyView({
|
|
205
|
+
body,
|
|
206
|
+
label,
|
|
207
|
+
}: {
|
|
208
|
+
/** The parsed JSON, the text, or `undefined`. */
|
|
209
|
+
body: unknown;
|
|
210
|
+
/** The tree's accessible label. */
|
|
211
|
+
label: string;
|
|
212
|
+
}) {
|
|
213
|
+
if (body === undefined) {
|
|
214
|
+
return <p className="http-muted">No body.</p>;
|
|
215
|
+
}
|
|
216
|
+
if (typeof body === "string") {
|
|
217
|
+
return (
|
|
218
|
+
<pre
|
|
219
|
+
className="http-tryit-text"
|
|
220
|
+
aria-label={label}
|
|
221
|
+
>
|
|
222
|
+
<code>{body}</code>
|
|
223
|
+
</pre>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
return (
|
|
227
|
+
<JsonView
|
|
228
|
+
value={body}
|
|
229
|
+
label={label}
|
|
230
|
+
expandDepth={2}
|
|
231
|
+
/>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** The real status, timing, headers and body of the last send. */
|
|
236
|
+
function ResultView({
|
|
237
|
+
result,
|
|
238
|
+
operation,
|
|
239
|
+
}: {
|
|
240
|
+
/** The outcome. */
|
|
241
|
+
result: TryItResult;
|
|
242
|
+
/** The operation, for its documented statuses. */
|
|
243
|
+
operation: DocOperation;
|
|
244
|
+
}) {
|
|
245
|
+
const response = result.ok ? result.response : undefined;
|
|
246
|
+
const error = result.ok ? result.response.error : result.error;
|
|
247
|
+
const problem = isApiError(error) ? error : undefined;
|
|
248
|
+
const note = response
|
|
249
|
+
? documentedNote(operation, response.status)
|
|
250
|
+
: undefined;
|
|
251
|
+
const ms = response ? response.durationMs : result.ok ? 0 : result.ms;
|
|
252
|
+
return (
|
|
253
|
+
<section
|
|
254
|
+
className="http-tryit-result"
|
|
255
|
+
aria-label="Response"
|
|
256
|
+
data-testid="tryit-result"
|
|
257
|
+
>
|
|
258
|
+
<div className="http-tryit-status">
|
|
259
|
+
<Badge tone={response?.ok ? "success" : "danger"}>
|
|
260
|
+
<span data-testid="tryit-status">
|
|
261
|
+
{response
|
|
262
|
+
? String(response.status)
|
|
263
|
+
: problem?.status === 0
|
|
264
|
+
? "No response"
|
|
265
|
+
: "Error"}
|
|
266
|
+
</span>
|
|
267
|
+
</Badge>
|
|
268
|
+
{response?.statusText && (
|
|
269
|
+
<span className="http-muted">{response.statusText}</span>
|
|
270
|
+
)}
|
|
271
|
+
{problem && <code>{problem.code}</code>}
|
|
272
|
+
<span
|
|
273
|
+
className="http-tryit-timing"
|
|
274
|
+
data-testid="tryit-timing"
|
|
275
|
+
>
|
|
276
|
+
{`${Math.round(ms)} ms`}
|
|
277
|
+
</span>
|
|
278
|
+
</div>
|
|
279
|
+
{note && (
|
|
280
|
+
<p
|
|
281
|
+
className="http-muted"
|
|
282
|
+
data-testid="tryit-documented"
|
|
283
|
+
>
|
|
284
|
+
{note}
|
|
285
|
+
</p>
|
|
286
|
+
)}
|
|
287
|
+
{!response && <ProblemBanner error={error} />}
|
|
288
|
+
{response && (
|
|
289
|
+
<>
|
|
290
|
+
{!response.ok && <ProblemBanner error={error} />}
|
|
291
|
+
<HeadersView headers={response.headers} />
|
|
292
|
+
<BodyView
|
|
293
|
+
body={response.body}
|
|
294
|
+
label={
|
|
295
|
+
response.ok
|
|
296
|
+
? "Response body"
|
|
297
|
+
: problem?.kind === "problem"
|
|
298
|
+
? "Problem body"
|
|
299
|
+
: "Response body"
|
|
300
|
+
}
|
|
301
|
+
/>
|
|
302
|
+
</>
|
|
303
|
+
)}
|
|
304
|
+
</section>
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Which snippet is showing. */
|
|
309
|
+
type SnippetTab = "curl" | "fetch";
|
|
310
|
+
|
|
311
|
+
/** The curl / fetch() snippets of the request the form would send. */
|
|
312
|
+
function Snippets({
|
|
313
|
+
request,
|
|
314
|
+
}: {
|
|
315
|
+
/** The request, when the form is complete. */ request:
|
|
316
|
+
TryItRequest | undefined;
|
|
317
|
+
}) {
|
|
318
|
+
const api = useDocsClient();
|
|
319
|
+
const config = useDocsUiConfig();
|
|
320
|
+
const [tab, setTab] = useState<SnippetTab>("curl");
|
|
321
|
+
const context = {
|
|
322
|
+
apiBase: api.base,
|
|
323
|
+
origin: typeof window === "undefined" ? "" : window.location.origin,
|
|
324
|
+
csrfHeader: config.csrfHeader,
|
|
325
|
+
};
|
|
326
|
+
const text = request
|
|
327
|
+
? tab === "curl"
|
|
328
|
+
? curlSnippet(request, context)
|
|
329
|
+
: fetchSnippet(request, context)
|
|
330
|
+
: undefined;
|
|
331
|
+
return (
|
|
332
|
+
<Tabs<SnippetTab>
|
|
333
|
+
label="Snippets"
|
|
334
|
+
value={tab}
|
|
335
|
+
onChange={setTab}
|
|
336
|
+
tabs={[
|
|
337
|
+
{ value: "curl", label: "curl" },
|
|
338
|
+
{ value: "fetch", label: "fetch()" },
|
|
339
|
+
]}
|
|
340
|
+
className="http-snippets"
|
|
341
|
+
>
|
|
342
|
+
{text === undefined ? (
|
|
343
|
+
<p className="http-muted">
|
|
344
|
+
Fill in the required fields to see the snippet.
|
|
345
|
+
</p>
|
|
346
|
+
) : (
|
|
347
|
+
<div className="http-snippet">
|
|
348
|
+
<CopyButton
|
|
349
|
+
text={text}
|
|
350
|
+
ariaLabel={`Copy the ${tab === "curl" ? "curl command" : "fetch() call"}`}
|
|
351
|
+
/>
|
|
352
|
+
<pre data-testid={`snippet-${tab}`}>
|
|
353
|
+
<code>{text}</code>
|
|
354
|
+
</pre>
|
|
355
|
+
</div>
|
|
356
|
+
)}
|
|
357
|
+
</Tabs>
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* The "try it" panel of one operation: a form built from its path and query
|
|
363
|
+
* parameters, a JSON editor for its body (prefilled with the schema's
|
|
364
|
+
* required fields), sent to the document's resolved server with the
|
|
365
|
+
* viewer's own client, so the CSRF header and `Content-Type` rules apply
|
|
366
|
+
* exactly as the snippets promise. Mutations ask first; destructive ones
|
|
367
|
+
* need the operation id typed.
|
|
368
|
+
*/
|
|
369
|
+
export function TryItPanel({
|
|
370
|
+
operation,
|
|
371
|
+
root,
|
|
372
|
+
readOnly = false,
|
|
373
|
+
probe,
|
|
374
|
+
onChanged,
|
|
375
|
+
}: TryItPanelProps) {
|
|
376
|
+
const api = useDocsClient();
|
|
377
|
+
const config = useDocsUiConfig();
|
|
378
|
+
const parameters = formParameters(operation);
|
|
379
|
+
const schema = bodySchema(operation);
|
|
380
|
+
const bodyOptional = operation.requestBody?.required !== true;
|
|
381
|
+
|
|
382
|
+
const [values, setValues] = useState<TryItValues>({});
|
|
383
|
+
const [body, setBody] = useState<JsonEditorState>(() =>
|
|
384
|
+
jsonEditorState(initialBodyText(operation, root), {
|
|
385
|
+
allowEmpty: bodyOptional,
|
|
386
|
+
}),
|
|
387
|
+
);
|
|
388
|
+
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
389
|
+
const [confirming, setConfirming] = useState<TryItRequest | null>(null);
|
|
390
|
+
const [pending, setPending] = useState(false);
|
|
391
|
+
const [result, setResult] = useState<TryItResult | null>(null);
|
|
392
|
+
|
|
393
|
+
// A standalone viewer knows neither the API's read-only setting nor the
|
|
394
|
+
// caller's permissions (no `probe`); the API's answer stays final either way.
|
|
395
|
+
const gate = tryItGate(operation, {
|
|
396
|
+
readOnly,
|
|
397
|
+
permitted:
|
|
398
|
+
operation.action === undefined || probe === undefined
|
|
399
|
+
? true
|
|
400
|
+
: probe(operation.action) === true,
|
|
401
|
+
});
|
|
402
|
+
const disabled = !gate.enabled || pending;
|
|
403
|
+
|
|
404
|
+
const built = useMemo(
|
|
405
|
+
() =>
|
|
406
|
+
buildRequest(
|
|
407
|
+
operation,
|
|
408
|
+
root,
|
|
409
|
+
values,
|
|
410
|
+
schema === undefined ? undefined : body.value,
|
|
411
|
+
),
|
|
412
|
+
[operation, root, values, schema, body.value],
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
async function send(request: TryItRequest): Promise<void> {
|
|
416
|
+
setPending(true);
|
|
417
|
+
const started = performance.now();
|
|
418
|
+
try {
|
|
419
|
+
const response = await api.requestRaw(request.method, request.path, {
|
|
420
|
+
query: request.query,
|
|
421
|
+
body: request.body,
|
|
422
|
+
});
|
|
423
|
+
setResult({ ok: true, response });
|
|
424
|
+
if (operation.mutation && response.ok) {
|
|
425
|
+
// The host shows what this changed.
|
|
426
|
+
onChanged?.();
|
|
427
|
+
}
|
|
428
|
+
} catch (error) {
|
|
429
|
+
setResult({ ok: false, error, ms: performance.now() - started });
|
|
430
|
+
} finally {
|
|
431
|
+
setPending(false);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function submit() {
|
|
436
|
+
if (!gate.enabled) {
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
if (schema !== undefined && !body.valid) {
|
|
440
|
+
setErrors({
|
|
441
|
+
body: body.error
|
|
442
|
+
? `Line ${body.error.line}: ${body.error.message}`
|
|
443
|
+
: "The body is not valid JSON",
|
|
444
|
+
});
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
if (!built.ok) {
|
|
448
|
+
setErrors(built.errors);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
setErrors({});
|
|
452
|
+
if (operation.mutation) {
|
|
453
|
+
setConfirming(built.request);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
void send(built.request);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const destructive = isDestructive(operation);
|
|
460
|
+
const csrfParameter = operation.parameters.find(
|
|
461
|
+
(parameter) =>
|
|
462
|
+
parameter.in === "header" && parameter.name === operation.csrf?.header,
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
return (
|
|
466
|
+
<section
|
|
467
|
+
className="http-tryit"
|
|
468
|
+
aria-labelledby={`tryit-${operation.id}`}
|
|
469
|
+
data-testid="tryit"
|
|
470
|
+
>
|
|
471
|
+
<h3 id={`tryit-${operation.id}`}>Try it</h3>
|
|
472
|
+
{!gate.enabled && (
|
|
473
|
+
<p
|
|
474
|
+
className="http-tryit-disabled"
|
|
475
|
+
role="note"
|
|
476
|
+
data-testid="tryit-disabled"
|
|
477
|
+
>
|
|
478
|
+
{gate.reason}
|
|
479
|
+
</p>
|
|
480
|
+
)}
|
|
481
|
+
<form
|
|
482
|
+
className="http-tryit-form"
|
|
483
|
+
noValidate
|
|
484
|
+
onSubmit={(event) => {
|
|
485
|
+
event.preventDefault();
|
|
486
|
+
submit();
|
|
487
|
+
}}
|
|
488
|
+
>
|
|
489
|
+
{parameters.length === 0 && schema === undefined && (
|
|
490
|
+
<p className="http-muted">No parameters.</p>
|
|
491
|
+
)}
|
|
492
|
+
{parameters.map((parameter) => {
|
|
493
|
+
const key = paramKey(parameter);
|
|
494
|
+
const kind = inputKind(parameter, root);
|
|
495
|
+
return (
|
|
496
|
+
<Field
|
|
497
|
+
key={key}
|
|
498
|
+
label={
|
|
499
|
+
<>
|
|
500
|
+
<code>{parameter.name}</code>{" "}
|
|
501
|
+
<span className="http-muted">{parameter.in}</span>
|
|
502
|
+
</>
|
|
503
|
+
}
|
|
504
|
+
required={parameter.required}
|
|
505
|
+
hint={kindHint(kind, parameter)}
|
|
506
|
+
error={errors[key]}
|
|
507
|
+
>
|
|
508
|
+
<ParameterInput
|
|
509
|
+
parameter={parameter}
|
|
510
|
+
kind={kind}
|
|
511
|
+
value={values[key]}
|
|
512
|
+
disabled={disabled}
|
|
513
|
+
onChange={(next) =>
|
|
514
|
+
setValues((current) => ({ ...current, [key]: next }))
|
|
515
|
+
}
|
|
516
|
+
/>
|
|
517
|
+
</Field>
|
|
518
|
+
);
|
|
519
|
+
})}
|
|
520
|
+
{csrfParameter && (
|
|
521
|
+
<p
|
|
522
|
+
className="http-muted"
|
|
523
|
+
data-testid="tryit-csrf"
|
|
524
|
+
>
|
|
525
|
+
{config.csrfHeader === csrfParameter.name
|
|
526
|
+
? `The ${csrfParameter.name} header is added by the app's client.`
|
|
527
|
+
: config.csrfHeader
|
|
528
|
+
? `The API asks for ${csrfParameter.name}, but this UI sends ${config.csrfHeader}: the API will refuse this with 403 CSRF_REJECTED.`
|
|
529
|
+
: `The API asks for ${csrfParameter.name}, but this UI is configured with no CSRF header: the API will refuse this with 403 CSRF_REJECTED.`}
|
|
530
|
+
</p>
|
|
531
|
+
)}
|
|
532
|
+
{schema !== undefined && (
|
|
533
|
+
<Field
|
|
534
|
+
label="Body (application/json)"
|
|
535
|
+
required={!bodyOptional}
|
|
536
|
+
hint={
|
|
537
|
+
bodyOptional
|
|
538
|
+
? "Optional: leave it empty to send none."
|
|
539
|
+
: "Prefilled with the schema's required fields."
|
|
540
|
+
}
|
|
541
|
+
error={errors.body}
|
|
542
|
+
>
|
|
543
|
+
<JsonEditor
|
|
544
|
+
value={body.text}
|
|
545
|
+
allowEmpty={bodyOptional}
|
|
546
|
+
readOnly={disabled}
|
|
547
|
+
rows={8}
|
|
548
|
+
onChange={setBody}
|
|
549
|
+
/>
|
|
550
|
+
</Field>
|
|
551
|
+
)}
|
|
552
|
+
<div className="http-tryit-actions">
|
|
553
|
+
<Button
|
|
554
|
+
type="submit"
|
|
555
|
+
variant={
|
|
556
|
+
operation.mutation
|
|
557
|
+
? destructive
|
|
558
|
+
? "danger"
|
|
559
|
+
: "primary"
|
|
560
|
+
: "primary"
|
|
561
|
+
}
|
|
562
|
+
disabled={disabled}
|
|
563
|
+
>
|
|
564
|
+
{pending ? "Sending…" : `Send ${operation.method}`}
|
|
565
|
+
</Button>
|
|
566
|
+
{result && (
|
|
567
|
+
<Button
|
|
568
|
+
variant="ghost"
|
|
569
|
+
onClick={() => setResult(null)}
|
|
570
|
+
>
|
|
571
|
+
Clear response
|
|
572
|
+
</Button>
|
|
573
|
+
)}
|
|
574
|
+
</div>
|
|
575
|
+
</form>
|
|
576
|
+
<Snippets request={built.ok ? built.request : undefined} />
|
|
577
|
+
{result && (
|
|
578
|
+
<ResultView
|
|
579
|
+
result={result}
|
|
580
|
+
operation={operation}
|
|
581
|
+
/>
|
|
582
|
+
)}
|
|
583
|
+
<ConfirmDialog
|
|
584
|
+
open={confirming !== null}
|
|
585
|
+
onClose={() => setConfirming(null)}
|
|
586
|
+
title={`Send ${operation.method} ${confirming?.path ?? operation.path}?`}
|
|
587
|
+
description={
|
|
588
|
+
destructive
|
|
589
|
+
? "This changes the real API and cannot be undone."
|
|
590
|
+
: "This changes the real API, as the app's own buttons would."
|
|
591
|
+
}
|
|
592
|
+
variant={destructive ? "danger" : "default"}
|
|
593
|
+
confirmLabel={`Send ${operation.method}`}
|
|
594
|
+
confirmText={destructive ? operation.id : undefined}
|
|
595
|
+
onConfirm={async () => {
|
|
596
|
+
if (confirming) {
|
|
597
|
+
await send(confirming);
|
|
598
|
+
}
|
|
599
|
+
}}
|
|
600
|
+
/>
|
|
601
|
+
</section>
|
|
602
|
+
);
|
|
603
|
+
}
|