@featurevisor/catalog 0.0.1 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/assets/index-BVAfG1yl.js +25 -0
- package/dist/assets/index-BrUFr8px.css +2 -0
- package/dist/favicon.png +0 -0
- package/dist/index.html +14 -0
- package/dist/logo-text.png +0 -0
- package/lib/entityTypes.d.ts +15 -0
- package/lib/entityTypes.js +89 -0
- package/lib/entityTypes.js.map +1 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +18 -0
- package/lib/index.js.map +1 -0
- package/lib/node/index.d.ts +74 -0
- package/lib/node/index.js +1352 -0
- package/lib/node/index.js.map +1 -0
- package/lib/types.d.ts +94 -0
- package/lib/types.js +3 -0
- package/lib/types.js.map +1 -0
- package/package.json +66 -13
- package/public/favicon.png +0 -0
- package/public/logo-text.png +0 -0
- package/src/App.tsx +60 -0
- package/src/api.ts +74 -0
- package/src/components/trees.tsx +202 -0
- package/src/components/ui.tsx +660 -0
- package/src/components/variables.tsx +700 -0
- package/src/components/variations.tsx +450 -0
- package/src/context/CatalogContext.tsx +30 -0
- package/src/entityTypes.ts +95 -0
- package/src/index.ts +1 -0
- package/src/main.tsx +26 -0
- package/src/node/index.spec.ts +236 -0
- package/src/node/index.ts +1888 -0
- package/src/pages/EntityDetailPage.tsx +1004 -0
- package/src/pages/HistoryPage.tsx +144 -0
- package/src/pages/HomePage.tsx +21 -0
- package/src/pages/ListPage.tsx +743 -0
- package/src/styles.css +59 -0
- package/src/types.ts +111 -0
- package/src/vite-env.d.ts +1 -0
- package/index.js +0 -1
|
@@ -0,0 +1,700 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Link } from "react-router-dom";
|
|
3
|
+
|
|
4
|
+
import { getEntityRoute } from "../entityTypes";
|
|
5
|
+
import { Badge, EntityKey, OverviewChip, OverviewMetaPanel, OverviewMetaRow } from "./ui";
|
|
6
|
+
|
|
7
|
+
export type SchemaLike = Record<string, unknown>;
|
|
8
|
+
|
|
9
|
+
type FlatSchemaRow = {
|
|
10
|
+
path: string;
|
|
11
|
+
typeLabel: string;
|
|
12
|
+
isRef: boolean;
|
|
13
|
+
required: boolean;
|
|
14
|
+
description?: string;
|
|
15
|
+
constraints: string[];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function slugifyFragment(value: string) {
|
|
19
|
+
return value
|
|
20
|
+
.toLowerCase()
|
|
21
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
22
|
+
.replace(/^-+|-+$/g, "");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isSchemaRef(schema: SchemaLike): schema is SchemaLike & { schema: string } {
|
|
26
|
+
return typeof schema.schema === "string";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function hasSchemaStructure(schema: SchemaLike) {
|
|
30
|
+
const schemaRef = isSchemaRef(schema);
|
|
31
|
+
const type = typeof schema.type === "string" ? schema.type : undefined;
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
!schemaRef &&
|
|
35
|
+
(type === "object" ||
|
|
36
|
+
type === "array" ||
|
|
37
|
+
(Array.isArray(schema.oneOf) && schema.oneOf.length > 0))
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function usesSchemaStructureTable(schema: SchemaLike) {
|
|
42
|
+
const type = typeof schema.type === "string" ? schema.type : undefined;
|
|
43
|
+
|
|
44
|
+
return type === "object" || type === "array";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function hasSchemaTableRows(schema: SchemaLike) {
|
|
48
|
+
return flattenSchemaRows(schema, "").length > 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getSchemaDescription(schema: SchemaLike) {
|
|
52
|
+
const description = schema.description;
|
|
53
|
+
|
|
54
|
+
return typeof description === "string" && description.trim().length > 0
|
|
55
|
+
? description.trim()
|
|
56
|
+
: undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function formatConstraintValue(value: unknown) {
|
|
60
|
+
if (typeof value === "string") {
|
|
61
|
+
return JSON.stringify(value);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
65
|
+
return String(value);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return JSON.stringify(value);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getSchemaConstraintLines(schema: SchemaLike) {
|
|
72
|
+
const lines: string[] = [];
|
|
73
|
+
|
|
74
|
+
if (Array.isArray(schema.enum) && schema.enum.length > 0) {
|
|
75
|
+
lines.push(`enum: ${schema.enum.map(formatConstraintValue).join(", ")}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if ("const" in schema && schema.const !== undefined) {
|
|
79
|
+
lines.push(`const: ${formatConstraintValue(schema.const)}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (typeof schema.minimum === "number") {
|
|
83
|
+
lines.push(`minimum: ${schema.minimum}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (typeof schema.maximum === "number") {
|
|
87
|
+
lines.push(`maximum: ${schema.maximum}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (typeof schema.minLength === "number") {
|
|
91
|
+
lines.push(`minLength: ${schema.minLength}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (typeof schema.maxLength === "number") {
|
|
95
|
+
lines.push(`maxLength: ${schema.maxLength}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (typeof schema.pattern === "string") {
|
|
99
|
+
lines.push(`pattern: ${schema.pattern}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (typeof schema.minItems === "number") {
|
|
103
|
+
lines.push(`minItems: ${schema.minItems}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (typeof schema.maxItems === "number") {
|
|
107
|
+
lines.push(`maxItems: ${schema.maxItems}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (schema.uniqueItems === true) {
|
|
111
|
+
lines.push("uniqueItems: true");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return lines;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function getArrayElementConstraintLines(schema: SchemaLike, items: SchemaLike) {
|
|
118
|
+
return [...getSchemaConstraintLines(schema), ...getSchemaConstraintLines(items)];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function createSchemaRow(
|
|
122
|
+
schema: SchemaLike,
|
|
123
|
+
path: string,
|
|
124
|
+
required: boolean,
|
|
125
|
+
typeLabel?: string,
|
|
126
|
+
): FlatSchemaRow {
|
|
127
|
+
const schemaRef = isSchemaRef(schema);
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
path,
|
|
131
|
+
typeLabel: typeLabel ?? (schemaRef ? schema.schema : getLeafTypeLabel(schema)),
|
|
132
|
+
isRef: schemaRef,
|
|
133
|
+
required,
|
|
134
|
+
description: getSchemaDescription(schema),
|
|
135
|
+
constraints: getSchemaConstraintLines(schema),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function appendAdditionalPropertyRows(schema: SchemaLike, prefix: string, rows: FlatSchemaRow[]) {
|
|
140
|
+
const additionalProperties = schema.additionalProperties;
|
|
141
|
+
|
|
142
|
+
if (additionalProperties === false) {
|
|
143
|
+
rows.push({
|
|
144
|
+
path: prefix ? `${prefix}.*` : "*",
|
|
145
|
+
typeLabel: "—",
|
|
146
|
+
isRef: false,
|
|
147
|
+
required: false,
|
|
148
|
+
description: undefined,
|
|
149
|
+
constraints: ["additionalProperties: false"],
|
|
150
|
+
});
|
|
151
|
+
return rows;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!additionalProperties || typeof additionalProperties !== "object") {
|
|
155
|
+
return rows;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const propertyPath = prefix ? `${prefix}.*` : "*";
|
|
159
|
+
const propertySchema = additionalProperties as SchemaLike;
|
|
160
|
+
|
|
161
|
+
if (
|
|
162
|
+
propertySchema.type === "object" &&
|
|
163
|
+
propertySchema.properties &&
|
|
164
|
+
typeof propertySchema.properties === "object"
|
|
165
|
+
) {
|
|
166
|
+
rows.push(...flattenSchemaRows(propertySchema, propertyPath, false));
|
|
167
|
+
return rows;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
rows.push(createSchemaRow(propertySchema, propertyPath, false));
|
|
171
|
+
return rows;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function TypeBadge(props: { children: React.ReactNode }) {
|
|
175
|
+
return <Badge tone="primary">{props.children}</Badge>;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function SchemaRefLink(props: { name: string; setKey?: string }) {
|
|
179
|
+
return (
|
|
180
|
+
<Link
|
|
181
|
+
to={getEntityRoute("schema", props.name, props.setKey)}
|
|
182
|
+
className="inline-flex hover:opacity-80"
|
|
183
|
+
>
|
|
184
|
+
<Badge tone="primary">{props.name}</Badge>
|
|
185
|
+
</Link>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function VariablePermalink(props: { targetId: string; label?: string }) {
|
|
190
|
+
return (
|
|
191
|
+
<a
|
|
192
|
+
href={`#${props.targetId}`}
|
|
193
|
+
aria-label={props.label || "Link to this variable"}
|
|
194
|
+
className="inline-flex rounded p-1 text-muted opacity-0 transition-opacity hover:text-primary focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary group-hover:opacity-100"
|
|
195
|
+
>
|
|
196
|
+
<svg
|
|
197
|
+
aria-hidden="true"
|
|
198
|
+
viewBox="0 0 24 24"
|
|
199
|
+
className="h-4 w-4"
|
|
200
|
+
fill="none"
|
|
201
|
+
stroke="currentColor"
|
|
202
|
+
strokeLinecap="round"
|
|
203
|
+
strokeLinejoin="round"
|
|
204
|
+
strokeWidth="2"
|
|
205
|
+
>
|
|
206
|
+
<path d="M10 13a5 5 0 0 0 7.07 0l2.83-2.83a5 5 0 0 0-7.07-7.07L11 4" />
|
|
207
|
+
<path d="M14 11a5 5 0 0 0-7.07 0L4.1 13.83a5 5 0 1 0 7.07 7.07L13 20" />
|
|
208
|
+
</svg>
|
|
209
|
+
</a>
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function getLeafTypeLabel(schema: SchemaLike) {
|
|
214
|
+
if (isSchemaRef(schema)) {
|
|
215
|
+
return schema.schema;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return typeof schema.type === "string" ? schema.type : "unknown";
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function flattenSchemaRows(schema: SchemaLike, prefix: string, required = false): FlatSchemaRow[] {
|
|
222
|
+
if (isSchemaRef(schema)) {
|
|
223
|
+
return [createSchemaRow(schema, prefix || "—", required)];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (Array.isArray(schema.oneOf) && schema.oneOf.length > 0) {
|
|
227
|
+
return (schema.oneOf as SchemaLike[]).flatMap((option, index) => {
|
|
228
|
+
const optionPrefix = prefix ? `${prefix} (${index + 1})` : `${index + 1}`;
|
|
229
|
+
|
|
230
|
+
return flattenSchemaRows(option, optionPrefix, required);
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const type = typeof schema.type === "string" ? schema.type : undefined;
|
|
235
|
+
|
|
236
|
+
if (type === "object") {
|
|
237
|
+
const requiredKeys = new Set(Array.isArray(schema.required) ? schema.required.map(String) : []);
|
|
238
|
+
const properties =
|
|
239
|
+
schema.properties && typeof schema.properties === "object"
|
|
240
|
+
? (schema.properties as Record<string, SchemaLike>)
|
|
241
|
+
: {};
|
|
242
|
+
const rows = Object.entries(properties).flatMap(([key, propertySchema]) =>
|
|
243
|
+
flattenSchemaRows(propertySchema, prefix ? `${prefix}.${key}` : key, requiredKeys.has(key)),
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
return appendAdditionalPropertyRows(schema, prefix, rows);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (type === "array") {
|
|
250
|
+
if (schema.items && typeof schema.items === "object") {
|
|
251
|
+
const items = schema.items as SchemaLike;
|
|
252
|
+
const itemType = getLeafTypeLabel(items);
|
|
253
|
+
const arrayPath = prefix ? `${prefix}[]` : "[]";
|
|
254
|
+
|
|
255
|
+
if (isSchemaRef(items) || (items.type && items.type !== "object")) {
|
|
256
|
+
const row = createSchemaRow(
|
|
257
|
+
schema,
|
|
258
|
+
arrayPath,
|
|
259
|
+
required,
|
|
260
|
+
isSchemaRef(items) ? itemType : `${itemType}[]`,
|
|
261
|
+
);
|
|
262
|
+
row.isRef = isSchemaRef(items);
|
|
263
|
+
row.description = getSchemaDescription(schema) || getSchemaDescription(items);
|
|
264
|
+
row.constraints = getArrayElementConstraintLines(schema, items);
|
|
265
|
+
|
|
266
|
+
return [row];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (items.type === "object" && items.properties && typeof items.properties === "object") {
|
|
270
|
+
const requiredKeys = new Set(
|
|
271
|
+
Array.isArray(items.required) ? items.required.map(String) : [],
|
|
272
|
+
);
|
|
273
|
+
const properties = items.properties as Record<string, SchemaLike>;
|
|
274
|
+
const rows = Object.entries(properties).flatMap(([key, propertySchema]) =>
|
|
275
|
+
flattenSchemaRows(
|
|
276
|
+
propertySchema,
|
|
277
|
+
prefix ? `${prefix}[].${key}` : `[].${key}`,
|
|
278
|
+
requiredKeys.has(key),
|
|
279
|
+
),
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
if (getSchemaConstraintLines(schema).length > 0) {
|
|
283
|
+
rows.unshift({
|
|
284
|
+
path: arrayPath,
|
|
285
|
+
typeLabel: "object[]",
|
|
286
|
+
isRef: false,
|
|
287
|
+
required,
|
|
288
|
+
description: getSchemaDescription(schema),
|
|
289
|
+
constraints: getSchemaConstraintLines(schema),
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return appendAdditionalPropertyRows(items, prefix ? `${prefix}[]` : "[]", rows);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return [createSchemaRow(schema, prefix || "—", required)];
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (type) {
|
|
301
|
+
return [createSchemaRow(schema, prefix || "—", required)];
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return [];
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function SchemaTable(props: { schema: SchemaLike; setKey?: string }) {
|
|
308
|
+
const rows = flattenSchemaRows(props.schema, "");
|
|
309
|
+
|
|
310
|
+
if (rows.length === 0) {
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return (
|
|
315
|
+
<div className="overflow-x-auto">
|
|
316
|
+
<table className="variable-schema-table w-full table-fixed text-xs leading-snug">
|
|
317
|
+
<colgroup>
|
|
318
|
+
<col className="variable-schema-col-path" />
|
|
319
|
+
<col className="variable-schema-col-type" />
|
|
320
|
+
<col className="variable-schema-col-constraints" />
|
|
321
|
+
<col className="variable-schema-col-description" />
|
|
322
|
+
</colgroup>
|
|
323
|
+
<thead>
|
|
324
|
+
<tr className="border-b border-border text-left text-[10px] font-semibold uppercase tracking-wide text-faint">
|
|
325
|
+
<th className="pb-1.5 pr-3 font-semibold">Path</th>
|
|
326
|
+
<th className="pb-1.5 pr-3 font-semibold">Type</th>
|
|
327
|
+
<th className="pb-1.5 pr-3 font-semibold">Constraints</th>
|
|
328
|
+
<th className="pb-1.5 font-semibold">Description</th>
|
|
329
|
+
</tr>
|
|
330
|
+
</thead>
|
|
331
|
+
<tbody>
|
|
332
|
+
{rows.map((row, index) => (
|
|
333
|
+
<tr key={`${row.path}-${index}`} className="border-b border-border/60 align-top">
|
|
334
|
+
<td className="py-1.5 pr-3 [overflow-wrap:anywhere]">
|
|
335
|
+
<span className="font-mono text-text">{row.path}</span>
|
|
336
|
+
{row.required && (
|
|
337
|
+
<span className="ml-1.5 text-[9px] font-semibold uppercase text-danger">
|
|
338
|
+
required
|
|
339
|
+
</span>
|
|
340
|
+
)}
|
|
341
|
+
</td>
|
|
342
|
+
<td className="py-1.5 pr-3">
|
|
343
|
+
{row.isRef ? (
|
|
344
|
+
<span className="inline-flex items-center gap-1">
|
|
345
|
+
<SchemaRefLink name={row.typeLabel} setKey={props.setKey} />
|
|
346
|
+
{row.path.endsWith("[]") && <TypeBadge>[]</TypeBadge>}
|
|
347
|
+
</span>
|
|
348
|
+
) : (
|
|
349
|
+
<TypeBadge>{row.typeLabel}</TypeBadge>
|
|
350
|
+
)}
|
|
351
|
+
</td>
|
|
352
|
+
<td className="py-1.5 pr-3 font-mono text-[11px] text-muted [overflow-wrap:anywhere]">
|
|
353
|
+
{row.constraints.length > 0 ? (
|
|
354
|
+
<ul className="space-y-0.5">
|
|
355
|
+
{row.constraints.map((constraint) => (
|
|
356
|
+
<li key={constraint}>{constraint}</li>
|
|
357
|
+
))}
|
|
358
|
+
</ul>
|
|
359
|
+
) : (
|
|
360
|
+
<span className="text-faint">—</span>
|
|
361
|
+
)}
|
|
362
|
+
</td>
|
|
363
|
+
<td className="py-1.5 text-muted [overflow-wrap:anywhere]">
|
|
364
|
+
{row.description || <span className="text-faint">—</span>}
|
|
365
|
+
</td>
|
|
366
|
+
</tr>
|
|
367
|
+
))}
|
|
368
|
+
</tbody>
|
|
369
|
+
</table>
|
|
370
|
+
</div>
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
type SchemaProperty = {
|
|
375
|
+
label: string;
|
|
376
|
+
value: React.ReactNode;
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
function SchemaPropertyValue(props: { children: React.ReactNode }) {
|
|
380
|
+
return <OverviewChip className="font-mono">{props.children}</OverviewChip>;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function getSchemaProperties(
|
|
384
|
+
schema: SchemaLike,
|
|
385
|
+
setKey?: string,
|
|
386
|
+
options: { includeType?: boolean } = {},
|
|
387
|
+
): SchemaProperty[] {
|
|
388
|
+
const properties: SchemaProperty[] = [];
|
|
389
|
+
|
|
390
|
+
if (isSchemaRef(schema)) {
|
|
391
|
+
properties.push({
|
|
392
|
+
label: "Schema",
|
|
393
|
+
value: <SchemaRefLink name={schema.schema} setKey={setKey} />,
|
|
394
|
+
});
|
|
395
|
+
return properties;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (options.includeType && typeof schema.type === "string") {
|
|
399
|
+
properties.push({
|
|
400
|
+
label: "Type",
|
|
401
|
+
value: <TypeBadge>{schema.type}</TypeBadge>,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (Array.isArray(schema.enum) && schema.enum.length > 0) {
|
|
406
|
+
properties.push({
|
|
407
|
+
label: "Enum",
|
|
408
|
+
value: (
|
|
409
|
+
<span className="flex flex-wrap gap-1.5">
|
|
410
|
+
{schema.enum.map((value, index) => (
|
|
411
|
+
<SchemaPropertyValue key={index}>{formatConstraintValue(value)}</SchemaPropertyValue>
|
|
412
|
+
))}
|
|
413
|
+
</span>
|
|
414
|
+
),
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if ("const" in schema && schema.const !== undefined) {
|
|
419
|
+
properties.push({
|
|
420
|
+
label: "Const",
|
|
421
|
+
value: <SchemaPropertyValue>{formatConstraintValue(schema.const)}</SchemaPropertyValue>,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (typeof schema.minimum === "number") {
|
|
426
|
+
properties.push({
|
|
427
|
+
label: "Minimum",
|
|
428
|
+
value: <SchemaPropertyValue>{schema.minimum}</SchemaPropertyValue>,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (typeof schema.maximum === "number") {
|
|
433
|
+
properties.push({
|
|
434
|
+
label: "Maximum",
|
|
435
|
+
value: <SchemaPropertyValue>{schema.maximum}</SchemaPropertyValue>,
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (typeof schema.minLength === "number") {
|
|
440
|
+
properties.push({
|
|
441
|
+
label: "Min length",
|
|
442
|
+
value: <SchemaPropertyValue>{schema.minLength}</SchemaPropertyValue>,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (typeof schema.maxLength === "number") {
|
|
447
|
+
properties.push({
|
|
448
|
+
label: "Max length",
|
|
449
|
+
value: <SchemaPropertyValue>{schema.maxLength}</SchemaPropertyValue>,
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (typeof schema.pattern === "string") {
|
|
454
|
+
properties.push({
|
|
455
|
+
label: "Pattern",
|
|
456
|
+
value: <SchemaPropertyValue>{schema.pattern}</SchemaPropertyValue>,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (typeof schema.minItems === "number") {
|
|
461
|
+
properties.push({
|
|
462
|
+
label: "Min items",
|
|
463
|
+
value: <SchemaPropertyValue>{schema.minItems}</SchemaPropertyValue>,
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (typeof schema.maxItems === "number") {
|
|
468
|
+
properties.push({
|
|
469
|
+
label: "Max items",
|
|
470
|
+
value: <SchemaPropertyValue>{schema.maxItems}</SchemaPropertyValue>,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (schema.uniqueItems === true) {
|
|
475
|
+
properties.push({
|
|
476
|
+
label: "Unique items",
|
|
477
|
+
value: <SchemaPropertyValue>true</SchemaPropertyValue>,
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return properties;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function hasSchemaProperties(schema: SchemaLike) {
|
|
485
|
+
if (Array.isArray(schema.oneOf) && schema.oneOf.length > 0) {
|
|
486
|
+
return true;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return getSchemaProperties(schema).length > 0;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function SchemaPropertiesPanel(props: {
|
|
493
|
+
schema: SchemaLike;
|
|
494
|
+
setKey?: string;
|
|
495
|
+
includeType?: boolean;
|
|
496
|
+
}) {
|
|
497
|
+
const properties = getSchemaProperties(props.schema, props.setKey, {
|
|
498
|
+
includeType: props.includeType,
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
if (properties.length === 0) {
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return (
|
|
506
|
+
<OverviewMetaPanel>
|
|
507
|
+
{properties.map((property) => (
|
|
508
|
+
<OverviewMetaRow key={property.label} label={property.label}>
|
|
509
|
+
{property.value}
|
|
510
|
+
</OverviewMetaRow>
|
|
511
|
+
))}
|
|
512
|
+
</OverviewMetaPanel>
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
export function SchemaPropertiesOverview(props: { schema: SchemaLike; setKey?: string }) {
|
|
517
|
+
const schema = props.schema;
|
|
518
|
+
|
|
519
|
+
if (!hasSchemaProperties(schema)) {
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (Array.isArray(schema.oneOf) && schema.oneOf.length > 0) {
|
|
524
|
+
return (
|
|
525
|
+
<div className="space-y-4">
|
|
526
|
+
{(schema.oneOf as SchemaLike[]).map((option, index) => (
|
|
527
|
+
<section key={index}>
|
|
528
|
+
<h3 className="mb-2 text-xs font-semibold uppercase tracking-wide text-faint">
|
|
529
|
+
Option {index + 1}
|
|
530
|
+
</h3>
|
|
531
|
+
<SchemaPropertiesPanel schema={option} setKey={props.setKey} includeType />
|
|
532
|
+
</section>
|
|
533
|
+
))}
|
|
534
|
+
</div>
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return <SchemaPropertiesPanel schema={schema} setKey={props.setKey} />;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function DefaultValueCodeBlock(props: { children: React.ReactNode; nested?: boolean }) {
|
|
542
|
+
return (
|
|
543
|
+
<pre
|
|
544
|
+
className={[
|
|
545
|
+
"overflow-x-auto font-mono text-xs leading-relaxed text-text [overflow-wrap:anywhere] whitespace-pre-wrap",
|
|
546
|
+
props.nested
|
|
547
|
+
? "rounded bg-elevated/70 px-2.5 py-2"
|
|
548
|
+
: "rounded-md border border-border bg-elevated p-3",
|
|
549
|
+
].join(" ")}
|
|
550
|
+
>
|
|
551
|
+
<code>{props.children}</code>
|
|
552
|
+
</pre>
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
export function VariableValueView(props: { value: unknown; nested?: boolean }) {
|
|
557
|
+
const value = props.value;
|
|
558
|
+
|
|
559
|
+
if (value === undefined || value === null) {
|
|
560
|
+
return <span className="text-muted">none</span>;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (typeof value === "string") {
|
|
564
|
+
return <DefaultValueCodeBlock nested={props.nested}>{value}</DefaultValueCodeBlock>;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (typeof value === "object") {
|
|
568
|
+
return (
|
|
569
|
+
<DefaultValueCodeBlock nested={props.nested}>
|
|
570
|
+
{JSON.stringify(value, null, 2)}
|
|
571
|
+
</DefaultValueCodeBlock>
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return <span className="font-mono text-sm text-text">{String(value)}</span>;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
export function isInlineVariableValue(value: unknown) {
|
|
579
|
+
return typeof value === "number" || typeof value === "boolean";
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function VariableDefinition(props: { name: string; schema: SchemaLike; setKey?: string }) {
|
|
583
|
+
const schema = props.schema;
|
|
584
|
+
const variableId = slugifyFragment(props.name);
|
|
585
|
+
const schemaRef = isSchemaRef(schema);
|
|
586
|
+
const type = typeof schema.type === "string" ? schema.type : undefined;
|
|
587
|
+
const showStructure = hasSchemaTableRows(schema);
|
|
588
|
+
const description = getSchemaDescription(schema);
|
|
589
|
+
const defaultValue = "defaultValue" in schema ? schema.defaultValue : undefined;
|
|
590
|
+
const inlineDefault = isInlineVariableValue(defaultValue);
|
|
591
|
+
|
|
592
|
+
return (
|
|
593
|
+
<section
|
|
594
|
+
id={variableId}
|
|
595
|
+
className="scroll-mt-6 rounded-lg border border-border bg-elevated/60 p-4 ring-1 ring-black/5"
|
|
596
|
+
>
|
|
597
|
+
<div className="group flex flex-wrap items-center gap-x-3 gap-y-1">
|
|
598
|
+
<h2 className="min-w-0 font-mono text-base font-semibold text-text">
|
|
599
|
+
<a
|
|
600
|
+
href={`#${variableId}`}
|
|
601
|
+
className="text-text no-underline hover:text-primary [overflow-wrap:anywhere]"
|
|
602
|
+
>
|
|
603
|
+
<EntityKey value={props.name} className="font-semibold" />
|
|
604
|
+
</a>
|
|
605
|
+
</h2>
|
|
606
|
+
{schemaRef ? (
|
|
607
|
+
<SchemaRefLink name={String(schema.schema)} setKey={props.setKey} />
|
|
608
|
+
) : type ? (
|
|
609
|
+
<TypeBadge>{type}</TypeBadge>
|
|
610
|
+
) : null}
|
|
611
|
+
<VariablePermalink targetId={variableId} />
|
|
612
|
+
{schema.deprecated === true && <Badge tone="warning">deprecated</Badge>}
|
|
613
|
+
</div>
|
|
614
|
+
|
|
615
|
+
{description && <p className="mt-1.5 text-sm text-muted">{description}</p>}
|
|
616
|
+
|
|
617
|
+
{showStructure && (
|
|
618
|
+
<div className="mt-4">
|
|
619
|
+
<h3 className="mb-2 text-xs font-semibold uppercase tracking-wide text-faint">
|
|
620
|
+
Structure
|
|
621
|
+
</h3>
|
|
622
|
+
<SchemaTable schema={schema} setKey={props.setKey} />
|
|
623
|
+
</div>
|
|
624
|
+
)}
|
|
625
|
+
|
|
626
|
+
{"defaultValue" in schema &&
|
|
627
|
+
(inlineDefault ? (
|
|
628
|
+
<p className="mt-3 text-sm">
|
|
629
|
+
<span className="text-xs font-semibold uppercase tracking-wide text-faint">
|
|
630
|
+
Default{" "}
|
|
631
|
+
</span>
|
|
632
|
+
<VariableValueView value={defaultValue} />
|
|
633
|
+
</p>
|
|
634
|
+
) : (
|
|
635
|
+
<div className="mt-4">
|
|
636
|
+
<h3 className="mb-2 text-xs font-semibold uppercase tracking-wide text-faint">
|
|
637
|
+
Default
|
|
638
|
+
</h3>
|
|
639
|
+
<VariableValueView value={defaultValue} />
|
|
640
|
+
</div>
|
|
641
|
+
))}
|
|
642
|
+
</section>
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function useScrollToHash(dependencies: React.DependencyList) {
|
|
647
|
+
React.useEffect(() => {
|
|
648
|
+
if (typeof window === "undefined" || !window.location.hash) {
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const targetId = decodeURIComponent(window.location.hash.slice(1));
|
|
653
|
+
|
|
654
|
+
if (!targetId) {
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const frame = window.requestAnimationFrame(() => {
|
|
659
|
+
const targetElement = document.getElementById(targetId);
|
|
660
|
+
|
|
661
|
+
if (!targetElement) {
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
targetElement.scrollIntoView({ block: "start" });
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
return () => {
|
|
669
|
+
window.cancelAnimationFrame(frame);
|
|
670
|
+
};
|
|
671
|
+
}, dependencies);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
export function FeatureVariablesList(props: {
|
|
675
|
+
variablesSchema: Record<string, unknown>;
|
|
676
|
+
setKey?: string;
|
|
677
|
+
}) {
|
|
678
|
+
const entries = Object.entries(props.variablesSchema).sort(([left], [right]) =>
|
|
679
|
+
left.localeCompare(right),
|
|
680
|
+
);
|
|
681
|
+
|
|
682
|
+
useScrollToHash([entries.length]);
|
|
683
|
+
|
|
684
|
+
if (entries.length === 0) {
|
|
685
|
+
return null;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
return (
|
|
689
|
+
<div className="space-y-4">
|
|
690
|
+
{entries.map(([name, schema]) => (
|
|
691
|
+
<VariableDefinition
|
|
692
|
+
key={name}
|
|
693
|
+
name={name}
|
|
694
|
+
schema={(schema || {}) as SchemaLike}
|
|
695
|
+
setKey={props.setKey}
|
|
696
|
+
/>
|
|
697
|
+
))}
|
|
698
|
+
</div>
|
|
699
|
+
);
|
|
700
|
+
}
|