@checkstack/api-docs-frontend 0.1.36 → 0.2.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # @checkstack/api-docs-frontend
2
2
 
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 7c97b43: Backfill missing package bumps for the `/rest` mount PR — these packages were
8
+ modified in that change but were not declared in its changeset:
9
+
10
+ - `@checkstack/api-docs-frontend`: schema renderer rewrite (`additionalProperties`,
11
+ `$ref` resolution, `oneOf`/`anyOf`/`allOf`, nullable unions, `format`
12
+ qualifiers) and the new path/query/header/cookie parameters table for GET
13
+ endpoints.
14
+ - `@checkstack/frontend`: Vite dev-server proxy for `/rest/*` so external REST
15
+ clients pointing at the Vite port resolve to the backend.
16
+ - `@checkstack/healthcheck-backend`: router handler now unpacks `input.systemId`
17
+ after `getSystemConfigurations` was refactored from `.input(z.string())` to
18
+ `.input(z.object({ systemId: z.string() }))`.
19
+
20
+ No behavior change beyond what the original PR already shipped.
21
+
22
+ ### Patch Changes
23
+
24
+ - Updated dependencies [9016526]
25
+ - @checkstack/common@0.10.0
26
+ - @checkstack/api-docs-common@0.1.13
27
+ - @checkstack/frontend-api@0.5.1
28
+ - @checkstack/ui@1.8.1
29
+
3
30
  ## 0.1.36
4
31
 
5
32
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/api-docs-frontend",
3
- "version": "0.1.36",
3
+ "version": "0.2.0",
4
4
  "license": "Elastic-2.0",
5
5
  "type": "module",
6
6
  "main": "src/index.tsx",
@@ -13,10 +13,10 @@
13
13
  "lint:code": "eslint . --max-warnings 0"
14
14
  },
15
15
  "dependencies": {
16
- "@checkstack/frontend-api": "0.4.2",
17
- "@checkstack/common": "0.8.0",
18
- "@checkstack/api-docs-common": "0.1.11",
19
- "@checkstack/ui": "1.7.1",
16
+ "@checkstack/frontend-api": "0.5.0",
17
+ "@checkstack/common": "0.9.0",
18
+ "@checkstack/api-docs-common": "0.1.12",
19
+ "@checkstack/ui": "1.8.0",
20
20
  "react": "^18.2.0",
21
21
  "react-router-dom": "^6.22.0",
22
22
  "lucide-react": "^0.344.0"
@@ -25,6 +25,6 @@
25
25
  "typescript": "^5.0.0",
26
26
  "@types/react": "^18.2.0",
27
27
  "@checkstack/tsconfig": "0.0.7",
28
- "@checkstack/scripts": "0.3.0"
28
+ "@checkstack/scripts": "0.3.1"
29
29
  }
30
30
  }
@@ -1,4 +1,4 @@
1
- import { useEffect, useState } from "react";
1
+ import { createContext, useContext, useEffect, useState } from "react";
2
2
  import {
3
3
  Card,
4
4
  CardContent,
@@ -29,6 +29,17 @@ interface OpenApiSpec {
29
29
  description?: string;
30
30
  };
31
31
  paths: Record<string, Record<string, OperationObject>>;
32
+ components?: {
33
+ schemas?: Record<string, SchemaObject>;
34
+ };
35
+ }
36
+
37
+ interface ParameterObject {
38
+ name: string;
39
+ in: "query" | "path" | "header" | "cookie";
40
+ required?: boolean;
41
+ description?: string;
42
+ schema?: SchemaObject;
32
43
  }
33
44
 
