@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,51 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
import { getCollection } from 'astro:content';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import { getSpecificationsForService } from '@utils/collections/services';
|
|
6
|
+
import { isEventCatalogScaleEnabled } from '@utils/feature';
|
|
7
|
+
|
|
8
|
+
export async function getStaticPaths() {
|
|
9
|
+
const services = await getCollection('services');
|
|
10
|
+
const servicesWithSpecifications = services.filter((service) => getSpecificationsForService(service).length > 0);
|
|
11
|
+
return servicesWithSpecifications.reduce<
|
|
12
|
+
{ params: { collection: string; id: string; version: string; specification: string }; props: { schema: string } }[]
|
|
13
|
+
>(
|
|
14
|
+
(acc, service) => {
|
|
15
|
+
const specifications = getSpecificationsForService(service);
|
|
16
|
+
return [
|
|
17
|
+
...acc,
|
|
18
|
+
...specifications.map((specification) => ({
|
|
19
|
+
params: {
|
|
20
|
+
collection: service.collection,
|
|
21
|
+
id: service.data.id,
|
|
22
|
+
version: service.data.version,
|
|
23
|
+
specification: specification.type,
|
|
24
|
+
},
|
|
25
|
+
props: {
|
|
26
|
+
schema: fs.readFileSync(path.join(path.dirname(service.filePath ?? ''), specification.path ?? ''), 'utf8'),
|
|
27
|
+
},
|
|
28
|
+
})),
|
|
29
|
+
];
|
|
30
|
+
},
|
|
31
|
+
[] as { params: { collection: string; id: string; version: string; specification: string }; props: { schema: string } }[]
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const GET: APIRoute = async ({ props }) => {
|
|
36
|
+
if (!isEventCatalogScaleEnabled()) {
|
|
37
|
+
return new Response(
|
|
38
|
+
JSON.stringify({
|
|
39
|
+
error: 'feature_not_available_on_server',
|
|
40
|
+
message: 'Schema API is not enabled for this deployment and supported in EventCatalog Scale.',
|
|
41
|
+
}),
|
|
42
|
+
{
|
|
43
|
+
status: 501,
|
|
44
|
+
headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' },
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
return new Response(props.schema, {
|
|
49
|
+
headers: { 'Content-Type': 'text/plain' },
|
|
50
|
+
});
|
|
51
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { getCollection } from 'astro:content';
|
|
2
|
+
import config from '@config';
|
|
3
|
+
import type { APIRoute } from 'astro';
|
|
4
|
+
import type { CollectionEntry } from 'astro:content';
|
|
5
|
+
import { getSpecificationsForService } from '@utils/collections/services';
|
|
6
|
+
import { isEventCatalogScaleEnabled } from '@utils/feature';
|
|
7
|
+
|
|
8
|
+
const events = await getCollection('events');
|
|
9
|
+
const commands = await getCollection('commands');
|
|
10
|
+
const queries = await getCollection('queries');
|
|
11
|
+
const services = await getCollection('services');
|
|
12
|
+
|
|
13
|
+
type ServiceWithSchema = {
|
|
14
|
+
collection: string;
|
|
15
|
+
id: string;
|
|
16
|
+
version: string;
|
|
17
|
+
specification: string;
|
|
18
|
+
summary: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const servicesWithSchemas = services.filter((service) => getSpecificationsForService(service).length > 0);
|
|
22
|
+
|
|
23
|
+
const servicesWithSchemasFlat = servicesWithSchemas.reduce<ServiceWithSchema[]>((acc, service) => {
|
|
24
|
+
return [
|
|
25
|
+
...acc,
|
|
26
|
+
...getSpecificationsForService(service).map((specification) => ({
|
|
27
|
+
collection: 'services',
|
|
28
|
+
id: service.data.id,
|
|
29
|
+
version: service.data.version,
|
|
30
|
+
specification: specification.type,
|
|
31
|
+
summary: service.data.summary?.trim() || '',
|
|
32
|
+
})),
|
|
33
|
+
];
|
|
34
|
+
}, []) as ServiceWithSchema[];
|
|
35
|
+
|
|
36
|
+
const messageHasSchema = (message: CollectionEntry<'events' | 'commands' | 'queries'>) => {
|
|
37
|
+
return message.data.schemaPath;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const GET: APIRoute = async ({ params, request }) => {
|
|
41
|
+
if (!isEventCatalogScaleEnabled()) {
|
|
42
|
+
return new Response(
|
|
43
|
+
JSON.stringify({
|
|
44
|
+
error: 'feature_not_available_on_server',
|
|
45
|
+
message: 'Schema API is not enabled for this deployment and supported in EventCatalog Scale.',
|
|
46
|
+
}),
|
|
47
|
+
{ status: 501, headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' } }
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const url = new URL(request.url);
|
|
52
|
+
const baseUrl = process.env.LLMS_TXT_BASE_URL || `${url.origin}`;
|
|
53
|
+
|
|
54
|
+
const formatVersionedItem = (item: any, type: string, extraParams?: string | string[]) => {
|
|
55
|
+
return `- [${item.data.name} - ${item.data.id} - ${item.data.version}](${baseUrl}/api/schemas/${type}/${item.data.id}/${item.data.version})} ${item.data.summary ? `- ${item.data.summary.trim()}` : ''}`;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const formatServiceWithSchema = (item: ServiceWithSchema) => {
|
|
59
|
+
return `- [${item.id} - ${item.version} - ${item.specification} specification](${baseUrl}/api/schemas/${item.collection}/${item.id}/${item.version}/${item.specification}) ${item.summary ? `- Specification for ${item.summary}` : ''}`;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const content = [
|
|
63
|
+
`# ${config.organizationName} EventCatalog Schemas`,
|
|
64
|
+
`List of schemas for events, commands, queries, and services in EventCatalog.`,
|
|
65
|
+
'',
|
|
66
|
+
`## Events\n${events
|
|
67
|
+
.filter(messageHasSchema)
|
|
68
|
+
.map((item) => formatVersionedItem(item, 'events'))
|
|
69
|
+
.join('\n')}`,
|
|
70
|
+
'',
|
|
71
|
+
`## Commands\n${commands
|
|
72
|
+
.filter(messageHasSchema)
|
|
73
|
+
.map((item) => formatVersionedItem(item, 'commands'))
|
|
74
|
+
.join('\n')}`,
|
|
75
|
+
'',
|
|
76
|
+
`## Queries\n${queries
|
|
77
|
+
.filter(messageHasSchema)
|
|
78
|
+
.map((item) => formatVersionedItem(item, 'queries'))
|
|
79
|
+
.join('\n')}`,
|
|
80
|
+
'',
|
|
81
|
+
`## Services\n${servicesWithSchemasFlat.map((item: any) => formatServiceWithSchema(item)).join('\n')}`,
|
|
82
|
+
].join('\n');
|
|
83
|
+
return new Response(content, {
|
|
84
|
+
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
|
|
85
|
+
});
|
|
86
|
+
};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
---
|
|
2
|
+
import VerticalSideBarLayout from '@layouts/VerticalSideBarLayout.astro';
|
|
3
|
+
import { getEvents } from '@utils/events';
|
|
4
|
+
import { getCommands } from '@utils/commands';
|
|
5
|
+
import { getQueries } from '@utils/queries';
|
|
6
|
+
import { getServices, getSpecificationsForService } from '@utils/collections/services';
|
|
7
|
+
import SchemaExplorer from '@components/SchemaExplorer/SchemaExplorer';
|
|
8
|
+
import { isEventCatalogScaleEnabled } from '@utils/feature';
|
|
9
|
+
import { getOwner } from '@utils/collections/owners';
|
|
10
|
+
import { buildUrl } from '@utils/url-builder';
|
|
11
|
+
import fs from 'fs';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
|
|
14
|
+
// Fetch all messages
|
|
15
|
+
const events = await getEvents({ getAllVersions: true });
|
|
16
|
+
const commands = await getCommands({ getAllVersions: true });
|
|
17
|
+
const queries = await getQueries({ getAllVersions: true });
|
|
18
|
+
|
|
19
|
+
// Fetch all services
|
|
20
|
+
const services = await getServices({ getAllVersions: true });
|
|
21
|
+
|
|
22
|
+
// Combine all messages
|
|
23
|
+
const allMessages = [...events, ...commands, ...queries];
|
|
24
|
+
|
|
25
|
+
// Helper function to enrich owners with full details
|
|
26
|
+
async function enrichOwners(ownersRaw: any[]) {
|
|
27
|
+
if (!ownersRaw || ownersRaw.length === 0) return [];
|
|
28
|
+
|
|
29
|
+
const owners = await Promise.all(ownersRaw.map(getOwner));
|
|
30
|
+
const filteredOwners = owners.filter((o) => o !== undefined);
|
|
31
|
+
|
|
32
|
+
return filteredOwners.map((o) => ({
|
|
33
|
+
id: o.data.id,
|
|
34
|
+
name: o.data.name,
|
|
35
|
+
type: o.collection,
|
|
36
|
+
href: buildUrl(`/docs/${o.collection}/${o.data.id}`),
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Filter messages with schemas and read schema content - only keep essential data
|
|
41
|
+
const messagesWithSchemas = await Promise.all(
|
|
42
|
+
allMessages
|
|
43
|
+
.filter((message) => message.data.schemaPath)
|
|
44
|
+
// Make sure the file exists
|
|
45
|
+
.filter((message) => fs.existsSync(path.join(path.dirname(message.filePath ?? ''), message.data.schemaPath ?? '')))
|
|
46
|
+
.map(async (message) => {
|
|
47
|
+
try {
|
|
48
|
+
// Get the schema file path
|
|
49
|
+
const schemaPath = message.data.schemaPath;
|
|
50
|
+
const fullSchemaPath = path.join(path.dirname(message.filePath ?? ''), schemaPath ?? '');
|
|
51
|
+
|
|
52
|
+
// Read the schema content
|
|
53
|
+
let schemaContent = '';
|
|
54
|
+
if (fs.existsSync(fullSchemaPath)) {
|
|
55
|
+
schemaContent = fs.readFileSync(fullSchemaPath, 'utf-8');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Get schema file extension
|
|
59
|
+
const schemaExtension = path.extname(schemaPath ?? '').slice(1);
|
|
60
|
+
|
|
61
|
+
// Enrich owners with full details
|
|
62
|
+
const enrichedOwners = await enrichOwners(message.data.owners || []);
|
|
63
|
+
|
|
64
|
+
// Only return essential data - strip out markdown, full data objects, etc.
|
|
65
|
+
return {
|
|
66
|
+
collection: message.collection,
|
|
67
|
+
data: {
|
|
68
|
+
id: message.data.id,
|
|
69
|
+
name: message.data.name,
|
|
70
|
+
version: message.data.version,
|
|
71
|
+
summary: message.data.summary,
|
|
72
|
+
schemaPath: message.data.schemaPath,
|
|
73
|
+
producers: message.data.producers || [],
|
|
74
|
+
consumers: message.data.consumers || [],
|
|
75
|
+
owners: enrichedOwners,
|
|
76
|
+
},
|
|
77
|
+
schemaContent,
|
|
78
|
+
schemaExtension,
|
|
79
|
+
};
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error(`Error reading schema for ${message.data.id}:`, error);
|
|
82
|
+
const enrichedOwners = await enrichOwners(message.data.owners || []);
|
|
83
|
+
return {
|
|
84
|
+
collection: message.collection,
|
|
85
|
+
data: {
|
|
86
|
+
id: message.data.id,
|
|
87
|
+
name: message.data.name,
|
|
88
|
+
version: message.data.version,
|
|
89
|
+
summary: message.data.summary,
|
|
90
|
+
schemaPath: message.data.schemaPath,
|
|
91
|
+
producers: message.data.producers || [],
|
|
92
|
+
consumers: message.data.consumers || [],
|
|
93
|
+
owners: enrichedOwners,
|
|
94
|
+
},
|
|
95
|
+
schemaContent: '',
|
|
96
|
+
schemaExtension: 'json',
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
// Filter services with specifications and read spec content - only keep essential data
|
|
103
|
+
const servicesWithSpecs = await Promise.all(
|
|
104
|
+
services.map(async (service) => {
|
|
105
|
+
try {
|
|
106
|
+
const specifications = getSpecificationsForService(service);
|
|
107
|
+
|
|
108
|
+
// Only include services that have specifications
|
|
109
|
+
if (specifications.length === 0) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Process each specification file for this service
|
|
114
|
+
return await Promise.all(
|
|
115
|
+
specifications.map(async (spec) => {
|
|
116
|
+
const specPath = path.join(path.dirname(service.filePath ?? ''), spec.path);
|
|
117
|
+
|
|
118
|
+
// Only include if the spec file exists
|
|
119
|
+
if (!fs.existsSync(specPath)) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const schemaContent = fs.readFileSync(specPath, 'utf-8');
|
|
124
|
+
// Use spec type (openapi, asyncapi) as the extension for proper labeling
|
|
125
|
+
const schemaExtension = spec.type;
|
|
126
|
+
|
|
127
|
+
// Enrich owners with full details
|
|
128
|
+
const enrichedOwners = await enrichOwners(service.data.owners || []);
|
|
129
|
+
|
|
130
|
+
// Only return essential data - strip out markdown, sends/receives, entities, etc.
|
|
131
|
+
return {
|
|
132
|
+
collection: 'services',
|
|
133
|
+
data: {
|
|
134
|
+
id: `${service.data.id}`,
|
|
135
|
+
name: `${service.data.name} - ${spec.name}`,
|
|
136
|
+
version: service.data.version,
|
|
137
|
+
summary: service.data.summary,
|
|
138
|
+
schemaPath: spec.path,
|
|
139
|
+
owners: enrichedOwners,
|
|
140
|
+
},
|
|
141
|
+
schemaContent,
|
|
142
|
+
schemaExtension,
|
|
143
|
+
specType: spec.type,
|
|
144
|
+
specName: spec.name,
|
|
145
|
+
specFilenameWithoutExtension: spec.filenameWithoutExtension,
|
|
146
|
+
};
|
|
147
|
+
})
|
|
148
|
+
);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
console.error(`Error reading specifications for service ${service.data.id}:`, error);
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
})
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
// Flatten and filter out null values
|
|
157
|
+
const flatServicesWithSpecs = servicesWithSpecs.flat().filter((service) => service !== null);
|
|
158
|
+
|
|
159
|
+
// Combine messages and services
|
|
160
|
+
const allSchemas = [...messagesWithSchemas, ...flatServicesWithSpecs];
|
|
161
|
+
|
|
162
|
+
const apiAccessEnabled = isEventCatalogScaleEnabled();
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
<VerticalSideBarLayout title="Schema Explorer - EventCatalog">
|
|
166
|
+
<main class="flex sm:px-8 docs-layout h-[calc(100vh-var(--header-height,0px)-64px)]">
|
|
167
|
+
<div class="flex docs-layout w-full h-full">
|
|
168
|
+
<div class="w-full lg:mr-2 pr-8 py-6 flex flex-col h-full">
|
|
169
|
+
<div class="w-full !max-w-none h-full flex flex-col overflow-hidden">
|
|
170
|
+
<SchemaExplorer client:load schemas={allSchemas as any} apiAccessEnabled={apiAccessEnabled} />
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
</main>
|
|
175
|
+
</VerticalSideBarLayout>
|
|
@@ -50,3 +50,12 @@ export const getAbsoluteFilePathForAstroFile = (filePath: string, fileName?: str
|
|
|
50
50
|
|
|
51
51
|
return resolveProjectPath(filePath, PROJECT_DIR);
|
|
52
52
|
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Checks if a file path is an Avro schema based on its extension
|
|
56
|
+
* @param filePath - The file path to check
|
|
57
|
+
* @returns True if the file is an Avro schema (.avro or .avsc)
|
|
58
|
+
*/
|
|
59
|
+
export const isAvroSchema = (filePath: string): boolean => {
|
|
60
|
+
return filePath.endsWith('.avro') || filePath.endsWith('.avsc');
|
|
61
|
+
};
|
package/package.json
CHANGED
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
import SchemaProp from './SchemaProperty.astro';
|
|
3
|
-
|
|
4
|
-
interface Props {
|
|
5
|
-
name: string;
|
|
6
|
-
details: any;
|
|
7
|
-
isRequired: boolean;
|
|
8
|
-
level: number;
|
|
9
|
-
isListItem?: boolean;
|
|
10
|
-
expand?: boolean;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const { name, details, isRequired, level, isListItem = false, expand = false } = Astro.props;
|
|
14
|
-
|
|
15
|
-
const hasNestedProperties = details.type === 'object' && details.properties && Object.keys(details.properties).length > 0;
|
|
16
|
-
const hasArrayItems = details.type === 'array' && details.items;
|
|
17
|
-
const hasArrayItemProperties =
|
|
18
|
-
hasArrayItems &&
|
|
19
|
-
((details.items.type === 'object' && details.items.properties) ||
|
|
20
|
-
details.items.allOf ||
|
|
21
|
-
details.items.oneOf ||
|
|
22
|
-
details.items.$ref);
|
|
23
|
-
const isCollapsible = hasNestedProperties || hasArrayItemProperties;
|
|
24
|
-
|
|
25
|
-
// Using template literal for class calculation remains safe
|
|
26
|
-
const indentationClass = `pl-${level * 3}`;
|
|
27
|
-
|
|
28
|
-
const contentId = `prop-${name}-${level}-${Math.random().toString(36).substring(2, 7)}`;
|
|
29
|
-
---
|
|
30
|
-
|
|
31
|
-
{/* Using simpler border class */}
|
|
32
|
-
<div class:list={['property-container mb-1.5 border-l border-gray-100 relative', indentationClass]}>
|
|
33
|
-
<div class="flex items-start space-x-1.5">
|
|
34
|
-
{
|
|
35
|
-
isCollapsible && (
|
|
36
|
-
<button
|
|
37
|
-
type="button"
|
|
38
|
-
aria-expanded={expand ? 'true' : 'false'}
|
|
39
|
-
aria-controls={contentId}
|
|
40
|
-
class="property-toggle text-gray-500 hover:text-gray-700 pt-0.5 focus:outline-none w-3 text-center flex-shrink-0"
|
|
41
|
-
>
|
|
42
|
-
<span class:list={['icon-collapsed font-mono text-xs', { hidden: expand }]}>></span>
|
|
43
|
-
<span class:list={['icon-expanded font-mono text-xs', { hidden: !expand }]}>v</span>
|
|
44
|
-
</button>
|
|
45
|
-
)
|
|
46
|
-
}
|
|
47
|
-
{!isCollapsible && <div class="w-3 h-4 flex-shrink-0" />}
|
|
48
|
-
|
|
49
|
-
<div class="flex-grow">
|
|
50
|
-
<div class="flex justify-between items-baseline">
|
|
51
|
-
<div>
|
|
52
|
-
<span class="font-semibold text-gray-800 text-sm">{name}</span>
|
|
53
|
-
<span class="ml-1.5 text-purple-600 font-mono text-xs">
|
|
54
|
-
{/* Expressions using ternary operators are generally safe */}
|
|
55
|
-
{details.type}
|
|
56
|
-
{details.type === 'array' && details.items?.type ? `[${details.items.type}]` : ''}
|
|
57
|
-
{details.format ? `<${details.format}>` : ''}
|
|
58
|
-
{details._refPath && <span class="text-blue-600 ml-1">→ {details._refName || details._refPath}</span>}
|
|
59
|
-
{details._refNotFound && <span class="text-red-600 ml-1">❌ ref not found</span>}
|
|
60
|
-
{
|
|
61
|
-
details.const !== undefined && (
|
|
62
|
-
<span>
|
|
63
|
-
constant: <code>{details.const}</code>
|
|
64
|
-
</span>
|
|
65
|
-
)
|
|
66
|
-
}
|
|
67
|
-
</span>
|
|
68
|
-
</div>
|
|
69
|
-
{isRequired && <span class="text-red-600 text-xs ml-3 flex-shrink-0">required</span>}
|
|
70
|
-
</div>
|
|
71
|
-
|
|
72
|
-
{details.description && <p class="text-gray-500 text-xs mt-0.5">{details.description}</p>}
|
|
73
|
-
{
|
|
74
|
-
details.title && details.title !== details.description && (
|
|
75
|
-
<p class="text-gray-500 text-xs mt-0.5 italic">Title: {details.title}</p>
|
|
76
|
-
)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
{/* Reverted arbitrary text size to standard 'text-xs' */}
|
|
80
|
-
<div class="text-xs text-gray-500 mt-0.5 space-y-0">
|
|
81
|
-
{
|
|
82
|
-
details.pattern && (
|
|
83
|
-
<div>
|
|
84
|
-
Match pattern: <code class="bg-gray-100 px-1 rounded text-gray-800 font-thin py-0.5">{details.pattern}</code>
|
|
85
|
-
</div>
|
|
86
|
-
)
|
|
87
|
-
}
|
|
88
|
-
{
|
|
89
|
-
details.minimum !== undefined && (
|
|
90
|
-
<div>
|
|
91
|
-
Minimum: <code class="bg-gray-100 px-1 rounded text-gray-800 font-thin py-0.5">{details.minimum}</code>
|
|
92
|
-
</div>
|
|
93
|
-
)
|
|
94
|
-
}
|
|
95
|
-
{
|
|
96
|
-
details.maximum !== undefined && (
|
|
97
|
-
<div>
|
|
98
|
-
Maximum: <code class="bg-gray-100 px-1 rounded text-gray-800 font-thin py-0.5">{details.maximum}</code>
|
|
99
|
-
</div>
|
|
100
|
-
)
|
|
101
|
-
}
|
|
102
|
-
{
|
|
103
|
-
details.minLength !== undefined && (
|
|
104
|
-
<div>
|
|
105
|
-
Min length: <code class="bg-gray-100 px-1 rounded text-gray-800 font-thin py-0.5">{details.minLength}</code>
|
|
106
|
-
</div>
|
|
107
|
-
)
|
|
108
|
-
}
|
|
109
|
-
{
|
|
110
|
-
details.maxLength !== undefined && (
|
|
111
|
-
<div>
|
|
112
|
-
Max length: <code class="bg-gray-100 px-1 rounded text-gray-800 font-thin py-0.5">{details.maxLength}</code>
|
|
113
|
-
</div>
|
|
114
|
-
)
|
|
115
|
-
}
|
|
116
|
-
{
|
|
117
|
-
details.enum && (
|
|
118
|
-
<div>
|
|
119
|
-
<span class="text-xs inline-block">Allowed values:</span>
|
|
120
|
-
|
|
121
|
-
{/* Map function rendering standard elements */}
|
|
122
|
-
{details.enum.map((val: string) => (
|
|
123
|
-
<span class="text-xs">
|
|
124
|
-
<code class="bg-gray-100 px-1 rounded text-gray-800 font-thin py-0.5">{val}</code>
|
|
125
|
-
</span>
|
|
126
|
-
))}
|
|
127
|
-
</div>
|
|
128
|
-
)
|
|
129
|
-
}
|
|
130
|
-
</div>
|
|
131
|
-
|
|
132
|
-
{
|
|
133
|
-
(hasNestedProperties || hasArrayItems) && (
|
|
134
|
-
// class:list with conditional object is standard and should be safe
|
|
135
|
-
<div id={contentId} class:list={['nested-content mt-1', { hidden: isCollapsible && !expand }]}>
|
|
136
|
-
{/* Recursive component calls */}
|
|
137
|
-
{hasNestedProperties &&
|
|
138
|
-
details.properties &&
|
|
139
|
-
Object.entries(details.properties).map(([nestedName, nestedDetails]) => (
|
|
140
|
-
<SchemaProp
|
|
141
|
-
name={nestedName}
|
|
142
|
-
details={nestedDetails}
|
|
143
|
-
isRequired={details.required?.includes(nestedName) ?? false}
|
|
144
|
-
level={level + 1}
|
|
145
|
-
expand={expand}
|
|
146
|
-
/>
|
|
147
|
-
))}
|
|
148
|
-
|
|
149
|
-
{hasArrayItemProperties && (
|
|
150
|
-
<div class="mt-1 border-l border-dashed border-gray-400 pl-3 ml-1.5">
|
|
151
|
-
<span class="text-xs italic text-gray-500 block mb-1">Item Details:</span>
|
|
152
|
-
{details.items.properties &&
|
|
153
|
-
Object.entries(details.items.properties).map(([itemPropName, itemPropDetails]) => (
|
|
154
|
-
<SchemaProp
|
|
155
|
-
name={itemPropName}
|
|
156
|
-
details={itemPropDetails}
|
|
157
|
-
isRequired={details.items.required?.includes(itemPropName) ?? false}
|
|
158
|
-
level={level + 1}
|
|
159
|
-
isListItem={true}
|
|
160
|
-
expand={expand}
|
|
161
|
-
/>
|
|
162
|
-
))}
|
|
163
|
-
{(details.items.allOf || details.items.oneOf || details.items.$ref) && !details.items.properties && (
|
|
164
|
-
<div class="text-xs text-gray-500 mt-1">
|
|
165
|
-
Complex array item schema detected. The properties should be processed by the parent SchemaViewer.
|
|
166
|
-
</div>
|
|
167
|
-
)}
|
|
168
|
-
</div>
|
|
169
|
-
)}
|
|
170
|
-
</div>
|
|
171
|
-
)
|
|
172
|
-
}
|
|
173
|
-
</div>
|
|
174
|
-
</div>
|
|
175
|
-
</div>
|
|
176
|
-
|
|
177
|
-
{/* Script tag content remains the same */}
|
|
178
|
-
<script is:inline>
|
|
179
|
-
function setupPropertyToggle() {
|
|
180
|
-
document.querySelectorAll('.property-toggle').forEach((button) => {
|
|
181
|
-
if (button.dataset.listenerAttached) return;
|
|
182
|
-
button.dataset.listenerAttached = 'true';
|
|
183
|
-
|
|
184
|
-
button.addEventListener('click', () => {
|
|
185
|
-
const contentId = button.getAttribute('aria-controls');
|
|
186
|
-
const content = document.getElementById(contentId);
|
|
187
|
-
const isExpanded = button.getAttribute('aria-expanded') === 'true';
|
|
188
|
-
const iconCollapsed = button.querySelector('.icon-collapsed');
|
|
189
|
-
const iconExpanded = button.querySelector('.icon-expanded');
|
|
190
|
-
|
|
191
|
-
if (content) {
|
|
192
|
-
button.setAttribute('aria-expanded', String(!isExpanded));
|
|
193
|
-
content.classList.toggle('hidden');
|
|
194
|
-
iconCollapsed?.classList.toggle('hidden', !isExpanded);
|
|
195
|
-
iconExpanded?.classList.toggle('hidden', isExpanded);
|
|
196
|
-
}
|
|
197
|
-
});
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
setupPropertyToggle();
|
|
202
|
-
|
|
203
|
-
document.addEventListener('astro:page-load', setupPropertyToggle);
|
|
204
|
-
</script>
|