@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.
Files changed (240) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +80 -2
  3. package/app/docs/App.tsx +72 -0
  4. package/app/docs/api/client.ts +303 -0
  5. package/app/docs/api/contract.ts +24 -0
  6. package/app/docs/api/docs.ts +38 -0
  7. package/app/docs/api/errors.ts +144 -0
  8. package/app/docs/boot.tsx +58 -0
  9. package/app/docs/components/Badge.tsx +65 -0
  10. package/app/docs/components/Button.tsx +37 -0
  11. package/app/docs/components/Card.tsx +41 -0
  12. package/app/docs/components/ConfirmDialog.tsx +173 -0
  13. package/app/docs/components/CopyButton.tsx +68 -0
  14. package/app/docs/components/Dialog.tsx +230 -0
  15. package/app/docs/components/EmptyState.tsx +34 -0
  16. package/app/docs/components/ErrorView.tsx +70 -0
  17. package/app/docs/components/Field.tsx +93 -0
  18. package/app/docs/components/JsonEditor.tsx +151 -0
  19. package/app/docs/components/JsonView.tsx +338 -0
  20. package/app/docs/components/KeyValue.tsx +63 -0
  21. package/app/docs/components/ProblemBanner.tsx +116 -0
  22. package/app/docs/components/ScreenErrorBoundary.tsx +141 -0
  23. package/app/docs/components/Spinner.tsx +33 -0
  24. package/app/docs/components/Table.tsx +33 -0
  25. package/app/docs/components/Tabs.tsx +226 -0
  26. package/app/docs/components/classNames.ts +4 -0
  27. package/app/docs/components/components.css +691 -0
  28. package/app/docs/components/copy.ts +89 -0
  29. package/app/docs/components/fieldContext.ts +29 -0
  30. package/app/docs/components/inputValues.ts +142 -0
  31. package/app/docs/components/inputs.tsx +387 -0
  32. package/app/docs/components/jsonParse.ts +338 -0
  33. package/app/docs/components/listKeys.ts +30 -0
  34. package/app/docs/config.ts +86 -0
  35. package/app/docs/context.ts +33 -0
  36. package/app/docs/embed.ts +74 -0
  37. package/app/docs/env.d.ts +2 -0
  38. package/app/docs/format.ts +9 -0
  39. package/app/docs/hooks/useUrlTab.ts +16 -0
  40. package/app/docs/main.tsx +11 -0
  41. package/app/docs/queryClient.ts +54 -0
  42. package/app/docs/router.tsx +194 -0
  43. package/app/docs/routing.ts +194 -0
  44. package/app/docs/screens/docs/document.ts +27 -0
  45. package/app/docs/screens/docs/http/OperationView.tsx +407 -0
  46. package/app/docs/screens/docs/http/Sidebar.tsx +190 -0
  47. package/app/docs/screens/docs/http/TryItPanel.tsx +603 -0
  48. package/app/docs/screens/docs/http/http.css +405 -0
  49. package/app/docs/screens/docs/http/index.tsx +395 -0
  50. package/app/docs/screens/docs/http/model.ts +604 -0
  51. package/app/docs/screens/docs/http/tryIt.ts +455 -0
  52. package/app/docs/screens/docs/permissions.ts +6 -0
  53. package/app/docs/screens/docs/schema/Prose.tsx +43 -0
  54. package/app/docs/screens/docs/schema/SchemaTree.tsx +319 -0
  55. package/app/docs/screens/docs/schema/index.ts +30 -0
  56. package/app/docs/screens/docs/schema/resolve.ts +466 -0
  57. package/app/docs/screens/docs/schema/schema.css +137 -0
  58. package/app/docs/screens/docs/schema/skeleton.ts +92 -0
  59. package/app/docs/screens/docs/viewer.tsx +154 -0
  60. package/app/docs/screens/docs/ws/InlineText.tsx +25 -0
  61. package/app/docs/screens/docs/ws/PayloadSchema.tsx +39 -0
  62. package/app/docs/screens/docs/ws/format.ts +82 -0
  63. package/app/docs/screens/docs/ws/index.tsx +286 -0
  64. package/app/docs/screens/docs/ws/items.tsx +786 -0
  65. package/app/docs/screens/docs/ws/model.ts +1178 -0
  66. package/app/docs/screens/docs/ws/panels.tsx +440 -0
  67. package/app/docs/screens/docs/ws/ws.css +231 -0
  68. package/app/docs/shortcuts.ts +94 -0
  69. package/app/docs/styles/base.css +847 -0
  70. package/app/docs/styles/tokens.css +143 -0
  71. package/app/docs/theme.ts +56 -0
  72. package/app/kit/unauthorized.ts +222 -0
  73. package/dist/docs/assets/main-0rchxtv5.js +18 -0
  74. package/dist/docs/assets/main-0rchxtv5.js.map +99 -0
  75. package/dist/docs/assets/main-cqgyvs86.css +1 -0
  76. package/dist/docs/manifest.json +26 -0
  77. package/dist/jobs/assets/AddFlowDialog-h6xkgx9b.js +6 -0
  78. package/dist/jobs/assets/AddFlowDialog-h6xkgx9b.js.map +11 -0
  79. package/dist/jobs/assets/AddFlowDialog-n7gtc84v.css +1 -0
  80. package/dist/jobs/assets/added-cbwsxv3e.js +4 -0
  81. package/dist/jobs/assets/added-cbwsxv3e.js.map +11 -0
  82. package/dist/jobs/assets/bodies-yyw5hmrp.js +6 -0
  83. package/dist/jobs/assets/bodies-yyw5hmrp.js.map +10 -0
  84. package/dist/jobs/assets/channel-tgkvqm8y.js +4 -0
  85. package/dist/jobs/assets/channel-tgkvqm8y.js.map +10 -0
  86. package/dist/jobs/assets/channelChecks-s4yz4yhw.js +4 -0
  87. package/dist/jobs/assets/channelChecks-s4yz4yhw.js.map +10 -0
  88. package/dist/jobs/assets/index-02427smp.css +1 -0
  89. package/dist/jobs/assets/index-09z2cjft.js +4 -0
  90. package/dist/jobs/assets/index-09z2cjft.js.map +16 -0
  91. package/dist/jobs/assets/index-18bn0gvy.css +1 -0
  92. package/dist/jobs/assets/index-47pxmyxn.js +4 -0
  93. package/dist/jobs/assets/index-47pxmyxn.js.map +10 -0
  94. package/dist/jobs/assets/index-4e55z986.js +4 -0
  95. package/dist/jobs/assets/index-4e55z986.js.map +11 -0
  96. package/dist/jobs/assets/index-5c933424.js +4 -0
  97. package/dist/jobs/assets/index-5c933424.js.map +18 -0
  98. package/dist/jobs/assets/index-5kbyqspm.css +1 -0
  99. package/dist/jobs/assets/index-5ng37hk6.css +1 -0
  100. package/dist/jobs/assets/index-5w8w7fvc.css +1 -0
  101. package/dist/jobs/assets/index-71qs5qyx.css +1 -0
  102. package/dist/jobs/assets/index-btpg4r8b.css +1 -0
  103. package/dist/jobs/assets/index-d5r5n0mm.js +4 -0
  104. package/dist/jobs/assets/index-d5r5n0mm.js.map +23 -0
  105. package/dist/jobs/assets/index-e0yy2skq.css +1 -0
  106. package/dist/jobs/assets/index-hs60b25v.js +10 -0
  107. package/dist/jobs/assets/index-hs60b25v.js.map +62 -0
  108. package/dist/jobs/assets/index-k2smrr70.js +4 -0
  109. package/dist/jobs/assets/index-k2smrr70.js.map +9 -0
  110. package/dist/jobs/assets/index-q24jw3w9.js +4 -0
  111. package/dist/jobs/assets/index-q24jw3w9.js.map +32 -0
  112. package/dist/jobs/assets/index-qfb80xen.css +1 -0
  113. package/dist/jobs/assets/index-rtynx6wj.js +4 -0
  114. package/dist/jobs/assets/index-rtynx6wj.js.map +10 -0
  115. package/dist/jobs/assets/live-6k7s6358.js +4 -0
  116. package/dist/jobs/assets/live-6k7s6358.js.map +10 -0
  117. package/dist/jobs/assets/main-0jmeh6mq.js +6 -0
  118. package/dist/jobs/assets/main-0jmeh6mq.js.map +12 -0
  119. package/dist/jobs/assets/main-3yj6bv90.js +5 -0
  120. package/dist/jobs/assets/main-3yj6bv90.js.map +10 -0
  121. package/dist/jobs/assets/main-4g69fn3x.js +5 -0
  122. package/dist/jobs/assets/main-4g69fn3x.js.map +10 -0
  123. package/dist/jobs/assets/main-4gte3eqq.js +5 -0
  124. package/dist/jobs/assets/main-4gte3eqq.js.map +10 -0
  125. package/dist/jobs/assets/main-4j1fcevh.js +5 -0
  126. package/dist/jobs/assets/main-4j1fcevh.js.map +10 -0
  127. package/dist/jobs/assets/main-54qwkz28.css +1 -0
  128. package/dist/jobs/assets/main-5grjvsy7.js +5 -0
  129. package/dist/jobs/assets/main-5grjvsy7.js.map +11 -0
  130. package/dist/jobs/assets/main-5zbx60gq.js +5 -0
  131. package/dist/jobs/assets/main-5zbx60gq.js.map +11 -0
  132. package/dist/jobs/assets/main-9prgjhg4.js +5 -0
  133. package/dist/jobs/assets/main-9prgjhg4.js.map +10 -0
  134. package/dist/jobs/assets/main-a250ffk7.js +17 -0
  135. package/dist/jobs/assets/main-a250ffk7.js.map +115 -0
  136. package/dist/jobs/assets/main-d6ge7w08.js +5 -0
  137. package/dist/jobs/assets/main-d6ge7w08.js.map +10 -0
  138. package/dist/jobs/assets/main-gngzht43.js +5 -0
  139. package/dist/jobs/assets/main-gngzht43.js.map +10 -0
  140. package/dist/jobs/assets/main-hg0hrg8j.js +5 -0
  141. package/dist/jobs/assets/main-hg0hrg8j.js.map +10 -0
  142. package/dist/jobs/assets/main-hm1ng3yg.js +5 -0
  143. package/dist/jobs/assets/main-hm1ng3yg.js.map +12 -0
  144. package/dist/jobs/assets/main-jkvd92z9.js +5 -0
  145. package/dist/jobs/assets/main-jkvd92z9.js.map +10 -0
  146. package/dist/jobs/assets/main-jv5xqd13.js +5 -0
  147. package/dist/jobs/assets/main-jv5xqd13.js.map +21 -0
  148. package/dist/jobs/assets/main-jxre7w93.js +5 -0
  149. package/dist/jobs/assets/main-jxre7w93.js.map +23 -0
  150. package/dist/jobs/assets/main-kwmqkjfx.js +5 -0
  151. package/dist/jobs/assets/main-kwmqkjfx.js.map +11 -0
  152. package/dist/jobs/assets/main-n9cf6bxm.js +5 -0
  153. package/dist/jobs/assets/main-n9cf6bxm.js.map +11 -0
  154. package/dist/jobs/assets/main-pegp0ygb.js +5 -0
  155. package/dist/jobs/assets/main-pegp0ygb.js.map +10 -0
  156. package/dist/jobs/assets/main-qr573853.js +5 -0
  157. package/dist/jobs/assets/main-qr573853.js.map +12 -0
  158. package/dist/jobs/assets/main-rq97rdr4.js +4 -0
  159. package/dist/jobs/assets/main-rq97rdr4.js.map +22 -0
  160. package/dist/jobs/assets/main-szjdmydc.js +5 -0
  161. package/dist/jobs/assets/main-szjdmydc.js.map +13 -0
  162. package/dist/jobs/assets/main-tnbhrm9n.js +7 -0
  163. package/dist/jobs/assets/main-tnbhrm9n.js.map +11 -0
  164. package/dist/jobs/assets/main-ts19j4n0.js +5 -0
  165. package/dist/jobs/assets/main-ts19j4n0.js.map +25 -0
  166. package/dist/jobs/assets/main-w93x1dzs.js +5 -0
  167. package/dist/jobs/assets/main-w93x1dzs.js.map +10 -0
  168. package/dist/jobs/assets/sections-0ksdxs8m.css +1 -0
  169. package/dist/jobs/assets/sections-0y7w4fb4.js +4 -0
  170. package/dist/jobs/assets/sections-0y7w4fb4.js.map +10 -0
  171. package/dist/jobs/assets/sse-mn0t0fh7.js +5 -0
  172. package/dist/jobs/assets/sse-mn0t0fh7.js.map +11 -0
  173. package/dist/jobs/assets/sseChecks-t7tcm7ra.js +4 -0
  174. package/dist/jobs/assets/sseChecks-t7tcm7ra.js.map +10 -0
  175. package/dist/jobs/assets/validation-ysjfy072.js +4 -0
  176. package/dist/jobs/assets/validation-ysjfy072.js.map +9 -0
  177. package/dist/jobs/manifest.json +511 -0
  178. package/dts/docs/assets.d.ts +144 -0
  179. package/dts/docs/assets.d.ts.map +1 -0
  180. package/dts/docs/browser/config.d.ts +61 -0
  181. package/dts/docs/browser/config.d.ts.map +1 -0
  182. package/dts/docs/browser/index.d.ts +12 -0
  183. package/dts/docs/browser/index.d.ts.map +1 -0
  184. package/dts/docs/docsUi.d.ts +42 -0
  185. package/dts/docs/docsUi.d.ts.map +1 -0
  186. package/dts/docs/hosted.d.ts +22 -0
  187. package/dts/docs/hosted.d.ts.map +1 -0
  188. package/dts/docs/index.d.ts +18 -0
  189. package/dts/docs/index.d.ts.map +1 -0
  190. package/dts/docs/shell.d.ts +57 -0
  191. package/dts/docs/shell.d.ts.map +1 -0
  192. package/dts/docs/types.d.ts +137 -0
  193. package/dts/docs/types.d.ts.map +1 -0
  194. package/dts/index.d.ts +21 -0
  195. package/dts/index.d.ts.map +1 -0
  196. package/dts/jobs/assets.d.ts +157 -0
  197. package/dts/jobs/assets.d.ts.map +1 -0
  198. package/dts/jobs/browser/config.d.ts +67 -0
  199. package/dts/jobs/browser/config.d.ts.map +1 -0
  200. package/dts/jobs/browser/index.d.ts +10 -0
  201. package/dts/jobs/browser/index.d.ts.map +1 -0
  202. package/dts/jobs/browser/unauthorized.d.ts +9 -0
  203. package/dts/jobs/browser/unauthorized.d.ts.map +1 -0
  204. package/dts/jobs/index.d.ts +24 -0
  205. package/dts/jobs/index.d.ts.map +1 -0
  206. package/dts/jobs/jobsUi.d.ts +63 -0
  207. package/dts/jobs/jobsUi.d.ts.map +1 -0
  208. package/dts/jobs/shell.d.ts +77 -0
  209. package/dts/jobs/shell.d.ts.map +1 -0
  210. package/dts/jobs/types.d.ts +146 -0
  211. package/dts/jobs/types.d.ts.map +1 -0
  212. package/dts/kit/browser/shared.d.ts +10 -0
  213. package/dts/kit/browser/shared.d.ts.map +1 -0
  214. package/dts/kit/browser/unauthorized.d.ts +74 -0
  215. package/dts/kit/browser/unauthorized.d.ts.map +1 -0
  216. package/dts/kit/unauthorized.d.ts +47 -0
  217. package/dts/kit/unauthorized.d.ts.map +1 -0
  218. package/guides/docs.md +170 -0
  219. package/guides/jobs.md +1120 -0
  220. package/lib/docs/assets.ts +427 -0
  221. package/lib/docs/browser/config.ts +66 -0
  222. package/lib/docs/browser/index.ts +24 -0
  223. package/lib/docs/docsUi.ts +623 -0
  224. package/lib/docs/hosted.ts +31 -0
  225. package/lib/docs/index.ts +37 -0
  226. package/lib/docs/shell.ts +156 -0
  227. package/lib/docs/types.ts +149 -0
  228. package/lib/index.ts +27 -0
  229. package/lib/jobs/assets.ts +448 -0
  230. package/lib/jobs/browser/config.ts +65 -0
  231. package/lib/jobs/browser/index.ts +9 -0
  232. package/lib/jobs/browser/unauthorized.ts +11 -0
  233. package/lib/jobs/index.ts +41 -0
  234. package/lib/jobs/jobsUi.ts +709 -0
  235. package/lib/jobs/shell.ts +205 -0
  236. package/lib/jobs/types.ts +162 -0
  237. package/lib/kit/browser/shared.ts +11 -0
  238. package/lib/kit/browser/unauthorized.ts +114 -0
  239. package/lib/kit/unauthorized.ts +222 -0
  240. 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
+ }