zudoku 0.33.2-local.4 → 0.34.0

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 (145) hide show
  1. package/README.md +121 -0
  2. package/dist/config/validators/common.d.ts +346 -346
  3. package/dist/config/validators/validate.d.ts +165 -165
  4. package/dist/lib/components/AnchorLink.d.ts +2 -2
  5. package/dist/lib/components/AnchorLink.js +4 -4
  6. package/dist/lib/components/AnchorLink.js.map +1 -1
  7. package/dist/lib/components/Heading.d.ts +1 -1
  8. package/dist/lib/components/context/ZudokuContext.d.ts +1 -1
  9. package/dist/lib/components/navigation/SidebarItem.js +6 -5
  10. package/dist/lib/components/navigation/SidebarItem.js.map +1 -1
  11. package/dist/lib/core/ZudokuContext.d.ts +4 -0
  12. package/dist/lib/core/ZudokuContext.js.map +1 -1
  13. package/dist/lib/plugins/openapi/OperationList.js +4 -1
  14. package/dist/lib/plugins/openapi/OperationList.js.map +1 -1
  15. package/dist/lib/plugins/openapi/OperationListItem.d.ts +1 -1
  16. package/dist/lib/plugins/openapi/OperationListItem.js +5 -3
  17. package/dist/lib/plugins/openapi/OperationListItem.js.map +1 -1
  18. package/dist/lib/plugins/openapi/graphql/gql.d.ts +1 -1
  19. package/dist/lib/plugins/openapi/graphql/gql.js +1 -1
  20. package/dist/lib/plugins/openapi/graphql/gql.js.map +1 -1
  21. package/dist/lib/plugins/openapi/graphql/graphql.d.ts +1 -0
  22. package/dist/lib/plugins/openapi/graphql/graphql.js +2 -0
  23. package/dist/lib/plugins/openapi/graphql/graphql.js.map +1 -1
  24. package/dist/lib/plugins/openapi/playground/ExamplesDropdown.d.ts +2 -2
  25. package/dist/lib/plugins/openapi/playground/ExamplesDropdown.js +1 -5
  26. package/dist/lib/plugins/openapi/playground/ExamplesDropdown.js.map +1 -1
  27. package/dist/lib/plugins/openapi/playground/Headers.js +1 -1
  28. package/dist/lib/plugins/openapi/playground/Headers.js.map +1 -1
  29. package/dist/lib/plugins/openapi/playground/IdentityDialog.d.ts +11 -0
  30. package/dist/lib/plugins/openapi/playground/IdentityDialog.js +14 -0
  31. package/dist/lib/plugins/openapi/playground/IdentityDialog.js.map +1 -0
  32. package/dist/lib/plugins/openapi/playground/IdentitySelector.d.ts +7 -0
  33. package/dist/lib/plugins/openapi/playground/IdentitySelector.js +10 -0
  34. package/dist/lib/plugins/openapi/playground/IdentitySelector.js.map +1 -0
  35. package/dist/lib/plugins/openapi/playground/Playground.d.ts +9 -1
  36. package/dist/lib/plugins/openapi/playground/Playground.js +75 -24
  37. package/dist/lib/plugins/openapi/playground/Playground.js.map +1 -1
  38. package/dist/lib/plugins/openapi/playground/QueryParams.js +1 -1
  39. package/dist/lib/plugins/openapi/playground/QueryParams.js.map +1 -1
  40. package/dist/lib/plugins/openapi/playground/RequestLoginDialog.d.ts +7 -0
  41. package/dist/lib/plugins/openapi/playground/RequestLoginDialog.js +8 -0
  42. package/dist/lib/plugins/openapi/playground/RequestLoginDialog.js.map +1 -0
  43. package/dist/lib/plugins/openapi/playground/rememberedIdentity.d.ts +17 -0
  44. package/dist/lib/plugins/openapi/playground/rememberedIdentity.js +11 -0
  45. package/dist/lib/plugins/openapi/playground/rememberedIdentity.js.map +1 -0
  46. package/dist/lib/plugins/openapi/playground/result-panel/ResponseTab.js +19 -13
  47. package/dist/lib/plugins/openapi/playground/result-panel/ResponseTab.js.map +1 -1
  48. package/dist/lib/plugins/openapi/playground/result-panel/ResultPanel.d.ts +6 -4
  49. package/dist/lib/plugins/openapi/playground/result-panel/ResultPanel.js +4 -3
  50. package/dist/lib/plugins/openapi/playground/result-panel/ResultPanel.js.map +1 -1
  51. package/dist/lib/ui/Checkbox.d.ts +2 -8
  52. package/dist/lib/ui/Checkbox.js +1 -13
  53. package/dist/lib/ui/Checkbox.js.map +1 -1
  54. package/dist/lib/ui/Command.d.ts +6 -6
  55. package/dist/lib/ui/Select.js +1 -1
  56. package/dist/lib/ui/Select.js.map +1 -1
  57. package/dist/lib/ui/SyntaxHighlight.d.ts +2 -1
  58. package/dist/lib/ui/SyntaxHighlight.js +19 -15
  59. package/dist/lib/ui/SyntaxHighlight.js.map +1 -1
  60. package/dist/lib/util/MdxComponents.d.ts +1 -1
  61. package/dist/lib/util/MdxComponents.js +2 -2
  62. package/dist/lib/util/MdxComponents.js.map +1 -1
  63. package/lib/{AuthenticationPlugin-BCYuduZ9.js → AuthenticationPlugin-4ip08maU.js} +3 -3
  64. package/lib/{AuthenticationPlugin-BCYuduZ9.js.map → AuthenticationPlugin-4ip08maU.js.map} +1 -1
  65. package/lib/Callout-B_sEhkYd.js +211 -0
  66. package/lib/Callout-B_sEhkYd.js.map +1 -0
  67. package/lib/{Dialog-mi6BrnrM.js → Dialog-sbgekbjb.js} +48 -33
  68. package/lib/{Dialog-mi6BrnrM.js.map → Dialog-sbgekbjb.js.map} +1 -1
  69. package/lib/{Markdown-DofXBcqg.js → Markdown-DZXjQjpH.js} +4099 -3848
  70. package/lib/Markdown-DZXjQjpH.js.map +1 -0
  71. package/lib/{MdxPage-KJcNWIgt.js → MdxPage-52vRwa_7.js} +13 -13
  72. package/lib/{MdxPage-KJcNWIgt.js.map → MdxPage-52vRwa_7.js.map} +1 -1
  73. package/lib/{OasProvider-HcqBeC4H.js → OasProvider-CR2nG1Eg.js} +4 -4
  74. package/lib/{OasProvider-HcqBeC4H.js.map → OasProvider-CR2nG1Eg.js.map} +1 -1
  75. package/lib/{OperationList-C3wnbFxp.js → OperationList-DndcCJUG.js} +1097 -1052
  76. package/lib/{OperationList-C3wnbFxp.js.map → OperationList-DndcCJUG.js.map} +1 -1
  77. package/lib/{Select-Co6MuS4j.js → Select-FAYHOYTy.js} +35 -35
  78. package/lib/{Select-Co6MuS4j.js.map → Select-FAYHOYTy.js.map} +1 -1
  79. package/lib/{SlotletProvider-CYFNHuok.js → SlotletProvider-TydSHROc.js} +4 -4
  80. package/lib/{SlotletProvider-CYFNHuok.js.map → SlotletProvider-TydSHROc.js.map} +1 -1
  81. package/lib/{chunk-IR6S3I6Y-CRDBmIgK.js → chunk-HA7DTUK3-ZGg2W6yV.js} +276 -276
  82. package/lib/chunk-HA7DTUK3-ZGg2W6yV.js.map +1 -0
  83. package/lib/{hook-LTe5qHSc.js → hook-CfCFKZ-2.js} +10 -7
  84. package/lib/{hook-LTe5qHSc.js.map → hook-CfCFKZ-2.js.map} +1 -1
  85. package/lib/index-DK7IuUyR.js +2201 -0
  86. package/lib/index-DK7IuUyR.js.map +1 -0
  87. package/lib/index.esm-CltAN0Tf.js +711 -0
  88. package/lib/index.esm-CltAN0Tf.js.map +1 -0
  89. package/lib/objectEntries-BS7aAgOm.js +12 -0
  90. package/lib/objectEntries-BS7aAgOm.js.map +1 -0
  91. package/lib/ui/Checkbox.js +15 -25
  92. package/lib/ui/Checkbox.js.map +1 -1
  93. package/lib/ui/Command.js +1 -1
  94. package/lib/ui/Select.js +1 -1
  95. package/lib/ui/Select.js.map +1 -1
  96. package/lib/ui/SyntaxHighlight.js +483 -502
  97. package/lib/ui/SyntaxHighlight.js.map +1 -1
  98. package/lib/{useExposedProps-D76yras4.js → useExposedProps-BslIn-FE.js} +2 -2
  99. package/lib/{useExposedProps-D76yras4.js.map → useExposedProps-BslIn-FE.js.map} +1 -1
  100. package/lib/zudoku.auth-auth0.js +1 -1
  101. package/lib/zudoku.auth-clerk.js +2 -2
  102. package/lib/zudoku.auth-openid.js +3 -3
  103. package/lib/zudoku.components.js +1390 -32
  104. package/lib/zudoku.components.js.map +1 -1
  105. package/lib/zudoku.hooks.js +1 -1
  106. package/lib/zudoku.plugin-api-catalog.js +5 -5
  107. package/lib/zudoku.plugin-api-keys.js +4 -4
  108. package/lib/zudoku.plugin-custom-pages.js +2 -2
  109. package/lib/zudoku.plugin-markdown.js +1 -1
  110. package/lib/zudoku.plugin-openapi.js +3 -3
  111. package/lib/zudoku.plugin-redirect.js +1 -1
  112. package/lib/zudoku.plugin-search-pagefind.js +84 -154
  113. package/lib/zudoku.plugin-search-pagefind.js.map +1 -1
  114. package/package.json +3 -3
  115. package/src/lib/components/AnchorLink.tsx +7 -7
  116. package/src/lib/components/navigation/SidebarItem.tsx +8 -23
  117. package/src/lib/core/ZudokuContext.ts +4 -0
  118. package/src/lib/plugins/openapi/OperationList.tsx +73 -33
  119. package/src/lib/plugins/openapi/OperationListItem.tsx +105 -92
  120. package/src/lib/plugins/openapi/graphql/gql.ts +3 -3
  121. package/src/lib/plugins/openapi/graphql/graphql.ts +3 -0
  122. package/src/lib/plugins/openapi/playground/ExamplesDropdown.tsx +30 -32
  123. package/src/lib/plugins/openapi/playground/Headers.tsx +0 -1
  124. package/src/lib/plugins/openapi/playground/IdentityDialog.tsx +74 -0
  125. package/src/lib/plugins/openapi/playground/IdentitySelector.tsx +54 -0
  126. package/src/lib/plugins/openapi/playground/Playground.tsx +164 -133
  127. package/src/lib/plugins/openapi/playground/QueryParams.tsx +0 -1
  128. package/src/lib/plugins/openapi/playground/RequestLoginDialog.tsx +51 -0
  129. package/src/lib/plugins/openapi/playground/rememberedIdentity.ts +26 -0
  130. package/src/lib/plugins/openapi/playground/result-panel/ResponseTab.tsx +24 -4
  131. package/src/lib/plugins/openapi/playground/result-panel/ResultPanel.tsx +66 -45
  132. package/src/lib/ui/Checkbox.tsx +8 -24
  133. package/src/lib/ui/Select.tsx +1 -1
  134. package/src/lib/ui/SyntaxHighlight.tsx +94 -96
  135. package/src/lib/util/MdxComponents.tsx +2 -2
  136. package/lib/Command-CrTA1FX0.js +0 -140
  137. package/lib/Command-CrTA1FX0.js.map +0 -1
  138. package/lib/Markdown-DofXBcqg.js.map +0 -1
  139. package/lib/chunk-IR6S3I6Y-CRDBmIgK.js.map +0 -1
  140. package/lib/index-CtkRMvMw.js +0 -2052
  141. package/lib/index-CtkRMvMw.js.map +0 -1
  142. package/lib/index-vn5bsvmU.js +0 -1399
  143. package/lib/index-vn5bsvmU.js.map +0 -1
  144. package/lib/useScrollToAnchor-DKyrbZoy.js +0 -977
  145. package/lib/useScrollToAnchor-DKyrbZoy.js.map +0 -1
