@checkstack/api-docs-frontend 0.1.35 → 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 +70 -0
- package/package.json +7 -7
- package/src/ApiDocsPage.tsx +400 -140
- package/src/index.tsx +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,75 @@
|
|
|
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
|
+
|
|
30
|
+
## 0.1.36
|
|
31
|
+
|
|
32
|
+
### Patch Changes
|
|
33
|
+
|
|
34
|
+
- 950d6ec: Fix mobile UserMenu items rendering at zero height, group menu items by
|
|
35
|
+
section, and unstack cramped card headers on small viewports.
|
|
36
|
+
|
|
37
|
+
- **UserMenu mobile bug**: On mobile, the user-menu Sheet rendered every
|
|
38
|
+
menu item as a grid row, which combined with `flex-shrink: 1` on each
|
|
39
|
+
item collapsed the buttons whose internal layout uses `display: flex`
|
|
40
|
+
(the items registered with `useNavigate` rather than `<Link>`) to zero
|
|
41
|
+
content height. Switched the mobile container to a flex column with
|
|
42
|
+
`[&>*]:shrink-0` and added `min-h-0` so the sheet scrolls correctly
|
|
43
|
+
when the list overflows.
|
|
44
|
+
|
|
45
|
+
- **UserMenu grouping**: Slot extensions now accept an optional `group`
|
|
46
|
+
field. The user menu buckets `UserMenuItemsSlot` extensions by `group`
|
|
47
|
+
and renders each group under a labeled header (`Workspace`,
|
|
48
|
+
`Reliability`, `Configuration`, `Documentation`, `Account`). Existing
|
|
49
|
+
core plugins are tagged with the appropriate group; third-party plugins
|
|
50
|
+
can pick any of these or supply their own label. Untagged extensions
|
|
51
|
+
render last with no header. `UserMenuItemsBottomSlot` is unaffected.
|
|
52
|
+
|
|
53
|
+
- **Card header responsiveness**: `CardHeaderRow` (the primitive shared by
|
|
54
|
+
Incident, Maintenance, Auth, Catalog, GitOps and other config cards) now
|
|
55
|
+
stacks vertically on narrow viewports and only switches to a single row
|
|
56
|
+
at the `sm` breakpoint, so titles and adjacent filter controls (e.g.
|
|
57
|
+
status `Select`, "Show resolved" checkbox) no longer cram together on
|
|
58
|
+
mobile. Refactored the Incident and Maintenance config pages to use the
|
|
59
|
+
primitive instead of a hand-rolled `flex items-center justify-between`
|
|
60
|
+
row, and made their `Select` triggers full-width on mobile.
|
|
61
|
+
|
|
62
|
+
- Updated dependencies [42abfff]
|
|
63
|
+
- Updated dependencies [3547670]
|
|
64
|
+
- Updated dependencies [1ef2e79]
|
|
65
|
+
- Updated dependencies [aa89bc5]
|
|
66
|
+
- Updated dependencies [950d6ec]
|
|
67
|
+
- Updated dependencies [3547670]
|
|
68
|
+
- @checkstack/common@0.9.0
|
|
69
|
+
- @checkstack/ui@1.8.0
|
|
70
|
+
- @checkstack/frontend-api@0.5.0
|
|
71
|
+
- @checkstack/api-docs-common@0.1.12
|
|
72
|
+
|
|
3
73
|
## 0.1.35
|
|
4
74
|
|
|
5
75
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/api-docs-frontend",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
17
|
-
"@checkstack/common": "0.
|
|
18
|
-
"@checkstack/api-docs-common": "0.1.
|
|
19
|
-
"@checkstack/ui": "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"
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"typescript": "^5.0.0",
|
|
26
26
|
"@types/react": "^18.2.0",
|
|
27
|
-
"@checkstack/tsconfig": "0.0.
|
|
28
|
-
"@checkstack/scripts": "0.1
|
|
27
|
+
"@checkstack/tsconfig": "0.0.7",
|
|
28
|
+
"@checkstack/scripts": "0.3.1"
|
|
29
29
|
}
|
|
30
30
|
}
|
package/src/ApiDocsPage.tsx
CHANGED
|
@@ -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="
|
|
90
|
+
return <Globe className="w-4 h-4 text-green-500" />;
|
|
73
91
|
}
|
|
74
92
|
case "user": {
|
|
75
|
-
return <User className="
|
|
93
|
+
return <User className="w-4 h-4 text-blue-500" />;
|
|
76
94
|
}
|
|
77
95
|
case "service": {
|
|
78
|
-
return <Server className="
|
|
96
|
+
return <Server className="w-4 h-4 text-purple-500" />;
|
|
79
97
|
}
|
|
80
98
|
case "authenticated": {
|
|
81
|
-
return <Lock className="
|
|
99
|
+
return <Lock className="w-4 h-4 text-amber-500" />;
|
|
82
100
|
}
|
|
83
101
|
default: {
|
|
84
|
-
return <Lock className="
|
|
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="
|
|
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
|
-
|
|
137
|
-
|
|
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
|
|
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"> <{format}></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
|
|
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
|
-
|
|
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
|
-
<
|
|
177
|
-
{
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
<
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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"> & </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
|
-
|
|
289
|
+
// Treat type: ["string", "null"] as a nullable union.
|
|
290
|
+
if (Array.isArray(schema.type)) {
|
|
193
291
|
return (
|
|
194
292
|
<span>
|
|
195
|
-
|
|
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
|
|
306
|
+
{schema.enum
|
|
307
|
+
.map((e) => (typeof e === "string" ? `"${e}"` : String(e)))
|
|
308
|
+
.join(" | ")}
|
|
205
309
|
</span>
|
|
206
310
|
);
|
|
207
311
|
}
|
|
208
312
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
<
|
|
218
|
-
{
|
|
219
|
-
|
|
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
|
|
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="
|
|
503
|
+
<ChevronDown className="w-4 h-4" />
|
|
257
504
|
) : (
|
|
258
|
-
<ChevronRight className="
|
|
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
|
|
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
|
|
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
|
|
315
|
-
|
|
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
|
|
324
|
-
<div className="
|
|
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="
|
|
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
|
|
423
|
-
//
|
|
424
|
-
|
|
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
|
-
<
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
<
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
<
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
}
|