@eventcatalog/core 2.63.0 → 2.64.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/dist/analytics/analytics.cjs +1 -1
- package/dist/analytics/analytics.js +2 -2
- package/dist/analytics/log-build.cjs +1 -1
- package/dist/analytics/log-build.js +3 -3
- package/dist/{chunk-I2FMV7LN.js → chunk-6AMZOBWI.js} +1 -1
- package/dist/{chunk-IRFM5IS7.js → chunk-CWGFHLMX.js} +1 -1
- package/dist/{chunk-GA274FBN.js → chunk-PLMTJHGH.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog.cjs +1 -1
- package/dist/eventcatalog.js +3 -3
- package/eventcatalog/astro.config.mjs +2 -1
- package/eventcatalog/public/icons/avro.svg +21 -0
- package/eventcatalog/public/icons/json-schema.svg +6 -0
- package/eventcatalog/public/icons/proto.svg +10 -0
- package/eventcatalog/src/components/Grids/utils.tsx +5 -3
- package/eventcatalog/src/components/MDX/RemoteFile.astro +5 -11
- package/eventcatalog/src/components/MDX/SchemaViewer/SchemaViewerRoot.astro +41 -6
- package/eventcatalog/src/components/SchemaExplorer/ApiAccessSection.tsx +139 -0
- package/eventcatalog/src/components/SchemaExplorer/AvroSchemaViewer.tsx +423 -0
- package/eventcatalog/src/components/SchemaExplorer/DiffViewer.tsx +102 -0
- package/eventcatalog/src/components/SchemaExplorer/JSONSchemaViewer.tsx +740 -0
- package/eventcatalog/src/components/SchemaExplorer/OwnersSection.tsx +56 -0
- package/eventcatalog/src/components/SchemaExplorer/Pagination.tsx +33 -0
- package/eventcatalog/src/components/SchemaExplorer/ProducersConsumersSection.tsx +91 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaCodeModal.tsx +93 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaContentViewer.tsx +130 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsHeader.tsx +181 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsPanel.tsx +232 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaExplorer.tsx +415 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaFilters.tsx +174 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaListItem.tsx +73 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaViewerModal.tsx +77 -0
- package/eventcatalog/src/components/SchemaExplorer/VersionHistoryModal.tsx +72 -0
- package/eventcatalog/src/components/SchemaExplorer/types.ts +45 -0
- package/eventcatalog/src/components/SchemaExplorer/utils.ts +81 -0
- package/eventcatalog/src/components/SideNav/ListViewSideBar/index.tsx +33 -2
- package/eventcatalog/src/components/Tables/columns/MessageTableColumns.tsx +2 -2
- package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +10 -0
- package/eventcatalog/src/pages/api/schemas/[collection]/[id]/[version]/index.ts +45 -0
- package/eventcatalog/src/pages/api/schemas/services/[id]/[version]/[specification]/index.ts +51 -0
- package/eventcatalog/src/pages/docs/llm/schemas.txt.ts +86 -0
- package/eventcatalog/src/pages/schemas/index.astro +175 -0
- package/eventcatalog/src/utils/files.ts +9 -0
- package/package.json +1 -1
- package/eventcatalog/src/components/MDX/SchemaViewer/SchemaProperty.astro +0 -204
- package/eventcatalog/src/components/MDX/SchemaViewer/SchemaViewer.astro +0 -705
|
@@ -0,0 +1,740 @@
|
|
|
1
|
+
import { useState, useEffect, useRef, useMemo } from 'react';
|
|
2
|
+
|
|
3
|
+
interface JSONSchemaViewerProps {
|
|
4
|
+
schema: any;
|
|
5
|
+
title?: string;
|
|
6
|
+
maxHeight?: string;
|
|
7
|
+
expand?: boolean | string;
|
|
8
|
+
search?: boolean | string;
|
|
9
|
+
id?: string;
|
|
10
|
+
onOpenFullscreen?: () => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface SchemaPropertyProps {
|
|
14
|
+
name: string;
|
|
15
|
+
details: any;
|
|
16
|
+
isRequired: boolean;
|
|
17
|
+
level: number;
|
|
18
|
+
isListItem?: boolean;
|
|
19
|
+
expand: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Helper function to count properties recursively
|
|
23
|
+
function countProperties(obj: any): number {
|
|
24
|
+
if (!obj || typeof obj !== 'object') return 0;
|
|
25
|
+
|
|
26
|
+
let count = 0;
|
|
27
|
+
if (obj.properties) {
|
|
28
|
+
count += Object.keys(obj.properties).length;
|
|
29
|
+
Object.values(obj.properties).forEach((prop: any) => {
|
|
30
|
+
count += countProperties(prop);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
if (obj.items) {
|
|
34
|
+
count += countProperties(obj.items);
|
|
35
|
+
}
|
|
36
|
+
if (obj._isRootArrayItem && obj._rootArraySchema?.items) {
|
|
37
|
+
// Don't double count
|
|
38
|
+
}
|
|
39
|
+
return count;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Schema processing functions
|
|
43
|
+
function mergeAllOfSchemas(schemaWithProcessor: any): any {
|
|
44
|
+
const { processSchema: processor, ...schema } = schemaWithProcessor;
|
|
45
|
+
if (!schema.allOf) return schema;
|
|
46
|
+
|
|
47
|
+
const mergedSchema: {
|
|
48
|
+
type: string;
|
|
49
|
+
properties: Record<string, any>;
|
|
50
|
+
required: string[];
|
|
51
|
+
description?: string;
|
|
52
|
+
[key: string]: any;
|
|
53
|
+
} = {
|
|
54
|
+
type: schema.type || 'object',
|
|
55
|
+
properties: {},
|
|
56
|
+
required: [],
|
|
57
|
+
description: schema.description,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Copy base schema properties first (excluding allOf)
|
|
61
|
+
Object.keys(schema).forEach((key) => {
|
|
62
|
+
if (key !== 'allOf' && key !== 'properties' && key !== 'required' && key !== 'description') {
|
|
63
|
+
mergedSchema[key] = schema[key];
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Copy base properties if they exist
|
|
68
|
+
if (schema.properties) {
|
|
69
|
+
mergedSchema.properties = { ...schema.properties };
|
|
70
|
+
}
|
|
71
|
+
if (schema.required) {
|
|
72
|
+
mergedSchema.required = [...schema.required];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
schema.allOf.forEach((subSchema: any) => {
|
|
76
|
+
const processedSubSchema = processor ? processor(subSchema) : subSchema;
|
|
77
|
+
|
|
78
|
+
if (processedSubSchema.properties) {
|
|
79
|
+
mergedSchema.properties = {
|
|
80
|
+
...mergedSchema.properties,
|
|
81
|
+
...processedSubSchema.properties,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (processedSubSchema.required) {
|
|
85
|
+
mergedSchema.required = [...new Set([...mergedSchema.required, ...processedSubSchema.required])];
|
|
86
|
+
}
|
|
87
|
+
if (processedSubSchema.description && !mergedSchema.description) {
|
|
88
|
+
mergedSchema.description = processedSubSchema.description;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
Object.keys(processedSubSchema).forEach((key) => {
|
|
92
|
+
if (key !== 'properties' && key !== 'required' && key !== 'description' && key !== 'type') {
|
|
93
|
+
if (!mergedSchema[key]) {
|
|
94
|
+
mergedSchema[key] = processedSubSchema[key];
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
return mergedSchema;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function processSchema(schema: any, rootSchema?: any): any {
|
|
104
|
+
if (!schema) return schema;
|
|
105
|
+
|
|
106
|
+
const root = rootSchema || schema;
|
|
107
|
+
|
|
108
|
+
// Handle $ref
|
|
109
|
+
if (schema.$ref) {
|
|
110
|
+
const refPath = schema.$ref;
|
|
111
|
+
let resolvedSchema = null;
|
|
112
|
+
let defName = '';
|
|
113
|
+
|
|
114
|
+
// Try draft-7 style first: #/definitions/
|
|
115
|
+
if (refPath.startsWith('#/definitions/')) {
|
|
116
|
+
defName = refPath.replace('#/definitions/', '');
|
|
117
|
+
if (root.definitions && root.definitions[defName]) {
|
|
118
|
+
resolvedSchema = root.definitions[defName];
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Try 2020-12 style: #/$defs/
|
|
122
|
+
else if (refPath.startsWith('#/$defs/')) {
|
|
123
|
+
defName = refPath.replace('#/$defs/', '');
|
|
124
|
+
if (root.$defs && root.$defs[defName]) {
|
|
125
|
+
resolvedSchema = root.$defs[defName];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// Try other common patterns
|
|
129
|
+
else if (refPath.startsWith('#/components/schemas/')) {
|
|
130
|
+
defName = refPath.replace('#/components/schemas/', '');
|
|
131
|
+
if (root.components && root.components.schemas && root.components.schemas[defName]) {
|
|
132
|
+
resolvedSchema = root.components.schemas[defName];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (resolvedSchema) {
|
|
137
|
+
const processedSchema = processSchema(resolvedSchema, root);
|
|
138
|
+
return {
|
|
139
|
+
...processedSchema,
|
|
140
|
+
_refPath: refPath,
|
|
141
|
+
_refName: defName,
|
|
142
|
+
_originalRef: schema.$ref,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
type: 'string',
|
|
148
|
+
description: `Reference to ${refPath} (definition not found in root schema)`,
|
|
149
|
+
title: defName || refPath.split('/').pop(),
|
|
150
|
+
_refPath: refPath,
|
|
151
|
+
_refName: defName,
|
|
152
|
+
_refNotFound: true,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (schema.allOf) {
|
|
157
|
+
return mergeAllOfSchemas({ ...schema, processSchema: (s: any) => processSchema(s, root) });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (schema.oneOf) {
|
|
161
|
+
const processedVariants = schema.oneOf.map((variant: any) => {
|
|
162
|
+
const processedVariant = processSchema(variant, root);
|
|
163
|
+
return {
|
|
164
|
+
title: processedVariant.title || variant.title || 'Unnamed Variant',
|
|
165
|
+
required: processedVariant.required || variant.required || [],
|
|
166
|
+
properties: processedVariant.properties || {},
|
|
167
|
+
...processedVariant,
|
|
168
|
+
};
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const allProperties: Record<string, any> = {};
|
|
172
|
+
processedVariants.forEach((variant: any) => {
|
|
173
|
+
if (variant.properties) {
|
|
174
|
+
Object.assign(allProperties, variant.properties);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
...schema,
|
|
180
|
+
type: schema.type || 'object',
|
|
181
|
+
properties: {
|
|
182
|
+
...(schema.properties || {}),
|
|
183
|
+
...allProperties,
|
|
184
|
+
},
|
|
185
|
+
variants: processedVariants,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Process nested schemas in properties
|
|
190
|
+
if (schema.properties) {
|
|
191
|
+
const processedProperties: Record<string, any> = {};
|
|
192
|
+
Object.entries(schema.properties).forEach(([key, prop]: [string, any]) => {
|
|
193
|
+
processedProperties[key] = processSchema(prop, root);
|
|
194
|
+
});
|
|
195
|
+
schema = { ...schema, properties: processedProperties };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Process array items
|
|
199
|
+
if (schema.type === 'array' && schema.items) {
|
|
200
|
+
schema = { ...schema, items: processSchema(schema.items, root) };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return schema;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// SchemaProperty component
|
|
207
|
+
const SchemaProperty = ({ name, details, isRequired, level, isListItem = false, expand }: SchemaPropertyProps) => {
|
|
208
|
+
const [isExpanded, setIsExpanded] = useState(expand);
|
|
209
|
+
const contentId = useRef(`prop-${name}-${level}-${Math.random().toString(36).substring(2, 7)}`).current;
|
|
210
|
+
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
setIsExpanded(expand);
|
|
213
|
+
}, [expand]);
|
|
214
|
+
|
|
215
|
+
const hasNestedProperties = details.type === 'object' && details.properties && Object.keys(details.properties).length > 0;
|
|
216
|
+
const hasArrayItems = details.type === 'array' && details.items;
|
|
217
|
+
const hasArrayItemProperties =
|
|
218
|
+
hasArrayItems &&
|
|
219
|
+
((details.items.type === 'object' && details.items.properties) ||
|
|
220
|
+
details.items.allOf ||
|
|
221
|
+
details.items.oneOf ||
|
|
222
|
+
details.items.$ref);
|
|
223
|
+
const isCollapsible = hasNestedProperties || hasArrayItemProperties;
|
|
224
|
+
|
|
225
|
+
const indentationClass = `pl-${level * 3}`;
|
|
226
|
+
|
|
227
|
+
return (
|
|
228
|
+
<div className={`property-container mb-1.5 border-l border-gray-100 relative ${indentationClass}`}>
|
|
229
|
+
<div className="flex items-start space-x-1.5">
|
|
230
|
+
{isCollapsible ? (
|
|
231
|
+
<button
|
|
232
|
+
type="button"
|
|
233
|
+
aria-expanded={isExpanded}
|
|
234
|
+
aria-controls={contentId}
|
|
235
|
+
onClick={() => setIsExpanded(!isExpanded)}
|
|
236
|
+
className="property-toggle text-gray-500 hover:text-gray-700 pt-0.5 focus:outline-none w-3 text-center flex-shrink-0"
|
|
237
|
+
>
|
|
238
|
+
<span className={`icon-collapsed font-mono text-xs ${isExpanded ? 'hidden' : ''}`}>></span>
|
|
239
|
+
<span className={`icon-expanded font-mono text-xs ${!isExpanded ? 'hidden' : ''}`}>v</span>
|
|
240
|
+
</button>
|
|
241
|
+
) : (
|
|
242
|
+
<div className="w-3 h-4 flex-shrink-0" />
|
|
243
|
+
)}
|
|
244
|
+
|
|
245
|
+
<div className="flex-grow">
|
|
246
|
+
<div className="flex justify-between items-baseline">
|
|
247
|
+
<div>
|
|
248
|
+
<span className="font-semibold text-gray-800 text-sm">{name}</span>
|
|
249
|
+
<span className="ml-1.5 text-purple-600 font-mono text-xs">
|
|
250
|
+
{details.type}
|
|
251
|
+
{details.type === 'array' && details.items?.type ? `[${details.items.type}]` : ''}
|
|
252
|
+
{details.format ? `<${details.format}>` : ''}
|
|
253
|
+
{details._refPath && <span className="text-blue-600 ml-1">→ {details._refName || details._refPath}</span>}
|
|
254
|
+
{details._refNotFound && <span className="text-red-600 ml-1">❌ ref not found</span>}
|
|
255
|
+
{details.const !== undefined && (
|
|
256
|
+
<span>
|
|
257
|
+
constant: <code>{details.const}</code>
|
|
258
|
+
</span>
|
|
259
|
+
)}
|
|
260
|
+
</span>
|
|
261
|
+
</div>
|
|
262
|
+
{isRequired && <span className="text-red-600 text-xs ml-3 flex-shrink-0">required</span>}
|
|
263
|
+
</div>
|
|
264
|
+
|
|
265
|
+
{details.description && <p className="text-gray-500 text-xs mt-0.5">{details.description}</p>}
|
|
266
|
+
{details.title && details.title !== details.description && (
|
|
267
|
+
<p className="text-gray-500 text-xs mt-0.5 italic">Title: {details.title}</p>
|
|
268
|
+
)}
|
|
269
|
+
|
|
270
|
+
<div className="text-xs text-gray-500 mt-0.5 space-y-0">
|
|
271
|
+
{details.pattern && (
|
|
272
|
+
<div>
|
|
273
|
+
Match pattern: <code className="bg-gray-100 px-1 rounded text-gray-800 font-thin py-0.5">{details.pattern}</code>
|
|
274
|
+
</div>
|
|
275
|
+
)}
|
|
276
|
+
{details.minimum !== undefined && (
|
|
277
|
+
<div>
|
|
278
|
+
Minimum: <code className="bg-gray-100 px-1 rounded text-gray-800 font-thin py-0.5">{details.minimum}</code>
|
|
279
|
+
</div>
|
|
280
|
+
)}
|
|
281
|
+
{details.maximum !== undefined && (
|
|
282
|
+
<div>
|
|
283
|
+
Maximum: <code className="bg-gray-100 px-1 rounded text-gray-800 font-thin py-0.5">{details.maximum}</code>
|
|
284
|
+
</div>
|
|
285
|
+
)}
|
|
286
|
+
{details.minLength !== undefined && (
|
|
287
|
+
<div>
|
|
288
|
+
Min length: <code className="bg-gray-100 px-1 rounded text-gray-800 font-thin py-0.5">{details.minLength}</code>
|
|
289
|
+
</div>
|
|
290
|
+
)}
|
|
291
|
+
{details.maxLength !== undefined && (
|
|
292
|
+
<div>
|
|
293
|
+
Max length: <code className="bg-gray-100 px-1 rounded text-gray-800 font-thin py-0.5">{details.maxLength}</code>
|
|
294
|
+
</div>
|
|
295
|
+
)}
|
|
296
|
+
{details.enum && (
|
|
297
|
+
<div>
|
|
298
|
+
<span className="text-xs inline-block">Allowed values:</span>
|
|
299
|
+
{details.enum.map((val: any, idx: number) => (
|
|
300
|
+
<span key={idx} className="text-xs">
|
|
301
|
+
{' '}
|
|
302
|
+
<code className="bg-gray-100 px-1 rounded text-gray-800 font-thin py-0.5">{val}</code>
|
|
303
|
+
</span>
|
|
304
|
+
))}
|
|
305
|
+
</div>
|
|
306
|
+
)}
|
|
307
|
+
</div>
|
|
308
|
+
|
|
309
|
+
{(hasNestedProperties || hasArrayItems) && (
|
|
310
|
+
<div id={contentId} className={`nested-content mt-1 ${isCollapsible && !isExpanded ? 'hidden' : ''}`}>
|
|
311
|
+
{hasNestedProperties &&
|
|
312
|
+
details.properties &&
|
|
313
|
+
Object.entries(details.properties).map(([nestedName, nestedDetails]: [string, any]) => (
|
|
314
|
+
<SchemaProperty
|
|
315
|
+
key={nestedName}
|
|
316
|
+
name={nestedName}
|
|
317
|
+
details={nestedDetails}
|
|
318
|
+
isRequired={details.required?.includes(nestedName) ?? false}
|
|
319
|
+
level={level + 1}
|
|
320
|
+
expand={expand}
|
|
321
|
+
/>
|
|
322
|
+
))}
|
|
323
|
+
|
|
324
|
+
{hasArrayItemProperties && (
|
|
325
|
+
<div className="mt-1 border-l border-dashed border-gray-400 pl-3 ml-1.5">
|
|
326
|
+
<span className="text-xs italic text-gray-500 block mb-1">Item Details:</span>
|
|
327
|
+
{details.items.properties &&
|
|
328
|
+
Object.entries(details.items.properties).map(([itemPropName, itemPropDetails]: [string, any]) => (
|
|
329
|
+
<SchemaProperty
|
|
330
|
+
key={itemPropName}
|
|
331
|
+
name={itemPropName}
|
|
332
|
+
details={itemPropDetails}
|
|
333
|
+
isRequired={details.items.required?.includes(itemPropName) ?? false}
|
|
334
|
+
level={level + 1}
|
|
335
|
+
isListItem={true}
|
|
336
|
+
expand={expand}
|
|
337
|
+
/>
|
|
338
|
+
))}
|
|
339
|
+
{(details.items.allOf || details.items.oneOf || details.items.$ref) && !details.items.properties && (
|
|
340
|
+
<div className="text-xs text-gray-500 mt-1">
|
|
341
|
+
Complex array item schema detected. The properties should be processed by the parent SchemaViewer.
|
|
342
|
+
</div>
|
|
343
|
+
)}
|
|
344
|
+
</div>
|
|
345
|
+
)}
|
|
346
|
+
</div>
|
|
347
|
+
)}
|
|
348
|
+
</div>
|
|
349
|
+
</div>
|
|
350
|
+
</div>
|
|
351
|
+
);
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
// Main JSONSchemaViewer component
|
|
355
|
+
export default function JSONSchemaViewer({
|
|
356
|
+
schema,
|
|
357
|
+
title,
|
|
358
|
+
maxHeight,
|
|
359
|
+
expand = false,
|
|
360
|
+
search = true,
|
|
361
|
+
id,
|
|
362
|
+
onOpenFullscreen,
|
|
363
|
+
}: JSONSchemaViewerProps) {
|
|
364
|
+
// Convert string props to booleans (MDX passes strings)
|
|
365
|
+
const expandBool = expand === true || expand === 'true';
|
|
366
|
+
const searchBool = search !== false && search !== 'false';
|
|
367
|
+
|
|
368
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
369
|
+
const [expandAll, setExpandAll] = useState(expandBool);
|
|
370
|
+
const [selectedVariantIndex, setSelectedVariantIndex] = useState(0);
|
|
371
|
+
const [currentMatches, setCurrentMatches] = useState<HTMLElement[]>([]);
|
|
372
|
+
const [currentMatchIndex, setCurrentMatchIndex] = useState(-1);
|
|
373
|
+
const searchInputRef = useRef<HTMLInputElement>(null);
|
|
374
|
+
const propertiesContainerRef = useRef<HTMLDivElement>(null);
|
|
375
|
+
|
|
376
|
+
const processedSchema = useMemo(() => processSchema(schema), [schema]);
|
|
377
|
+
|
|
378
|
+
// Handle root-level array schemas
|
|
379
|
+
const { displaySchema, isRootArray } = useMemo(() => {
|
|
380
|
+
let display = processedSchema;
|
|
381
|
+
let isArray = false;
|
|
382
|
+
|
|
383
|
+
if (processedSchema.type === 'array' && processedSchema.items) {
|
|
384
|
+
isArray = true;
|
|
385
|
+
if (processedSchema.items.type === 'object' && processedSchema.items.properties) {
|
|
386
|
+
display = {
|
|
387
|
+
...processedSchema.items,
|
|
388
|
+
description: processedSchema.description || processedSchema.items.description,
|
|
389
|
+
_isRootArrayItem: true,
|
|
390
|
+
_rootArraySchema: processedSchema,
|
|
391
|
+
};
|
|
392
|
+
} else if (processedSchema.items.allOf || processedSchema.items.oneOf || processedSchema.items.$ref) {
|
|
393
|
+
display = {
|
|
394
|
+
...processSchema(processedSchema.items),
|
|
395
|
+
description: processedSchema.description || processedSchema.items.description,
|
|
396
|
+
_isRootArrayItem: true,
|
|
397
|
+
_rootArraySchema: processedSchema,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return { displaySchema: display, isRootArray: isArray };
|
|
403
|
+
}, [processedSchema]);
|
|
404
|
+
|
|
405
|
+
const { description, properties, required = [], variants } = displaySchema;
|
|
406
|
+
const totalProperties = useMemo(() => countProperties(displaySchema), [displaySchema]);
|
|
407
|
+
|
|
408
|
+
// Search functionality
|
|
409
|
+
useEffect(() => {
|
|
410
|
+
if (!propertiesContainerRef.current) return;
|
|
411
|
+
|
|
412
|
+
const propertyContainers = propertiesContainerRef.current.querySelectorAll('.property-container');
|
|
413
|
+
const matches: HTMLElement[] = [];
|
|
414
|
+
|
|
415
|
+
if (searchQuery === '') {
|
|
416
|
+
// Reset search
|
|
417
|
+
propertyContainers.forEach((container) => {
|
|
418
|
+
container.classList.remove('search-match', 'search-no-match', 'search-current-match', 'search-dimmed');
|
|
419
|
+
const nameEl = container.querySelector('.font-semibold');
|
|
420
|
+
if (nameEl) {
|
|
421
|
+
nameEl.innerHTML = nameEl.textContent || '';
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
setCurrentMatches([]);
|
|
425
|
+
setCurrentMatchIndex(-1);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const query = searchQuery.toLowerCase().trim();
|
|
430
|
+
|
|
431
|
+
propertyContainers.forEach((container) => {
|
|
432
|
+
const nameEl = container.querySelector('.font-semibold');
|
|
433
|
+
if (!nameEl) return;
|
|
434
|
+
|
|
435
|
+
const propName = (nameEl.textContent || '').toLowerCase();
|
|
436
|
+
|
|
437
|
+
if (propName.includes(query)) {
|
|
438
|
+
container.classList.add('search-match');
|
|
439
|
+
container.classList.remove('search-dimmed');
|
|
440
|
+
matches.push(container as HTMLElement);
|
|
441
|
+
|
|
442
|
+
// Highlight the search term
|
|
443
|
+
const regex = new RegExp(`(${query})`, 'gi');
|
|
444
|
+
nameEl.innerHTML = (nameEl.textContent || '').replace(regex, '<mark class="bg-yellow-200 rounded px-0.5">$1</mark>');
|
|
445
|
+
|
|
446
|
+
// Expand parent containers and remove dimming from them
|
|
447
|
+
let parent = container.parentElement;
|
|
448
|
+
while (parent && parent !== propertiesContainerRef.current) {
|
|
449
|
+
if (parent.classList.contains('nested-content') && parent.classList.contains('hidden')) {
|
|
450
|
+
const parentPropertyContainer = parent.closest('.property-container');
|
|
451
|
+
if (parentPropertyContainer) {
|
|
452
|
+
const toggleBtn = parentPropertyContainer.querySelector('.property-toggle');
|
|
453
|
+
if (toggleBtn && toggleBtn.getAttribute('aria-expanded') === 'false') {
|
|
454
|
+
(toggleBtn as HTMLButtonElement).click();
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
// Remove dimming from parent property containers so they're fully visible
|
|
459
|
+
if (parent.classList.contains('property-container')) {
|
|
460
|
+
parent.classList.remove('search-dimmed');
|
|
461
|
+
}
|
|
462
|
+
parent = parent.parentElement;
|
|
463
|
+
}
|
|
464
|
+
} else {
|
|
465
|
+
container.classList.remove('search-match', 'search-current-match');
|
|
466
|
+
container.classList.add('search-dimmed');
|
|
467
|
+
nameEl.innerHTML = nameEl.textContent || '';
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
setCurrentMatches(matches);
|
|
472
|
+
if (matches.length > 0) {
|
|
473
|
+
setCurrentMatchIndex(0);
|
|
474
|
+
matches[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
475
|
+
} else {
|
|
476
|
+
setCurrentMatchIndex(-1);
|
|
477
|
+
}
|
|
478
|
+
}, [searchQuery]);
|
|
479
|
+
|
|
480
|
+
// Update match highlighting
|
|
481
|
+
useEffect(() => {
|
|
482
|
+
currentMatches.forEach((match, index) => {
|
|
483
|
+
if (index === currentMatchIndex) {
|
|
484
|
+
match.classList.add('search-current-match');
|
|
485
|
+
} else {
|
|
486
|
+
match.classList.remove('search-current-match');
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
if (currentMatchIndex >= 0 && currentMatches[currentMatchIndex]) {
|
|
491
|
+
currentMatches[currentMatchIndex].scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
492
|
+
}
|
|
493
|
+
}, [currentMatchIndex, currentMatches]);
|
|
494
|
+
|
|
495
|
+
const handleExpandAll = () => {
|
|
496
|
+
setExpandAll(true);
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
const handleCollapseAll = () => {
|
|
500
|
+
setExpandAll(false);
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
const handlePrevMatch = () => {
|
|
504
|
+
if (currentMatchIndex > 0) {
|
|
505
|
+
setCurrentMatchIndex(currentMatchIndex - 1);
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
const handleNextMatch = () => {
|
|
510
|
+
if (currentMatchIndex < currentMatches.length - 1) {
|
|
511
|
+
setCurrentMatchIndex(currentMatchIndex + 1);
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
if (!schema) {
|
|
516
|
+
return (
|
|
517
|
+
<div className="flex items-center justify-center h-full text-gray-500 p-8">
|
|
518
|
+
<p className="text-sm">Unable to parse JSON schema</p>
|
|
519
|
+
</div>
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const containerStyle = maxHeight
|
|
524
|
+
? {
|
|
525
|
+
maxHeight: maxHeight.includes('px') ? maxHeight : `${maxHeight}px`,
|
|
526
|
+
minHeight: '15em',
|
|
527
|
+
}
|
|
528
|
+
: {};
|
|
529
|
+
|
|
530
|
+
// Use h-full when no maxHeight (SchemaExplorer context), otherwise size based on content (MDX context)
|
|
531
|
+
const heightClass = maxHeight ? '' : 'h-full';
|
|
532
|
+
const overflowClass = maxHeight ? 'overflow-hidden' : '';
|
|
533
|
+
|
|
534
|
+
return (
|
|
535
|
+
<div
|
|
536
|
+
id={id}
|
|
537
|
+
className={`${heightClass} ${overflowClass} flex flex-col bg-white border border-gray-100 rounded-md shadow-sm`}
|
|
538
|
+
style={containerStyle}
|
|
539
|
+
>
|
|
540
|
+
{/* Toolbar */}
|
|
541
|
+
{searchBool && (
|
|
542
|
+
<div className="flex-shrink-0 bg-white pt-4 px-4 mb-4 pb-3 border-b border-gray-100 shadow-sm">
|
|
543
|
+
<div className="flex flex-col sm:flex-row gap-3">
|
|
544
|
+
<div className="flex-1 relative">
|
|
545
|
+
<input
|
|
546
|
+
ref={searchInputRef}
|
|
547
|
+
type="text"
|
|
548
|
+
value={searchQuery}
|
|
549
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
550
|
+
placeholder="Search properties..."
|
|
551
|
+
className="w-full px-3 py-1.5 pr-20 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
552
|
+
onKeyDown={(e) => {
|
|
553
|
+
if (e.key === 'Enter') {
|
|
554
|
+
e.preventDefault();
|
|
555
|
+
if (e.shiftKey) {
|
|
556
|
+
handlePrevMatch();
|
|
557
|
+
} else {
|
|
558
|
+
handleNextMatch();
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}}
|
|
562
|
+
/>
|
|
563
|
+
<div className="absolute right-2 top-1/2 transform -translate-y-1/2 flex items-center gap-1">
|
|
564
|
+
<button
|
|
565
|
+
onClick={handlePrevMatch}
|
|
566
|
+
disabled={currentMatches.length === 0 || currentMatchIndex <= 0}
|
|
567
|
+
className="p-1 text-gray-400 hover:text-gray-600 disabled:opacity-30 disabled:cursor-not-allowed"
|
|
568
|
+
title="Previous match"
|
|
569
|
+
>
|
|
570
|
+
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
571
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 19l-7-7 7-7" />
|
|
572
|
+
</svg>
|
|
573
|
+
</button>
|
|
574
|
+
<button
|
|
575
|
+
onClick={handleNextMatch}
|
|
576
|
+
disabled={currentMatches.length === 0 || currentMatchIndex >= currentMatches.length - 1}
|
|
577
|
+
className="p-1 text-gray-400 hover:text-gray-600 disabled:opacity-30 disabled:cursor-not-allowed"
|
|
578
|
+
title="Next match"
|
|
579
|
+
>
|
|
580
|
+
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
581
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5l7 7-7 7" />
|
|
582
|
+
</svg>
|
|
583
|
+
</button>
|
|
584
|
+
</div>
|
|
585
|
+
</div>
|
|
586
|
+
<div className="flex items-center gap-2">
|
|
587
|
+
{onOpenFullscreen && (
|
|
588
|
+
<button
|
|
589
|
+
onClick={onOpenFullscreen}
|
|
590
|
+
className="px-3 py-1.5 text-xs font-medium text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-500"
|
|
591
|
+
title="Open in fullscreen"
|
|
592
|
+
>
|
|
593
|
+
<svg
|
|
594
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
595
|
+
className="h-3.5 w-3.5 inline-block"
|
|
596
|
+
fill="none"
|
|
597
|
+
viewBox="0 0 24 24"
|
|
598
|
+
stroke="currentColor"
|
|
599
|
+
>
|
|
600
|
+
<path
|
|
601
|
+
strokeLinecap="round"
|
|
602
|
+
strokeLinejoin="round"
|
|
603
|
+
strokeWidth={2}
|
|
604
|
+
d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"
|
|
605
|
+
/>
|
|
606
|
+
</svg>
|
|
607
|
+
</button>
|
|
608
|
+
)}
|
|
609
|
+
<button
|
|
610
|
+
onClick={handleExpandAll}
|
|
611
|
+
className="px-3 py-1.5 text-xs font-medium text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-500"
|
|
612
|
+
>
|
|
613
|
+
Expand All
|
|
614
|
+
</button>
|
|
615
|
+
<button
|
|
616
|
+
onClick={handleCollapseAll}
|
|
617
|
+
className="px-3 py-1.5 text-xs font-medium text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-500"
|
|
618
|
+
>
|
|
619
|
+
Collapse All
|
|
620
|
+
</button>
|
|
621
|
+
<div className="text-xs text-gray-500">
|
|
622
|
+
{totalProperties} {totalProperties === 1 ? 'property' : 'properties'}
|
|
623
|
+
</div>
|
|
624
|
+
</div>
|
|
625
|
+
</div>
|
|
626
|
+
{searchQuery && (
|
|
627
|
+
<div className="mt-2 text-xs text-gray-600">
|
|
628
|
+
{currentMatches.length > 0
|
|
629
|
+
? `${currentMatchIndex + 1} of ${currentMatches.length} ${currentMatches.length === 1 ? 'match' : 'matches'}`
|
|
630
|
+
: 'No properties found'}
|
|
631
|
+
</div>
|
|
632
|
+
)}
|
|
633
|
+
</div>
|
|
634
|
+
)}
|
|
635
|
+
|
|
636
|
+
{/* Content */}
|
|
637
|
+
<div className="flex-1 px-4 pb-4 overflow-auto">
|
|
638
|
+
{isRootArray && (
|
|
639
|
+
<div className="mb-4 p-3 bg-blue-50 border border-blue-200 rounded-md">
|
|
640
|
+
<div className="flex items-center space-x-2">
|
|
641
|
+
<span className="text-blue-600 font-medium text-sm">Array Schema</span>
|
|
642
|
+
<span className="text-blue-500 font-mono text-xs">array[object]</span>
|
|
643
|
+
</div>
|
|
644
|
+
<p className="text-blue-700 text-xs mt-1">
|
|
645
|
+
This schema defines an array of objects. Each item in the array has the properties shown below.
|
|
646
|
+
</p>
|
|
647
|
+
</div>
|
|
648
|
+
)}
|
|
649
|
+
{description && <p className="text-gray-600 text-xs mb-5">{description}</p>}
|
|
650
|
+
|
|
651
|
+
{variants && (
|
|
652
|
+
<div className="mb-4">
|
|
653
|
+
<div className="flex items-center space-x-2">
|
|
654
|
+
<span className="text-sm text-gray-600">(one of)</span>
|
|
655
|
+
<select
|
|
656
|
+
value={selectedVariantIndex}
|
|
657
|
+
onChange={(e) => setSelectedVariantIndex(parseInt(e.target.value))}
|
|
658
|
+
className="form-select text-sm border-gray-300 rounded-md shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
|
|
659
|
+
>
|
|
660
|
+
{variants.map((variant: any, index: number) => (
|
|
661
|
+
<option key={index} value={index}>
|
|
662
|
+
{variant.title}
|
|
663
|
+
</option>
|
|
664
|
+
))}
|
|
665
|
+
</select>
|
|
666
|
+
</div>
|
|
667
|
+
</div>
|
|
668
|
+
)}
|
|
669
|
+
|
|
670
|
+
{properties ? (
|
|
671
|
+
<div ref={propertiesContainerRef}>
|
|
672
|
+
{Object.entries(properties).map(([name, details]: [string, any]) => (
|
|
673
|
+
<SchemaProperty
|
|
674
|
+
key={name}
|
|
675
|
+
name={name}
|
|
676
|
+
details={details}
|
|
677
|
+
isRequired={
|
|
678
|
+
variants ? variants[selectedVariantIndex]?.required?.includes(name) || false : required.includes(name)
|
|
679
|
+
}
|
|
680
|
+
level={0}
|
|
681
|
+
expand={expandAll}
|
|
682
|
+
/>
|
|
683
|
+
))}
|
|
684
|
+
</div>
|
|
685
|
+
) : !isRootArray ? (
|
|
686
|
+
<p className="text-gray-500 text-sm">Schema does not contain any properties.</p>
|
|
687
|
+
) : (
|
|
688
|
+
<div className="text-center py-8">
|
|
689
|
+
<div className="text-gray-500 text-sm">
|
|
690
|
+
<p>
|
|
691
|
+
This array contains items of type:{' '}
|
|
692
|
+
<span className="font-mono text-blue-600">{processedSchema.items?.type || 'unknown'}</span>
|
|
693
|
+
</p>
|
|
694
|
+
{processedSchema.items?.description && (
|
|
695
|
+
<p className="text-xs mt-2 text-gray-600">{processedSchema.items.description}</p>
|
|
696
|
+
)}
|
|
697
|
+
</div>
|
|
698
|
+
</div>
|
|
699
|
+
)}
|
|
700
|
+
|
|
701
|
+
{searchQuery && currentMatches.length === 0 && (
|
|
702
|
+
<div className="text-center py-8">
|
|
703
|
+
<div className="text-gray-400 text-sm">
|
|
704
|
+
<svg className="mx-auto h-12 w-12 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
705
|
+
<path
|
|
706
|
+
strokeLinecap="round"
|
|
707
|
+
strokeLinejoin="round"
|
|
708
|
+
strokeWidth="2"
|
|
709
|
+
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
|
710
|
+
/>
|
|
711
|
+
</svg>
|
|
712
|
+
<p>No properties match your search</p>
|
|
713
|
+
<p className="text-xs mt-1">Try a different search term or clear the search to see all properties</p>
|
|
714
|
+
</div>
|
|
715
|
+
</div>
|
|
716
|
+
)}
|
|
717
|
+
</div>
|
|
718
|
+
|
|
719
|
+
<style>{`
|
|
720
|
+
.search-dimmed {
|
|
721
|
+
opacity: 0.4;
|
|
722
|
+
transition: opacity 0.2s ease;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
.search-match {
|
|
726
|
+
opacity: 1;
|
|
727
|
+
transition: opacity 0.2s ease;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
.search-current-match {
|
|
731
|
+
background-color: rgba(59, 130, 246, 0.1);
|
|
732
|
+
border-left: 3px solid #3b82f6;
|
|
733
|
+
padding-left: 8px;
|
|
734
|
+
margin-left: -11px;
|
|
735
|
+
transition: all 0.2s ease;
|
|
736
|
+
}
|
|
737
|
+
`}</style>
|
|
738
|
+
</div>
|
|
739
|
+
);
|
|
740
|
+
}
|