34
45
  interface OperationObject {
@@ -36,6 +47,7 @@ interface OperationObject {
36
47
  description?: string;
37
48
  operationId?: string;
38
49
  tags?: string[];
50
+ parameters?: ParameterObject[];
39
51
  requestBody?: {
40
52
  content?: {
41
53
  "application/json"?: {
@@ -57,31 +69,37 @@ interface OperationObject {
57
69
  }
58
70
 
59
71
  interface SchemaObject {
60
- type?: string;
72
+ type?: string | string[];
61
73
  properties?: Record<string, SchemaObject>;
62
74
  items?: SchemaObject;
63
75
  required?: string[];
64
76
  description?: string;
65
- enum?: string[];
77
+ enum?: (string | number | boolean | null)[];
78
+ format?: string;
79
+ nullable?: boolean;
66
80
  $ref?: string;
81
+ additionalProperties?: SchemaObject | boolean;
82
+ oneOf?: SchemaObject[];
83
+ anyOf?: SchemaObject[];
84
+ allOf?: SchemaObject[];
67
85
  }
68
86
 
69
87
  function getUserTypeIcon(userType?: string) {
70
88
  switch (userType) {
71
89
  case "public": {
72
- return <Globe className="h-4 w-4 text-green-500" />;
90
+ return <Globe className="w-4 h-4 text-green-500" />;
73
91
  }
74
92
  case "user": {
75
- return <User className="h-4 w-4 text-blue-500" />;
93
+ return <User className="w-4 h-4 text-blue-500" />;
76
94
  }
77
95
  case "service": {
78
- return <Server className="h-4 w-4 text-purple-500" />;
96
+ return <Server className="w-4 h-4 text-purple-500" />;
79
97
  }
80
98
  case "authenticated": {
81
- return <Lock className="h-4 w-4 text-amber-500" />;
99
+ return <Lock className="w-4 h-4 text-amber-500" />;
82
100
  }
83
101
  default: {
84
- return <Lock className="h-4 w-4 text-gray-500" />;
102
+ return <Lock className="w-4 h-4 text-gray-500" />;
85
103
  }
86
104
  }
87
105
  }
@@ -120,7 +138,7 @@ function CopyButton({ text }: { text: string }) {
120
138
 
121
139
  return (
122
140
  <Button variant="ghost" size="sm" onClick={handleCopy}>
123
- {copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
141
+ {copied ? <Check className="w-4 h-4" /> : <Copy className="w-4 h-4" />}
124
142
  </Button>
125
143
  );
126
144
  }
@@ -131,19 +149,30 @@ function generateFetchExample(
131
149
  operation: OperationObject,
132
150
  ): string {
133
151
  const baseUrl = "http://localhost:3000";
152
+ const upperMethod = method.toUpperCase();
134
153
  const hasBody = operation.requestBody?.content?.["application/json"]?.schema;
135
154
 
136
- let example = `const response = await fetch("${baseUrl}${path}", {
137
- method: "${method.toUpperCase()}",
155
+ const queryParams =
156
+ operation.parameters?.filter((p) => p.in === "query") ?? [];
157
+ const queryString =
158
+ queryParams.length > 0
159
+ ? "?" +
160
+ queryParams
161
+ .map((p) => `${p.name}=<${p.required ? "required" : "optional"}>`)
162
+ .join("&")
163
+ : "";
164
+
165
+ const includeContentType = hasBody;
166
+ let example = `const response = await fetch("${baseUrl}${path}${queryString}", {
167
+ method: "${upperMethod}",
138
168
  headers: {
139
- "Content-Type": "application/json",
140
- "Authorization": "Bearer ck_<application-id>_<secret>"
169
+ ${includeContentType ? ' "Content-Type": "application/json",\n' : ""} "Authorization": "Bearer ck_<application-id>_<secret>"
141
170
  }`;
142
171
 
143
172
  if (hasBody) {
144
173
  example += `,
145
174
  body: JSON.stringify({
146
- // Request body - see schema below
175
+ // Request body - see schema above
147
176
  })`;
148
177
  }
149
178
 
@@ -155,45 +184,118 @@ const data = await response.json();`;
155
184
  return example;
156
185
  }
157
186
 
187
+ const SchemasContext = createContext<Record<string, SchemaObject>>({});
188
+
189
+ const PRIMITIVE_COLORS: Record<string, string> = {
190
+ string: "text-green-600 dark:text-green-400",
191
+ number: "text-amber-600 dark:text-amber-400",
192
+ boolean: "text-red-600 dark:text-red-400",
193
+ integer: "text-amber-600 dark:text-amber-400",
194
+ null: "text-gray-500",
195
+ };
196
+
197
+ function PrimitiveType({ type, format }: { type: string; format?: string }) {
198
+ const className = PRIMITIVE_COLORS[type] ?? "text-gray-600";
199
+ return (
200
+ <span className={className}>
201
+ {type}
202
+ {format ? (
203
+ <span className="text-muted-foreground"> &lt;{format}&gt;</span>
204
+ ) : null}
205
+ </span>
206
+ );
207
+ }
208
+
209
+ const MAX_DEPTH = 12;
210
+
158
211
  function SchemaDisplay({
159
212
  schema,
160
213
  depth = 0,
214
+ refStack = [],
161
215
  }: {
162
216
  schema?: SchemaObject;
163
217
  depth?: number;
218
+ /** Tracks $refs already in the current chain to halt cycles. */
219
+ refStack?: string[];
164
220
  }) {
221
+ const schemas = useContext(SchemasContext);
222
+
165
223
  if (!schema) return <span className="text-muted-foreground">unknown</span>;
224
+ if (depth > MAX_DEPTH) {
225
+ return <span className="text-muted-foreground">…</span>;
226
+ }
166
227
 
228
+ // Resolve $ref via the spec's components.schemas registry, guarding against
229
+ // cycles. If the ref can't be resolved we show its name as a leaf.
167
230
  if (schema.$ref) {
168
- const refName = schema.$ref.split("/").pop();
231
+ const refName = schema.$ref.split("/").pop() ?? schema.$ref;
232
+ if (refStack.includes(schema.$ref)) {
233
+ return (
234
+ <span
235
+ className="text-purple-600 dark:text-purple-400"
236
+ title="recursive reference"
237
+ >
238
+ {refName} ↻
239
+ </span>
240
+ );
241
+ }
242
+ const resolved = schemas[refName];
243
+ if (!resolved) {
244
+ return (
245
+ <span className="text-purple-600 dark:text-purple-400">{refName}</span>
246
+ );
247
+ }
169
248
  return (
170
- <span className="text-purple-600 dark:text-purple-400">{refName}</span>
249
+ <span>
250
+ <span className="mr-1 text-purple-600 dark:text-purple-400">
251
+ {refName}
252
+ </span>
253
+ <SchemaDisplay
254
+ schema={resolved}
255
+ depth={depth}
256
+ refStack={[...refStack, schema.$ref]}
257
+ />
258
+ </span>
171
259
  );
172
260
  }
173
261
 
174
- if (schema.type === "object" && schema.properties) {
262
+ // Union / intersection render variants separated by | or & .
263
+ const variants = schema.oneOf ?? schema.anyOf;
264
+ if (variants && variants.length > 0) {
175
265
  return (
176
- <div className="font-mono text-sm" style={{ marginLeft: depth * 16 }}>
177
- {"{"}
178
- {Object.entries(schema.properties).map(([key, value]) => (
179
- <div key={key} className="ml-4">
180
- <span className="text-blue-600 dark:text-blue-400">{key}</span>
181
- {schema.required?.includes(key) && (
182
- <span className="text-red-500">*</span>
183
- )}
184
- : <SchemaDisplay schema={value} depth={depth + 1} />
185
- </div>
266
+ <span>
267
+ {variants.map((v, i) => (
268
+ <span key={i}>
269
+ {i > 0 && <span className="text-muted-foreground"> | </span>}
270
+ <SchemaDisplay schema={v} depth={depth} refStack={refStack} />
271
+ </span>
272
+ ))}
273
+ </span>
274
+ );
275
+ }
276
+ if (schema.allOf && schema.allOf.length > 0) {
277
+ return (
278
+ <span>
279
+ {schema.allOf.map((v, i) => (
280
+ <span key={i}>
281
+ {i > 0 && <span className="text-muted-foreground"> &amp; </span>}
282
+ <SchemaDisplay schema={v} depth={depth} refStack={refStack} />
283
+ </span>
186
284
  ))}
187
- {"}"}
188
- </div>
285
+ </span>
189
286
  );
190
287
  }
191
288
 
192
- if (schema.type === "array" && schema.items) {
289
+ // Treat type: ["string", "null"] as a nullable union.
290
+ if (Array.isArray(schema.type)) {
193
291
  return (
194
292
  <span>
195
- <SchemaDisplay schema={schema.items} depth={depth} />
196
- []
293
+ {schema.type.map((t, i) => (
294
+ <span key={i}>
295
+ {i > 0 && <span className="text-muted-foreground"> | </span>}
296
+ <PrimitiveType type={t} format={schema.format} />
297
+ </span>
298
+ ))}
197
299
  </span>
198
300
  );
199
301
  }
@@ -201,22 +303,167 @@ function SchemaDisplay({
201
303
  if (schema.enum) {
202
304
  return (
203
305
  <span className="text-green-600 dark:text-green-400">
204
- {schema.enum.map((e) => `"${e}"`).join(" | ")}
306
+ {schema.enum
307
+ .map((e) => (typeof e === "string" ? `"${e}"` : String(e)))
308
+ .join(" | ")}
205
309
  </span>
206
310
  );
207
311
  }
208
312
 
209
- const typeColors: Record<string, string> = {
210
- string: "text-green-600 dark:text-green-400",
211
- number: "text-amber-600 dark:text-amber-400",
212
- boolean: "text-red-600 dark:text-red-400",
213
- integer: "text-amber-600 dark:text-amber-400",
313
+ if (
314
+ schema.type === "object" ||
315
+ schema.properties ||
316
+ schema.additionalProperties !== undefined
317
+ ) {
318
+ const props = schema.properties;
319
+ const ap = schema.additionalProperties;
320
+
321
+ // zod `z.record(K, V)` → no `properties`, just `additionalProperties: V`.
322
+ // Render as `{ [key]: V }` so the value type is visible.
323
+ if (!props && ap !== undefined && ap !== false) {
324
+ return (
325
+ <div
326
+ className="inline-block font-mono text-sm align-top"
327
+ style={{ marginLeft: depth * 16 }}
328
+ >
329
+ {"{ "}
330
+ <span className="text-muted-foreground">[key]</span>:{" "}
331
+ {ap === true ? (
332
+ <span className="text-gray-600">any</span>
333
+ ) : (
334
+ <SchemaDisplay schema={ap} depth={depth + 1} refStack={refStack} />
335
+ )}
336
+ {" }"}
337
+ </div>
338
+ );
339
+ }
340
+
341
+ if (props) {
342
+ return (
343
+ <div
344
+ className="inline-block font-mono text-sm align-top"
345
+ style={{ marginLeft: depth * 16 }}
346
+ >
347
+ {"{"}
348
+ {Object.entries(props).map(([key, value]) => (
349
+ <div key={key} className="ml-4">
350
+ <span className="text-blue-600 dark:text-blue-400">{key}</span>
351
+ {schema.required?.includes(key) && (
352
+ <span className="text-red-500">*</span>
353
+ )}
354
+ :{" "}
355
+ <SchemaDisplay
356
+ schema={value}
357
+ depth={depth + 1}
358
+ refStack={refStack}
359
+ />
360
+ </div>
361
+ ))}
362
+ {ap !== undefined && ap !== false && (
363
+ <div className="ml-4">
364
+ <span className="text-muted-foreground">[key]</span>:{" "}
365
+ {ap === true ? (
366
+ <span className="text-gray-600">any</span>
367
+ ) : (
368
+ <SchemaDisplay
369
+ schema={ap}
370
+ depth={depth + 1}
371
+ refStack={refStack}
372
+ />
373
+ )}
374
+ </div>
375
+ )}
376
+ {"}"}
377
+ </div>
378
+ );
379
+ }
380
+
381
+ return <PrimitiveType type="object" />;
382
+ }
383
+
384
+ if (schema.type === "array" && schema.items) {
385
+ return (
386
+ <span>
387
+ <SchemaDisplay
388
+ schema={schema.items}
389
+ depth={depth}
390
+ refStack={refStack}
391
+ />
392
+ []
393
+ </span>
394
+ );
395
+ }
396
+
397
+ if (typeof schema.type === "string") {
398
+ return <PrimitiveType type={schema.type} format={schema.format} />;
399
+ }
400
+
401
+ return <span className="text-gray-600">unknown</span>;
402
+ }
403
+
404
+ function ParametersTable({
405
+ parameters,
406
+ }: {
407
+ parameters: ParameterObject[];
408
+ }) {
409
+ const byLocation: Record<string, ParameterObject[]> = {};
410
+ for (const p of parameters) {
411
+ (byLocation[p.in] ??= []).push(p);
412
+ }
413
+
414
+ const sectionTitle: Record<string, string> = {
415
+ query: "Query Parameters",
416
+ path: "Path Parameters",
417
+ header: "Header Parameters",
418
+ cookie: "Cookie Parameters",
214
419
  };
215
420
 
216
421
  return (
217
- <span className={typeColors[schema.type ?? ""] ?? "text-gray-600"}>
218
- {schema.type ?? "unknown"}
219
- </span>
422
+ <div className="space-y-3">
423
+ {(["path", "query", "header", "cookie"] as const).map((loc) => {
424
+ const items = byLocation[loc];
425
+ if (!items || items.length === 0) return null;
426
+ return (
427
+ <div key={loc}>
428
+ <h4 className="mb-2 text-sm font-medium">{sectionTitle[loc]}</h4>
429
+ <div className="overflow-x-auto rounded-md bg-muted">
430
+ <table className="w-full text-sm">
431
+ <thead>
432
+ <tr className="text-left border-b border-border/50">
433
+ <th className="px-3 py-2 font-medium">Name</th>
434
+ <th className="px-3 py-2 font-medium">Type</th>
435
+ <th className="px-3 py-2 font-medium">Description</th>
436
+ </tr>
437
+ </thead>
438
+ <tbody>
439
+ {items.map((p) => (
440
+ <tr
441
+ key={`${p.in}:${p.name}`}
442
+ className="align-top border-t border-border/30"
443
+ >
444
+ <td className="px-3 py-2 font-mono">
445
+ <span className="text-blue-600 dark:text-blue-400">
446
+ {p.name}
447
+ </span>
448
+ {p.required && (
449
+ <span className="text-red-500">*</span>
450
+ )}
451
+ </td>
452
+ <td className="px-3 py-2 font-mono">
453
+ <SchemaDisplay schema={p.schema} />
454
+ </td>
455
+ <td className="px-3 py-2 text-muted-foreground">
456
+ {p.description ?? ""}
457
+ </td>
458
+ </tr>
459
+ ))}
460
+ </tbody>
461
+ </table>
462
+ </div>
463
+ </div>
464
+ );
465
+ })}
466
+ </div>
220
467
  );
221
468
  }
222
469
 
@@ -248,21 +495,21 @@ function EndpointCard({
248
495
  return (
249
496
  <Card className="mb-2">
250
497
  <CardHeader
251
- className="cursor-pointer hover:bg-accent/50 transition-colors py-3"
498
+ className="py-3 transition-colors cursor-pointer hover:bg-accent/50"
252
499
  onClick={() => setIsOpen(!isOpen)}
253
500
  >
254
501
  <div className="flex items-center gap-3">
255
502
  {isOpen ? (
256
- <ChevronDown className="h-4 w-4" />
503
+ <ChevronDown className="w-4 h-4" />
257
504
  ) : (
258
- <ChevronRight className="h-4 w-4" />
505
+ <ChevronRight className="w-4 h-4" />
259
506
  )}
260
507
  <Badge
261
508
  className={`${methodColors[method]} text-white uppercase text-xs font-mono`}
262
509
  >
263
510
  {method}
264
511
  </Badge>
265
- <code className="font-mono text-sm flex-1 text-left">{path}</code>
512
+ <code className="flex-1 font-mono text-sm text-left">{path}</code>
266
513
  <div className="flex items-center gap-2">
267
514
  {getUserTypeIcon(meta?.userType)}
268
515
  <Badge
@@ -295,7 +542,7 @@ function EndpointCard({
295
542
 
296
543
  {meta?.accessRules && meta.accessRules.length > 0 && (
297
544
  <div>
298
- <h4 className="text-sm font-medium mb-2">
545
+ <h4 className="mb-2 text-sm font-medium">
299
546
  Required Access Rules
300
547
  </h4>
301
548
  <div className="flex flex-wrap gap-2">
@@ -308,11 +555,19 @@ function EndpointCard({
308
555
  </div>
309
556
  )}
310
557
 
558
+ {operation.parameters && operation.parameters.length > 0 && (
559
+ <ParametersTable parameters={operation.parameters} />
560
+ )}
561
+
311
562
  <div className="grid gap-4 md:grid-cols-2">
312
563
  {inputSchema && (
313
564
  <div>
314
- <h4 className="text-sm font-medium mb-2">Input Schema</h4>
315
- <div className="bg-muted rounded-md p-3 overflow-x-auto">
565
+ <h4 className="mb-2 text-sm font-medium">
566
+ {method.toLowerCase() === "get"
567
+ ? "Input Schema (encoded as query params)"
568
+ : "Input Schema"}
569
+ </h4>
570
+ <div className="p-3 overflow-x-auto rounded-md bg-muted">
316
571
  <SchemaDisplay schema={inputSchema} />
317
572
  </div>
318
573
  </div>
@@ -320,8 +575,8 @@ function EndpointCard({
320
575
 
321
576
  {outputSchema && (
322
577
  <div>
323
- <h4 className="text-sm font-medium mb-2">Output Schema</h4>
324
- <div className="bg-muted rounded-md p-3 overflow-x-auto">
578
+ <h4 className="mb-2 text-sm font-medium">Output Schema</h4>
579
+ <div className="p-3 overflow-x-auto rounded-md bg-muted">
325
580
  <SchemaDisplay schema={outputSchema} />
326
581
  </div>
327
582
  </div>
@@ -335,7 +590,7 @@ function EndpointCard({
335
590
  text={generateFetchExample(path, method, operation)}
336
591
  />
337
592
  </div>
338
- <pre className="bg-muted rounded-md p-3 overflow-x-auto text-sm">
593
+ <pre className="p-3 overflow-x-auto text-sm rounded-md bg-muted">
339
594
  <code>{generateFetchExample(path, method, operation)}</code>
340
595
  </pre>
341
596
  </div>
@@ -419,9 +674,10 @@ export function ApiDocsPage() {
419
674
  continue;
420
675
  }
421
676
 
422
- // Extract plugin name from path (e.g., /catalog/getEntities -> catalog)
423
- // Path can be /api/plugin/... or /plugin/... depending on OpenAPI prefix setting
424
- const pluginMatch = path.match(/^\/?(?:api\/)?([^/]+)/);
677
+ // Extract plugin name from path (e.g. /rest/catalog/getEntities -> catalog).
678
+ // Paths in the generated spec are prefixed with /rest (the REST mount); a
679
+ // bare /plugin/... fallback is kept in case the prefix is ever stripped.
680
+ const pluginMatch = path.match(/^\/?(?:rest\/)?([^/]+)/);
425
681
  const pluginName = pluginMatch?.[1] ?? "other";
426
682
 
427
683
  if (!endpointsByPlugin[pluginName]) {
@@ -432,91 +688,95 @@ export function ApiDocsPage() {
432
688
  }
433
689
 
434
690
  return (
435
- <PageLayout
436
- title={spec.info.title}
437
- subtitle={spec.info.description}
438
- icon={FileCode}
439
- loading={loading}
440
- maxWidth="full"
441
- >
442
- <Badge variant="secondary">v{spec.info.version}</Badge>
443
-
444
- <div className="flex flex-wrap items-center gap-2">
445
- <span className="text-sm text-muted-foreground">Filter by access:</span>
446
- <Button
447
- variant={selectedTypes.size === 0 ? "primary" : "outline"}
448
- size="sm"
449
- onClick={showAll}
450
- >
451
- All
452
- </Button>
453
- <Button
454
- variant={selectedTypes.has("authenticated") ? "primary" : "outline"}
455
- size="sm"
456
- onClick={() => toggleType("authenticated")}
457
- >
458
- Authenticated
459
- </Button>
460
- <Button
461
- variant={selectedTypes.has("public") ? "primary" : "outline"}
462
- size="sm"
463
- onClick={() => toggleType("public")}
464
- >
465
- Public
466
- </Button>
467
- <Button
468
- variant={selectedTypes.has("user") ? "primary" : "outline"}
469
- size="sm"
470
- onClick={() => toggleType("user")}
471
- >
472
- User Only
473
- </Button>
474
- <Button
475
- variant={selectedTypes.has("service") ? "primary" : "outline"}
476
- size="sm"
477
- onClick={() => toggleType("service")}
478
- >
479
- Service Only
480
- </Button>
481
- </div>
482
-
483
- <Card>
484
- <CardHeader>
485
- <CardTitle>Authentication</CardTitle>
486
- <CardDescription>
487
- Endpoints marked as <strong>authenticated</strong> or{" "}
488
- <strong>public</strong> can be accessed using an application token.
489
- Other endpoints are for internal use only.
490
- </CardDescription>
491
- </CardHeader>
492
- <CardContent>
493
- <pre className="bg-muted rounded-md p-3 overflow-x-auto text-sm">
494
- <code>
495
- Authorization: Bearer ck_{"<application-id>"}_{"<secret>"}
496
- </code>
497
- </pre>
498
- </CardContent>
499
- </Card>
500
-
501
- <div className="space-y-8">
502
- {Object.entries(endpointsByPlugin)
503
- .toSorted(([a], [b]) => a.localeCompare(b))
504
- .map(([pluginName, endpoints]) => (
505
- <div key={pluginName}>
506
- <h2 className="text-xl font-semibold mb-4 capitalize">
507
- {pluginName}
508
- </h2>
509
- {endpoints.map(({ path, method, operation }) => (
510
- <EndpointCard
511
- key={`${method}-${path}`}
512
- path={path}
513
- method={method}
514
- operation={operation}
515
- />
516
- ))}
517
- </div>
518
- ))}
519
- </div>
520
- </PageLayout>
691
+ <SchemasContext.Provider value={spec.components?.schemas ?? {}}>
692
+ <PageLayout
693
+ title={spec.info.title}
694
+ subtitle={spec.info.description}
695
+ icon={FileCode}
696
+ loading={loading}
697
+ maxWidth="full"
698
+ >
699
+ <Badge variant="secondary">v{spec.info.version}</Badge>
700
+
701
+ <div className="flex flex-wrap items-center gap-2">
702
+ <span className="text-sm text-muted-foreground">
703
+ Filter by access:
704
+ </span>
705
+ <Button
706
+ variant={selectedTypes.size === 0 ? "primary" : "outline"}
707
+ size="sm"
708
+ onClick={showAll}
709
+ >
710
+ All
711
+ </Button>
712
+ <Button
713
+ variant={selectedTypes.has("authenticated") ? "primary" : "outline"}
714
+ size="sm"
715
+ onClick={() => toggleType("authenticated")}
716
+ >
717
+ Authenticated
718
+ </Button>
719
+ <Button
720
+ variant={selectedTypes.has("public") ? "primary" : "outline"}
721
+ size="sm"
722
+ onClick={() => toggleType("public")}
723
+ >
724
+ Public
725
+ </Button>
726
+ <Button
727
+ variant={selectedTypes.has("user") ? "primary" : "outline"}
728
+ size="sm"
729
+ onClick={() => toggleType("user")}
730
+ >
731
+ User Only
732
+ </Button>
733
+ <Button
734
+ variant={selectedTypes.has("service") ? "primary" : "outline"}
735
+ size="sm"
736
+ onClick={() => toggleType("service")}
737
+ >
738
+ Service Only
739
+ </Button>
740
+ </div>
741
+
742
+ <Card>
743
+ <CardHeader>
744
+ <CardTitle>Authentication</CardTitle>
745
+ <CardDescription>
746
+ Endpoints marked as <strong>authenticated</strong> or{" "}
747
+ <strong>public</strong> can be accessed using an application
748
+ token. Other endpoints are for internal use only.
749
+ </CardDescription>
750
+ </CardHeader>
751
+ <CardContent>
752
+ <pre className="p-3 overflow-x-auto text-sm rounded-md bg-muted">
753
+ <code>
754
+ Authorization: Bearer ck_{"<application-id>"}_{"<secret>"}
755
+ </code>
756
+ </pre>
757
+ </CardContent>
758
+ </Card>
759
+
760
+ <div className="space-y-8">
761
+ {Object.entries(endpointsByPlugin)
762
+ .toSorted(([a], [b]) => a.localeCompare(b))
763
+ .map(([pluginName, endpoints]) => (
764
+ <div key={pluginName}>
765
+ <h2 className="mb-4 text-xl font-semibold capitalize">
766
+ {pluginName}
767
+ </h2>
768
+ {endpoints.map(({ path, method, operation }) => (
769
+ <EndpointCard
770
+ key={`${method}-${path}`}
771
+ path={path}
772
+ method={method}
773
+ operation={operation}
774
+ />
775
+ ))}
776
+ </div>
777
+ ))}
778
+ </div>
779
+ </PageLayout>
780
+ </SchemasContext.Provider>
521
781
  );
522
782
  }