@@ -5,9 +5,6 @@ import { FormProvider, useForm } from "react-hook-form";
5
5
  import { Alert, AlertDescription, AlertTitle } from "zudoku/ui/Alert.js";
6
6
  import { PathRenderer } from "../../../components/PathRenderer.js";
7
7
 
8
- import { Button } from "zudoku/ui/Button.js";
9
- import { Label } from "zudoku/ui/Label.js";
10
- import { RadioGroup, RadioGroupItem } from "zudoku/ui/RadioGroup.js";
11
8
  import {
12
9
  Select,
13
10
  SelectContent,
@@ -18,16 +15,21 @@ import {
18
15
  import { Textarea } from "zudoku/ui/Textarea.js";
19
16
  import { useSelectedServer } from "../../../authentication/state.js";
20
17
  import { useApiIdentities } from "../../../components/context/ZudokuContext.js";
21
- import { Card } from "../../../ui/Card.js";
22
18
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../../ui/Tabs.js";
23
19
  import { cn } from "../../../util/cn.js";
20
+ import { objectEntries } from "../../../util/objectEntries.js";
21
+ import { useLatest } from "../../../util/useLatest.js";
24
22
  import { ColorizedParam } from "../ColorizedParam.js";
25
- import { Content } from "../SidecarExamples.js";
23
+ import { type Content } from "../SidecarExamples.js";
26
24
  import { createUrl } from "./createUrl.js";
27
25
  import ExamplesDropdown from "./ExamplesDropdown.js";
28
26
  import { Headers } from "./Headers.js";
27
+ import { IdentityDialog } from "./IdentityDialog.js";
28
+ import IdentitySelector from "./IdentitySelector.js";
29
29
  import { PathParams } from "./PathParams.js";
30
30
  import { QueryParams } from "./QueryParams.js";
31
+ import { useIdentityStore } from "./rememberedIdentity.js";
32
+ import RequestLoginDialog from "./RequestLoginDialog.js";
31
33
  import { ResultPanel } from "./result-panel/ResultPanel.js";
32
34
  import SubmitButton from "./SubmitButton.js";
33
35
 
@@ -56,8 +58,17 @@ export type PathParam = {
56
58
  isRequired?: boolean;
57
59
  };
58
60
 
61
+ const bodyContentTypeMap = {
62
+ Plain: "text/plain",
63
+ JSON: "application/json",
64
+ XML: "application/xml",
65
+ YAML: "application/yaml",
66
+ CSV: "text/csv",
67
+ } as const;
68
+
59
69
  export type PlaygroundForm = {
60
70
  body: string;
71
+ bodyContentType: keyof typeof bodyContentTypeMap;
61
72
  queryParams: Array<{
62
73
  name: string;
63
74
  value: string;
@@ -120,12 +131,20 @@ export const Playground = ({
120
131
  const { selectedServer, setSelectedServer } = useSelectedServer(
121
132
  servers.map((url) => ({ url })),
122
133
  );
134
+ const [showSelectIdentity, setShowSelectIdentity] = useState(false);
135
+ const identities = useApiIdentities();
136
+ const { setRememberedIdentity, getRememberedIdentity } = useIdentityStore();
123
137
  const [, startTransition] = useTransition();
124
138
  const [skipLogin, setSkipLogin] = useState(false);
139
+ const [showLongRunningWarning, setShowLongRunningWarning] = useState(false);
140
+ const abortControllerRef = useRef<AbortController | undefined>(undefined);
141
+ const latestSetRememberedIdentity = useLatest(setRememberedIdentity);
142
+
125
143
  const { register, control, handleSubmit, watch, setValue, ...form } =
126
144
  useForm<PlaygroundForm>({
127
145
  defaultValues: {
128
146
  body: defaultBody,
147
+ bodyContentType: "JSON",
129
148
  queryParams: queryParams
130
149
  .map((param) => ({
131
150
  name: param.name,
@@ -158,36 +177,42 @@ export const Playground = ({
158
177
  active: false,
159
178
  },
160
179
  ]),
161
- identity: NO_IDENTITY,
180
+ identity: getRememberedIdentity(
181
+ identities.data?.map((i) => i.id) ?? [],
182
+ ),
162
183
  },
163
184
  });
164
185
  const formState = watch();
165
- const identities = useApiIdentities();
186
+ const formRef = useRef<HTMLFormElement>(null);
166
187
 
167
- const setOnce = useRef(false);
168
188
  useEffect(() => {
169
- if (setOnce.current) return;
170
- const firstIdentity = identities.data?.at(0);
171
- if (firstIdentity) {
172
- setValue("identity", firstIdentity.id);
173
- setOnce.current = true;
189
+ if (formState.identity) {
190
+ latestSetRememberedIdentity.current(formState.identity);
174
191
  }
175
- }, [setValue, identities.data]);
176
-
177
- const formRef = useRef<HTMLFormElement>(null);
192
+ }, [latestSetRememberedIdentity, formState.identity]);
178
193
 
179
194
  const queryMutation = useMutation({
180
195
  mutationFn: async (data: PlaygroundForm) => {
181
196
  const start = performance.now();
197
+
198
+ const shouldSetContentType = !data.headers.some(
199
+ (h) => h.active && h.name.toLowerCase() === "content-type",
200
+ );
201
+
202
+ const headers = Object.fromEntries([
203
+ ...data.headers
204
+ .filter((h) => h.name && h.active)
205
+ .map((header) => [header.name, header.value]),
206
+ ...(shouldSetContentType
207
+ ? [["content-type", bodyContentTypeMap[data.bodyContentType]]]
208
+ : []),
209
+ ]);
210
+
182
211
  const request = new Request(
183
212
  createUrl(server ?? selectedServer, url, data),
184
213
  {
185
214
  method: method.toUpperCase(),
186
- headers: Object.fromEntries(
187
- data.headers
188
- .filter((h) => h.name && h.active)
189
- .map((header) => [header.name, header.value]),
190
- ),
215
+ headers,
191
216
  body: data.body ? data.body : undefined,
192
217
  },
193
218
  );
@@ -197,15 +222,23 @@ export const Playground = ({
197
222
  ?.find((i) => i.id === data.identity)
198
223
  ?.authorizeRequest(request);
199
224
  }
225
+
226
+ const warningTimeout = setTimeout(
227
+ () => setShowLongRunningWarning(true),
228
+ 3210,
229
+ );
230
+ abortControllerRef.current = new AbortController();
231
+
200
232
  try {
201
233
  const response = await fetch(request, {
202
- signal: AbortSignal.timeout(5000),
234
+ signal: abortControllerRef.current.signal,
203
235
  });
204
236
 
205
- const time = performance.now() - start;
237
+ clearTimeout(warningTimeout);
238
+ setShowLongRunningWarning(false);
206
239
 
240
+ const time = performance.now() - start;
207
241
  const body = await response.text();
208
-
209
242
  const url = new URL(request.url);
210
243
 
211
244
  return {
@@ -226,6 +259,8 @@ export const Playground = ({
226
259
  },
227
260
  } satisfies PlaygroundResult;
228
261
  } catch (error) {
262
+ clearTimeout(warningTimeout);
263
+ setShowLongRunningWarning(false);
229
264
  if (error instanceof TypeError) {
230
265
  throw new Error(
231
266
  "The request failed, possibly due to network issues or CORS policy.",
@@ -237,6 +272,12 @@ export const Playground = ({
237
272
  },
238
273
  });
239
274
 
275
+ useEffect(() => {
276
+ return () => {
277
+ abortControllerRef.current?.abort();
278
+ };
279
+ }, []);
280
+
240
281
  const path = (
241
282
  <PathRenderer
242
283
  path={url}
@@ -270,9 +311,9 @@ export const Playground = ({
270
311
  ));
271
312
 
272
313
  const serverSelect = (
273
- <div className="inline-block opacity-50 hover:opacity-100 transition translate-y-[4px]">
314
+ <div className="inline-block opacity-50 hover:opacity-100 transition">
274
315
  {server ? (
275
- <span>{server.replace(/^https?:\/\//, "")}</span>
316
+ <span>{server.replace(/^https?:\/\//, "").replace(/\/$/, "")}</span>
276
317
  ) : (
277
318
  servers.length > 1 && (
278
319
  <Select
@@ -282,13 +323,13 @@ export const Playground = ({
282
323
  value={selectedServer}
283
324
  defaultValue={selectedServer}
284
325
  >
285
- <SelectTrigger className="p-0 border-none flex-row-reverse bg-transparent text-xs gap-0.5 h-auto">
326
+ <SelectTrigger className="p-0 border-none flex-row-reverse bg-transparent text-xs gap-0.5 h-auto translate-y-[4px]">
286
327
  <SelectValue />
287
328
  </SelectTrigger>
288
329
  <SelectContent>
289
330
  {servers.map((s) => (
290
331
  <SelectItem key={s} value={s}>
291
- {s.replace(/^https?:\/\//, "")}
332
+ {s.replace(/^https?:\/\//, "").replace(/\/$/, "")}
292
333
  </SelectItem>
293
334
  ))}
294
335
  </SelectContent>
@@ -299,62 +340,45 @@ export const Playground = ({
299
340
  );
300
341
 
301
342
  const showLogin = requiresLogin && !skipLogin;
343
+ const isBodySupported = ["POST", "PUT", "PATCH", "DELETE"].includes(
344
+ method.toUpperCase(),
345
+ );
302
346
 
303
347
  return (
304
348
  <FormProvider
305
349
  {...{ register, control, handleSubmit, watch, setValue, ...form }}
306
350
  >
307
351
  <form
308
- onSubmit={handleSubmit((data) => queryMutation.mutateAsync(data))}
352
+ onSubmit={handleSubmit((data) => {
353
+ if (identities.data?.length === 0 || data.identity) {
354
+ queryMutation.mutate(data);
355
+ } else {
356
+ setShowSelectIdentity(true);
357
+ }
358
+ })}
309
359
  ref={formRef}
310
360
  className="relative"
311
361
  >
312
- {showLogin && (
313
- <div className="absolute top-1/2 right-1/2 -translate-y-1/2 translate-x-1/2 z-50 max-w-md">
314
- <Alert>
315
- <AlertTitle className="mb-2">
316
- Welcome to the Playground!
317
- </AlertTitle>
318
- <AlertDescription className="flex flex-col gap-2">
319
- <div className="mb-2">
320
- The Playground is a tool for developers to test and explore
321
- our APIs. To use the Playground, you need to login.
322
- </div>
323
- <div className="flex gap-2 justify-between">
324
- <Button
325
- type="button"
326
- variant="ghost"
327
- onClick={() => setSkipLogin(true)}
328
- >
329
- Skip
330
- </Button>
331
- <div className="flex gap-2">
332
- {onSignUp && (
333
- <Button
334
- type="button"
335
- variant="outline"
336
- onClick={onSignUp}
337
- >
338
- Sign Up
339
- </Button>
340
- )}
341
- {onLogin && (
342
- <Button type="button" variant="default" onClick={onLogin}>
343
- Login
344
- </Button>
345
- )}
346
- </div>
347
- </div>
348
- </AlertDescription>
349
- </Alert>
350
- </div>
351
- )}
352
- <div
353
- className={cn(
354
- "grid grid-cols-2 text-sm h-full",
355
- showLogin && "opacity-30 pointer-events-none",
356
- )}
357
- >
362
+ <IdentityDialog
363
+ identities={identities.data ?? []}
364
+ open={showSelectIdentity}
365
+ onOpenChange={setShowSelectIdentity}
366
+ onSubmit={({ rememberedIdentity, identity }) => {
367
+ if (rememberedIdentity) {
368
+ setValue("identity", identity ?? NO_IDENTITY);
369
+ }
370
+ setShowSelectIdentity(false);
371
+ queryMutation.mutate({ ...formState, identity });
372
+ }}
373
+ />
374
+ <RequestLoginDialog
375
+ open={showLogin}
376
+ setOpen={(open) => setSkipLogin(!open)}
377
+ onSignUp={onSignUp}
378
+ onLogin={onLogin}
379
+ />
380
+
381
+ <div className="grid grid-cols-2 text-sm h-full">
358
382
  <div className="flex flex-col gap-4 p-4 after:bg-muted-foreground/20 relative after:absolute after:w-px after:inset-0 after:left-auto">
359
383
  <div className="flex gap-2 items-stretch">
360
384
  <div className="flex flex-1 items-center w-full border rounded-md">
@@ -372,7 +396,7 @@ export const Playground = ({
372
396
  <SubmitButton
373
397
  identities={identities.data ?? []}
374
398
  formRef={formRef}
375
- disabled={form.formState.isSubmitting}
399
+ disabled={identities.isLoading || form.formState.isSubmitting}
376
400
  />
377
401
  </div>
378
402
  <Tabs defaultValue="parameters">
@@ -397,7 +421,12 @@ export const Playground = ({
397
421
  <div className="w-2 h-2 rounded-full bg-blue-400 ml-2" />
398
422
  )}
399
423
  </TabsTrigger>
400
- <TabsTrigger value="body">Body</TabsTrigger>
424
+ <TabsTrigger value="body">
425
+ Body
426
+ {formState.body && (
427
+ <div className="w-2 h-2 rounded-full bg-blue-400 ml-2" />
428
+ )}
429
+ </TabsTrigger>
401
430
  </TabsList>
402
431
  </div>
403
432
  <TabsContent value="headers">
@@ -431,31 +460,58 @@ export const Playground = ({
431
460
  <Textarea
432
461
  {...register("body")}
433
462
  className={cn(
434
- "border w-full rounded-lg p-2 bg-muted h-40 font-mono",
435
- !["POST", "PUT", "PATCH", "DELETE"].includes(
436
- method.toUpperCase(),
437
- ) && "h-20",
463
+ "border w-full rounded-lg bg-muted/40 p-2 h-64 font-mono text-[13px]",
464
+ !isBodySupported && "h-20 bg-muted",
438
465
  )}
439
466
  placeholder={
440
- !["POST", "PUT", "PATCH", "DELETE"].includes(
441
- method.toUpperCase(),
442
- )
467
+ !isBodySupported
443
468
  ? "This request does not support a body"
444
469
  : undefined
445
470
  }
446
- disabled={
447
- !["POST", "PUT", "PATCH", "DELETE"].includes(
448
- method.toUpperCase(),
449
- )
450
- }
471
+ disabled={!isBodySupported}
451
472
  />
452
- {examples && (
453
- <ExamplesDropdown
454
- examples={examples}
455
- onSelect={(example) =>
456
- setValue("body", JSON.stringify(example.value, null, 2))
457
- }
458
- />
473
+ {isBodySupported && (
474
+ <div className="flex items-center gap-2 mt-2 justify-between">
475
+ <Select
476
+ value={formState.bodyContentType}
477
+ onValueChange={(value) =>
478
+ setValue(
479
+ "bodyContentType",
480
+ value as keyof typeof bodyContentTypeMap,
481
+ )
482
+ }
483
+ >
484
+ <SelectTrigger className="w-[100px]">
485
+ <SelectValue />
486
+ </SelectTrigger>
487
+ <SelectContent>
488
+ {Object.keys(bodyContentTypeMap).map((format) => (
489
+ <SelectItem key={format} value={format}>
490
+ {format}
491
+ </SelectItem>
492
+ ))}
493
+ </SelectContent>
494
+ </Select>
495
+ {examples && examples.length > 0 && (
496
+ <ExamplesDropdown
497
+ examples={examples}
498
+ onSelect={(example, mediaType) => {
499
+ setValue(
500
+ "body",
501
+ JSON.stringify(example.value, null, 2),
502
+ );
503
+
504
+ const format = objectEntries(bodyContentTypeMap).find(
505
+ ([_, contentType]) => contentType === mediaType,
506
+ )?.[0];
507
+
508
+ if (format) {
509
+ setValue("bodyContentType", format);
510
+ }
511
+ }}
512
+ />
513
+ )}
514
+ </div>
459
515
  )}
460
516
  </TabsContent>
461
517
  <TabsContent value="auth">
@@ -470,43 +526,11 @@ export const Playground = ({
470
526
  </Alert>
471
527
  )}
472
528
  <div className="flex flex-col items-center gap-2">
473
- <Card className="w-full overflow-hidden">
474
- <RadioGroup
475
- onValueChange={(value) => setValue("identity", value)}
476
- value={formState.identity}
477
- defaultValue={formState.identity}
478
- className="gap-0"
479
- disabled={identities.data?.length === 0}
480
- >
481
- <Label
482
- className="h-12 border-b items-center flex p-4 cursor-pointer hover:bg-accent"
483
- htmlFor="none"
484
- >
485
- <RadioGroupItem value={NO_IDENTITY} id="none">
486
- None
487
- </RadioGroupItem>
488
- <Label htmlFor="none" className="ml-2">
489
- None
490
- </Label>
491
- </Label>
492
- {identities.data?.map((identity) => (
493
- <Label
494
- key={identity.id}
495
- className="h-12 border-b items-center flex p-4 cursor-pointer hover:bg-accent"
496
- >
497
- <RadioGroupItem
498
- value={identity.id}
499
- id={identity.id}
500
- >
501
- {identity.label}
502
- </RadioGroupItem>
503
- <Label htmlFor={identity.id} className="ml-2">
504
- {identity.label}
505
- </Label>
506
- </Label>
507
- ))}
508
- </RadioGroup>
509
- </Card>
529
+ <IdentitySelector
530
+ value={formState.identity}
531
+ identities={identities.data ?? []}
532
+ setValue={(value) => setValue("identity", value)}
533
+ />
510
534
  </div>
511
535
  </div>
512
536
  </TabsContent>
@@ -517,6 +541,13 @@ export const Playground = ({
517
541
  showPathParamsWarning={formState.pathParams.some(
518
542
  (p) => p.value === "",
519
543
  )}
544
+ showLongRunningWarning={showLongRunningWarning}
545
+ onCancel={() => {
546
+ abortControllerRef.current?.abort(
547
+ "Request cancelled by the user",
548
+ );
549
+ setShowLongRunningWarning(false);
550
+ }}
520
551
  />
521
552
  </div>
522
553
  </form>
@@ -43,7 +43,6 @@ export const QueryParams = ({
43
43
  name={`queryParams.${i}.active`}
44
44
  render={({ field }) => (
45
45
  <Checkbox
46
- variant="outline"
47
46
  id={`queryParams.${i}.active`}
48
47
  className="mr-2"
49
48
  checked={field.value}
@@ -0,0 +1,51 @@
1
+ import { Button } from "zudoku/ui/Button.js";
2
+ import {
3
+ Dialog,
4
+ DialogContent,
5
+ DialogDescription,
6
+ DialogFooter,
7
+ DialogTitle,
8
+ } from "zudoku/ui/Dialog.js";
9
+
10
+ const RequestLoginDialog = ({
11
+ open,
12
+ setOpen,
13
+ onSignUp,
14
+ onLogin,
15
+ }: {
16
+ open: boolean;
17
+ onSignUp?: () => void;
18
+ onLogin?: () => void;
19
+ setOpen: (open: boolean) => void;
20
+ }) => {
21
+ return (
22
+ <Dialog open={open} onOpenChange={setOpen}>
23
+ <DialogContent>
24
+ <DialogTitle>Welcome to the Playground!</DialogTitle>
25
+ <DialogDescription>
26
+ The Playground is a tool for developers to test and explore our APIs.
27
+ To use the Playground, you need to login.
28
+ </DialogDescription>
29
+ <DialogFooter className="flex gap-2 sm:justify-between">
30
+ <Button type="button" variant="ghost" onClick={() => setOpen(false)}>
31
+ Skip
32
+ </Button>
33
+ <div className="flex gap-2">
34
+ {onSignUp && (
35
+ <Button type="button" variant="outline" onClick={onSignUp}>
36
+ Sign Up
37
+ </Button>
38
+ )}
39
+ {onLogin && (
40
+ <Button type="button" variant="default" onClick={onLogin}>
41
+ Login
42
+ </Button>
43
+ )}
44
+ </div>
45
+ </DialogFooter>
46
+ </DialogContent>
47
+ </Dialog>
48
+ );
49
+ };
50
+
51
+ export default RequestLoginDialog;
@@ -0,0 +1,26 @@
1
+ import { create } from "zustand";
2
+ import { createJSONStorage, persist } from "zustand/middleware";
3
+
4
+ interface IdentityState {
5
+ rememberedIdentity: string | null;
6
+ setRememberedIdentity: (identity: string | null) => void;
7
+ getRememberedIdentity: (availableIdentities: string[]) => string | undefined;
8
+ }
9
+
10
+ export const useIdentityStore = create<IdentityState>()(
11
+ persist(
12
+ (set, get) => ({
13
+ rememberedIdentity: null,
14
+ setRememberedIdentity: (identity: string | null) =>
15
+ set({ rememberedIdentity: identity }),
16
+ getRememberedIdentity: (availableIdentities: string[]) =>
17
+ availableIdentities.find(
18
+ (identity) => identity === get().rememberedIdentity,
19
+ ),
20
+ }),
21
+ {
22
+ name: "identity-storage",
23
+ storage: createJSONStorage(() => sessionStorage),
24
+ },
25
+ ),
26
+ );
@@ -1,6 +1,7 @@
1
1
  import { useQuery } from "@tanstack/react-query";
2
2
  import { ChevronRightIcon } from "lucide-react";
3
3
  import { Fragment, useState } from "react";
4
+ import { Callout } from "zudoku/ui/Callout.js";
4
5
  import {
5
6
  Collapsible,
6
7
  CollapsibleContent,
@@ -30,6 +31,14 @@ const statusCodeMap: Record<number, string> = {
30
31
  500: "Internal Server Error",
31
32
  };
32
33
 
34
+ const humanFileSize = (bytes: number) => {
35
+ const exponent = Math.floor(Math.log(bytes) / Math.log(1000.0));
36
+ const decimal = (bytes / Math.pow(1000.0, exponent)).toFixed(
37
+ exponent ? 2 : 0,
38
+ );
39
+ return `${decimal} ${exponent ? `${"kMGTPEZY"[exponent - 1]}B` : "B"}`;
40
+ };
41
+
33
42
  const mimeTypeToLanguage = (mimeType: string) => {
34
43
  const mimeTypeMapping = {
35
44
  "application/json": "json",
@@ -83,6 +92,8 @@ const sortHeadersByRelevance = (
83
92
  });
84
93
  };
85
94
 
95
+ const SYNTAX_HIGHLIGHT_MAX_SIZE_THRESHOLD = 64_000;
96
+
86
97
  export const ResponseTab = ({
87
98
  body = "",
88
99
  headers,
@@ -114,9 +125,10 @@ export const ResponseTab = ({
114
125
  });
115
126
 
116
127
  const sortedHeaders = sortHeadersByRelevance([...headers]);
128
+ const shouldDisableHighlighting = size > SYNTAX_HIGHLIGHT_MAX_SIZE_THRESHOLD;
117
129
 
118
130
  return (
119
- <div className="flex flex-col gap-2 h-full overflow-y-scroll max-h-[calc(100vh-220px)] py-4">
131
+ <div className="flex flex-col gap-2 h-full overflow-auto max-h-[calc(100vh-220px)] ">
120
132
  <Collapsible defaultOpen>
121
133
  <CollapsibleTrigger className="flex items-center gap-2 hover:text-primary group">
122
134
  <ChevronRightIcon className="h-4 w-4 transition-transform duration-200 group-data-[state=open]:rotate-[90deg]" />
@@ -151,6 +163,12 @@ export const ResponseTab = ({
151
163
  </Collapsible>
152
164
 
153
165
  <Card className="shadow-none">
166
+ {shouldDisableHighlighting && (
167
+ <Callout type="info" className="my-0 p-2">
168
+ Code highlight is disabled for responses larger than{" "}
169
+ {humanFileSize(SYNTAX_HIGHLIGHT_MAX_SIZE_THRESHOLD)}
170
+ </Callout>
171
+ )}
154
172
  <SyntaxHighlight
155
173
  language={
156
174
  view === "types"
@@ -161,8 +179,9 @@ export const ResponseTab = ({
161
179
  : detectedLanguage
162
180
  : "json"
163
181
  }
182
+ showCopy="always"
183
+ disabled={shouldDisableHighlighting}
164
184
  noBackground
165
- // playground dialog has h-5/6 ≈ 83.333vh
166
185
  className="overflow-x-auto p-4 text-xs max-h-[calc(83.333vh-180px)]"
167
186
  code={
168
187
  (view === "raw"
@@ -173,7 +192,7 @@ export const ResponseTab = ({
173
192
  }
174
193
  />
175
194
  </Card>
176
- <div className="flex gap-2 justify-between">
195
+ <div className="flex gap-2 justify-between items-center">
177
196
  <div className="flex text-xs gap-2 border bg-muted rounded-md p-2 items-center h-8 font-mono divide-x">
178
197
  <div>
179
198
  <span className="text-muted-foreground">Status</span> {status}{" "}
@@ -184,7 +203,8 @@ export const ResponseTab = ({
184
203
  {time.toFixed(0)}ms
185
204
  </div>
186
205
  <div>
187
- <span className="text-muted-foreground">Size</span> {size}B
206
+ <span className="text-muted-foreground">Size</span>{" "}
207
+ {humanFileSize(size)}
188
208
  </div>
189
209
  </div>
190
210
  {jsonContent && (