@eventcatalog/core 4.10.13 → 4.10.15
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-SSSN5FXC.js → chunk-7LWOCX4R.js} +1 -1
- package/dist/{chunk-IIECZFXN.js → chunk-A4JRWRCV.js} +1 -1
- package/dist/{chunk-JHUICVBT.js → chunk-HBVTU6ZP.js} +1 -1
- package/dist/{chunk-PADMH2RJ.js → chunk-I6HUOX4R.js} +1 -1
- package/dist/{chunk-34RMFKFB.js → chunk-Q2ATWE7N.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/docs/api/02-config.md +60 -6
- package/dist/docs/development/ask-your-architecture/02-eventcatalog-assistant/02-configuration.md +18 -11
- package/dist/docs/development/authentication/providers/03-setting-up-github.md +29 -0
- package/dist/docs/development/bring-your-own-documentation/custom-pages/02-adding-custom-docs.md +2 -2
- package/dist/docs/development/bring-your-own-documentation/custom-pages/03-components.md +17 -0
- package/dist/docs/development/components/07-resource-references.md +7 -1
- package/dist/docs/development/components/components/05-architecture-graph.md +65 -0
- package/dist/docs/development/components/components/{05-attachments.md → 06-attachments.md} +1 -1
- package/dist/docs/development/components/components/{06-channel-information.md → 07-channel-information.md} +1 -1
- package/dist/docs/development/components/components/{07-custom-properties.md → 08-custom-properties.md} +1 -1
- package/dist/docs/development/components/components/{08-design.md → 09-design.md} +1 -1
- package/dist/docs/development/components/components/{09-entitymap.md → 10-entitymap.md} +1 -1
- package/dist/docs/development/components/components/{10-flow.md → 11-flow.md} +1 -1
- package/dist/docs/development/components/components/{11-link.md → 12-link.md} +1 -1
- package/dist/docs/development/components/components/{12-mermaid-file-loader.md → 13-mermaid-file-loader.md} +13 -1
- package/dist/docs/development/components/components/{13-message-table.md → 14-message-table.md} +1 -1
- package/dist/docs/development/components/components/{14-nodegraph.md → 15-nodegraph.md} +1 -1
- package/dist/docs/development/components/components/{15-openapi.md → 16-openapi.md} +1 -1
- package/dist/docs/development/components/components/{16-prompt.md → 17-prompt.md} +1 -1
- package/dist/docs/development/components/components/{17-remote-schema.md → 18-remote-schema.md} +1 -1
- package/dist/docs/development/components/components/{18-resource-group-table.md → 19-resource-group-table.md} +1 -1
- package/dist/docs/development/components/components/{19-resource-link.md → 20-resource-link.md} +1 -1
- package/dist/docs/development/components/components/{20-schema.md → 21-schema.md} +1 -1
- package/dist/docs/development/components/components/{21-schema-viewer.md → 22-schema-viewer.md} +1 -1
- package/dist/docs/development/components/components/{22-steps.md → 23-steps.md} +1 -1
- package/dist/docs/development/components/components/{23-tabs.md → 24-tabs.md} +1 -1
- package/dist/docs/development/components/components/{24-tiles.md → 25-tiles.md} +1 -1
- package/dist/docs/development/components/components/{25-visibility.md → 26-visibility.md} +1 -1
- package/dist/docs/development/components/diagram-syntax/06-mermaid.md +45 -6
- package/dist/docs/development/customization/04-documentation-sidebar.md +3 -3
- package/dist/docs/development/customization/05-resource-sidebar.md +235 -0
- package/dist/docs/development/customization/09-embed-eventcatalog.md +179 -0
- package/dist/docs/development/customization/_category_.json +1 -1
- package/dist/docs/development/deployment/build-and-deploy.md +4 -0
- package/dist/docs/development/deployment/hosting-options.md +1 -1
- package/dist/docs/development/deployment/link-validation.md +105 -0
- package/dist/docs/development/developer-tools/eventcatalog-linter/00-overview.md +76 -0
- package/dist/docs/development/developer-tools/eventcatalog-linter/01-setup.md +144 -0
- package/dist/docs/development/developer-tools/eventcatalog-linter/_category_.json +10 -0
- package/dist/docs/development/developer-tools/eventcatalog-linter/how-to/01-configure-rules.md +130 -0
- package/dist/docs/development/developer-tools/eventcatalog-linter/how-to/02-run-in-ci.md +93 -0
- package/dist/docs/development/developer-tools/eventcatalog-linter/how-to/03-use-custom-frontmatter.md +81 -0
- package/dist/docs/development/developer-tools/eventcatalog-linter/how-to/04-reference-external-catalogs.md +51 -0
- package/dist/docs/development/developer-tools/eventcatalog-linter/how-to/05-fix-common-problems.md +120 -0
- package/dist/docs/development/developer-tools/eventcatalog-linter/how-to/_category_.json +12 -0
- package/dist/docs/development/developer-tools/eventcatalog-linter/reference/01-cli.md +109 -0
- package/dist/docs/development/developer-tools/eventcatalog-linter/reference/02-configuration.md +120 -0
- package/dist/docs/development/developer-tools/eventcatalog-linter/reference/03-rules.md +287 -0
- package/dist/docs/development/developer-tools/eventcatalog-linter/reference/04-versions.md +77 -0
- package/dist/docs/development/developer-tools/eventcatalog-linter/reference/05-supported-resources.md +75 -0
- package/dist/docs/development/developer-tools/eventcatalog-linter/reference/_category_.json +12 -0
- package/dist/docs/development/guides/97-architecture-graph.md +78 -0
- package/dist/docs/development/guides/domains/02-create-domain.md +1 -1
- package/dist/docs/development/guides/domains/03-ownership-and-language/02-adding-ubiquitous-language.md +29 -0
- package/dist/docs/development/guides/domains/09-reference.md +10 -0
- package/dist/docs/development/guides/resources/messages/04-message-channels/02-adding-channels.md +2 -2
- package/dist/docs/development/guides/resources/messages/04-message-channels/04-adding-messages-to-services.md +8 -0
- package/dist/docs/development/guides/resources/schemas/03-add-specifications-to-services/01-add-openapi-specifications.md +2 -2
- package/dist/docs/development/guides/resources/schemas/03-add-specifications-to-services/02-add-asyncapi-specifications.md +3 -3
- package/dist/docs/development/guides/systems/05-system-context-maps.md +2 -0
- package/dist/docs/development/license-keys/license-validation.md +7 -3
- package/dist/docs/development/license-keys/overview.md +7 -3
- package/dist/docs/development/upgrading/upgrading.md +3 -3
- package/dist/docs/development/upgrading/v2.md +3 -3
- package/dist/docs/federation/00-overview.md +158 -0
- package/dist/docs/federation/01-first-federation.md +199 -0
- package/dist/docs/federation/_category_.json +12 -0
- package/dist/docs/federation/explanation/_category_.json +12 -0
- package/dist/docs/federation/explanation/how-it-works.md +129 -0
- package/dist/docs/federation/explanation/lockfile-and-cache.md +134 -0
- package/dist/docs/federation/explanation/mvp-status-and-feedback.md +86 -0
- package/dist/docs/federation/explanation/ownership-and-references.md +134 -0
- package/dist/docs/federation/how-to/_category_.json +12 -0
- package/dist/docs/federation/how-to/configure-github-sources.md +156 -0
- package/dist/docs/federation/how-to/configure-validation-rules.md +127 -0
- package/dist/docs/federation/how-to/run-in-ci.md +138 -0
- package/dist/docs/federation/how-to/share-assets-and-components.md +125 -0
- package/dist/docs/federation/how-to/use-local-sources.md +138 -0
- package/dist/docs/{plugins/eventcatalog-federation → federation/legacy-federation}/00-introduction.md +11 -2
- package/dist/docs/{plugins/eventcatalog-federation → federation/legacy-federation}/01-installation.md +11 -3
- package/dist/docs/{plugins/eventcatalog-federation → federation/legacy-federation}/02-plugin-configuration.md +11 -2
- package/dist/docs/{plugins/eventcatalog-federation → federation/legacy-federation}/03-examples.md +11 -1
- package/dist/docs/{plugins/eventcatalog-federation → federation/legacy-federation}/04-configuration.md +11 -1
- package/dist/docs/{plugins/eventcatalog-federation → federation/legacy-federation}/05-setup-team-catalog.md +11 -2
- package/dist/docs/federation/legacy-federation/_category_.json +12 -0
- package/dist/docs/federation/reference/_category_.json +12 -0
- package/dist/docs/federation/reference/cli.md +122 -0
- package/dist/docs/federation/reference/configuration.md +192 -0
- package/dist/docs/federation/reference/diagnostic-rules.md +173 -0
- package/dist/docs/federation/reference/generated-output.md +118 -0
- package/dist/docs/federation/reference/supported-content.md +86 -0
- package/dist/docs/federation/reference/troubleshooting.md +208 -0
- package/dist/docs/plugins/01-intro.md +3 -3
- package/dist/docs/plugins/03-all-plugins.md +2 -2
- package/dist/docs/plugins/asyncapi/00-intro.md +4 -4
- package/dist/docs/plugins/asyncapi/02-plugin-configuration.md +4 -4
- package/dist/docs/plugins/backstage/00-overview.md +53 -0
- package/dist/docs/plugins/backstage/01-get-started.md +95 -0
- package/dist/docs/plugins/backstage/02-embed-entity-tabs.md +109 -0
- package/dist/docs/plugins/backstage/03-add-overview-cards.md +79 -0
- package/dist/docs/plugins/backstage/04-control-theme-and-size.md +54 -0
- package/dist/docs/plugins/backstage/05-migrate-to-annotations.md +49 -0
- package/dist/docs/plugins/backstage/06-configuration.md +85 -0
- package/dist/docs/plugins/backstage/07-components.md +129 -0
- package/dist/docs/plugins/backstage/08-how-embeds-work.md +52 -0
- package/dist/docs/plugins/backstage/_category_.json +4 -3
- package/dist/docs/plugins/eventbridge/00-intro.md +5 -5
- package/dist/docs/plugins/eventbridge/03-api.md +3 -3
- package/dist/docs/plugins/graphql/00-intro.md +2 -2
- package/dist/docs/plugins/graphql/02-plugin-configuration.md +2 -2
- package/dist/docs/plugins/hookdeck/01-intro.md +1 -1
- package/dist/docs/plugins/openapi/00-intro.md +1 -1
- package/dist/eventcatalog.cjs +1 -1
- package/dist/eventcatalog.js +5 -5
- package/dist/generate.cjs +1 -1
- package/dist/generate.js +3 -3
- package/dist/utils/cli-logger.cjs +1 -1
- package/dist/utils/cli-logger.js +2 -2
- package/eventcatalog/src/components/SchemaExplorer/AvroSchemaViewer.tsx +21 -19
- package/eventcatalog/src/components/SchemaExplorer/JSONSchemaViewer.test.tsx +114 -0
- package/eventcatalog/src/components/SchemaExplorer/JSONSchemaViewer.tsx +107 -86
- package/eventcatalog/src/components/SchemaExplorer/ProtobufSchemaViewer.test.tsx +91 -0
- package/eventcatalog/src/components/SchemaExplorer/ProtobufSchemaViewer.tsx +115 -33
- package/eventcatalog/src/components/SchemaExplorer/SchemaViewerHover.test.tsx +33 -0
- package/eventcatalog/src/components/SchemaExplorer/protobuf-validation.test.ts +84 -0
- package/eventcatalog/src/components/SchemaExplorer/protobuf-validation.ts +149 -0
- package/eventcatalog/src/utils/collections/glob-loader.ts +2 -2
- package/eventcatalog/src/utils/protobuf-schema.ts +211 -16
- package/package.json +8 -8
- package/dist/docs/cli/channels.md +0 -180
- package/dist/docs/cli/commands.md +0 -183
- package/dist/docs/cli/custom-docs.md +0 -78
- package/dist/docs/cli/data-products.md +0 -177
- package/dist/docs/cli/data-stores.md +0 -166
- package/dist/docs/cli/diagrams.md +0 -147
- package/dist/docs/cli/domains.md +0 -280
- package/dist/docs/cli/entities.md +0 -138
- package/dist/docs/cli/events.md +0 -186
- package/dist/docs/cli/export.md +0 -27
- package/dist/docs/cli/governance.md +0 -24
- package/dist/docs/cli/messages.md +0 -69
- package/dist/docs/cli/queries.md +0 -183
- package/dist/docs/cli/services.md +0 -266
- package/dist/docs/cli/snapshots.md +0 -44
- package/dist/docs/cli/teams.md +0 -75
- package/dist/docs/cli/users.md +0 -75
- package/dist/docs/cli/utilities.md +0 -43
- package/dist/docs/development/developer-tools/eventcatalog-linter.md +0 -732
- package/dist/docs/plugins/backstage/00-intro.md +0 -67
- package/dist/docs/plugins/backstage/01-installation.md +0 -242
- package/dist/docs/plugins/backstage/02-api.md +0 -51
- package/dist/docs/plugins/backstage/03-examples.md +0 -12
- package/dist/docs/plugins/eventcatalog-federation/_category_.json +0 -11
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { act, createElement } from 'react';
|
|
3
|
+
import { createRoot, type Root } from 'react-dom/client';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import JSONSchemaViewer from './JSONSchemaViewer';
|
|
6
|
+
|
|
7
|
+
const constraintOnlyVariantSchema = {
|
|
8
|
+
type: 'object',
|
|
9
|
+
properties: {
|
|
10
|
+
detail: { $ref: '#/$defs/detail' },
|
|
11
|
+
},
|
|
12
|
+
$defs: {
|
|
13
|
+
detail: {
|
|
14
|
+
type: 'object',
|
|
15
|
+
properties: {
|
|
16
|
+
data: { $ref: '#/$defs/data' },
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
data: {
|
|
20
|
+
type: 'object',
|
|
21
|
+
properties: {
|
|
22
|
+
userId: { type: 'string' },
|
|
23
|
+
customerId: { type: 'string' },
|
|
24
|
+
correlationId: { type: 'string' },
|
|
25
|
+
},
|
|
26
|
+
required: ['correlationId'],
|
|
27
|
+
oneOf: [
|
|
28
|
+
{
|
|
29
|
+
title: 'Authenticated user',
|
|
30
|
+
required: ['userId'],
|
|
31
|
+
not: { required: ['customerId'] },
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
title: 'Customer account',
|
|
35
|
+
required: ['customerId'],
|
|
36
|
+
not: { required: ['userId'] },
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
describe('JSONSchemaViewer variants', () => {
|
|
44
|
+
let container: HTMLDivElement;
|
|
45
|
+
let root: Root;
|
|
46
|
+
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
|
49
|
+
container = document.createElement('div');
|
|
50
|
+
document.body.appendChild(container);
|
|
51
|
+
root = createRoot(container);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
act(() => root.unmount());
|
|
56
|
+
container.remove();
|
|
57
|
+
vi.unstubAllGlobals();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('renders parent properties selected by constraint-only oneOf branches', () => {
|
|
61
|
+
act(() => {
|
|
62
|
+
root.render(createElement(JSONSchemaViewer, { schema: constraintOnlyVariantSchema, expand: true, search: false }));
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect(container.textContent).toContain('Authenticated user');
|
|
66
|
+
expect(container.textContent).toContain('userId');
|
|
67
|
+
expect(container.textContent).not.toContain('customerId');
|
|
68
|
+
expect(container.textContent).toContain('correlationId');
|
|
69
|
+
|
|
70
|
+
const selector = container.querySelector<HTMLSelectElement>('select');
|
|
71
|
+
expect(selector).not.toBeNull();
|
|
72
|
+
|
|
73
|
+
act(() => {
|
|
74
|
+
selector!.value = '1';
|
|
75
|
+
selector!.dispatchEvent(new Event('change', { bubbles: true }));
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
expect(container.textContent).toContain('customerId');
|
|
79
|
+
expect(container.textContent).not.toContain('userId');
|
|
80
|
+
expect(container.textContent).toContain('correlationId');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('applies constraint-only oneOf branches at the schema root', () => {
|
|
84
|
+
const rootSchema = constraintOnlyVariantSchema.$defs.data;
|
|
85
|
+
|
|
86
|
+
act(() => {
|
|
87
|
+
root.render(createElement(JSONSchemaViewer, { schema: rootSchema, expand: true, search: false }));
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
expect(container.textContent).toContain('userId');
|
|
91
|
+
expect(container.textContent).not.toContain('customerId');
|
|
92
|
+
expect(container.textContent).toContain('correlationId');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('does not render array item parent properties twice when variants are present', () => {
|
|
96
|
+
const arraySchema = {
|
|
97
|
+
type: 'object',
|
|
98
|
+
properties: {
|
|
99
|
+
identities: {
|
|
100
|
+
type: 'array',
|
|
101
|
+
items: constraintOnlyVariantSchema.$defs.data,
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
act(() => {
|
|
107
|
+
root.render(createElement(JSONSchemaViewer, { schema: arraySchema, expand: true, search: false }));
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
expect(container.textContent?.match(/userId/g)).toHaveLength(1);
|
|
111
|
+
expect(container.textContent).not.toContain('customerId');
|
|
112
|
+
expect(container.textContent?.match(/correlationId/g)).toHaveLength(1);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -31,6 +31,15 @@ function formatType(type: any): string {
|
|
|
31
31
|
return type ?? '';
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
function getForbiddenVariantProperties(variant: any): string[] {
|
|
35
|
+
const forbiddenRequired = variant?.not?.required;
|
|
36
|
+
|
|
37
|
+
// `not: { required: ['field'] }` forbids that field from being present. When
|
|
38
|
+
// multiple fields are listed, JSON Schema only forbids them being present
|
|
39
|
+
// together, so none of them can safely be hidden individually.
|
|
40
|
+
return Array.isArray(forbiddenRequired) && forbiddenRequired.length === 1 ? forbiddenRequired : [];
|
|
41
|
+
}
|
|
42
|
+
|
|
34
43
|
// Helper function to count properties recursively
|
|
35
44
|
function countProperties(obj: any): number {
|
|
36
45
|
if (!obj || typeof obj !== 'object') return 0;
|
|
@@ -173,26 +182,35 @@ function processSchema(schema: any, rootSchema?: any): any {
|
|
|
173
182
|
const variantsArray = schema.oneOf || schema.anyOf;
|
|
174
183
|
const variantType = schema.oneOf ? 'oneOf' : 'anyOf';
|
|
175
184
|
|
|
185
|
+
const processedProperties = schema.properties
|
|
186
|
+
? Object.fromEntries(
|
|
187
|
+
Object.entries(schema.properties).map(([key, prop]: [string, any]) => [key, processSchema(prop, root)])
|
|
188
|
+
)
|
|
189
|
+
: {};
|
|
190
|
+
|
|
176
191
|
const processedVariants = variantsArray.map((variant: any, index: number) => {
|
|
177
192
|
const processedVariant = processSchema(variant, root);
|
|
193
|
+
const forbiddenProperties = new Set(getForbiddenVariantProperties(processedVariant));
|
|
194
|
+
const properties = Object.fromEntries(
|
|
195
|
+
Object.entries({ ...processedProperties, ...(processedVariant.properties || {}) }).filter(
|
|
196
|
+
([propertyName]) => !forbiddenProperties.has(propertyName)
|
|
197
|
+
)
|
|
198
|
+
);
|
|
199
|
+
|
|
178
200
|
return {
|
|
201
|
+
...processedVariant,
|
|
179
202
|
title: processedVariant.title || variant.title || processedVariant.$id || `Option ${index + 1}`,
|
|
180
203
|
description: processedVariant.description || variant.description,
|
|
181
|
-
required: processedVariant.required || variant.required || [],
|
|
182
|
-
properties
|
|
204
|
+
required: [...new Set([...(schema.required || []), ...(processedVariant.required || variant.required || [])])],
|
|
205
|
+
properties,
|
|
183
206
|
type: processedVariant.type || variant.type || 'object',
|
|
184
|
-
...processedVariant,
|
|
185
207
|
};
|
|
186
208
|
});
|
|
187
209
|
|
|
188
210
|
return {
|
|
189
211
|
...schema,
|
|
190
212
|
type: schema.type || 'object',
|
|
191
|
-
properties:
|
|
192
|
-
? Object.fromEntries(
|
|
193
|
-
Object.entries(schema.properties).map(([key, prop]: [string, any]) => [key, processSchema(prop, root)])
|
|
194
|
-
)
|
|
195
|
-
: {},
|
|
213
|
+
properties: processedProperties,
|
|
196
214
|
variants: processedVariants,
|
|
197
215
|
variantType,
|
|
198
216
|
};
|
|
@@ -315,86 +333,88 @@ const SchemaProperty = ({ name, details, isRequired, level, isListItem = false,
|
|
|
315
333
|
)}
|
|
316
334
|
|
|
317
335
|
<div className="flex-grow">
|
|
318
|
-
<div className="
|
|
319
|
-
<div>
|
|
320
|
-
<span className="font-semibold text-[rgb(var(--ec-page-text))] text-sm">{name}</span>
|
|
321
|
-
<span className="ml-1.5 text-[rgb(var(--ec-accent))] font-mono text-xs">
|
|
322
|
-
{hasVariants ? (details.variantType === 'anyOf' ? 'anyOf' : 'oneOf') : formatType(details.type)}
|
|
323
|
-
{hasType(details.type, 'array') && details.items?.type ? `[${formatType(details.items.type)}]` : ''}
|
|
324
|
-
{details.format ? `<${details.format}>` : ''}
|
|
325
|
-
{details._refPath && (
|
|
326
|
-
<span className="text-blue-600 dark:text-blue-400 ml-1">→ {details._refName || details._refPath}</span>
|
|
327
|
-
)}
|
|
328
|
-
{details._refNotFound && <span className="text-red-600 dark:text-red-400 ml-1">❌ ref not found</span>}
|
|
329
|
-
{details.const !== undefined && (
|
|
330
|
-
<span>
|
|
331
|
-
constant: <code>{details.const}</code>
|
|
332
|
-
</span>
|
|
333
|
-
)}
|
|
334
|
-
</span>
|
|
335
|
-
</div>
|
|
336
|
-
{isRequired && <span className="text-red-600 dark:text-red-400 text-xs ml-3 flex-shrink-0">required</span>}
|
|
337
|
-
</div>
|
|
338
|
-
|
|
339
|
-
{details.description && <p className="text-[rgb(var(--ec-page-text-muted))] text-xs mt-0.5">{details.description}</p>}
|
|
340
|
-
{details.title && details.title !== details.description && (
|
|
341
|
-
<p className="text-[rgb(var(--ec-page-text-muted))] text-xs mt-0.5 italic">Title: {details.title}</p>
|
|
342
|
-
)}
|
|
343
|
-
|
|
344
|
-
<div className="text-xs text-[rgb(var(--ec-page-text-muted))] mt-0.5 space-y-0">
|
|
345
|
-
{details.pattern && (
|
|
346
|
-
<div>
|
|
347
|
-
Match pattern:{' '}
|
|
348
|
-
<code className="bg-[rgb(var(--ec-content-hover))] px-1 rounded text-[rgb(var(--ec-page-text))] font-thin py-0.5">
|
|
349
|
-
{details.pattern}
|
|
350
|
-
</code>
|
|
351
|
-
</div>
|
|
352
|
-
)}
|
|
353
|
-
{details.minimum !== undefined && (
|
|
354
|
-
<div>
|
|
355
|
-
Minimum:{' '}
|
|
356
|
-
<code className="bg-[rgb(var(--ec-content-hover))] px-1 rounded text-[rgb(var(--ec-page-text))] font-thin py-0.5">
|
|
357
|
-
{details.minimum}
|
|
358
|
-
</code>
|
|
359
|
-
</div>
|
|
360
|
-
)}
|
|
361
|
-
{details.maximum !== undefined && (
|
|
362
|
-
<div>
|
|
363
|
-
Maximum:{' '}
|
|
364
|
-
<code className="bg-[rgb(var(--ec-content-hover))] px-1 rounded text-[rgb(var(--ec-page-text))] font-thin py-0.5">
|
|
365
|
-
{details.maximum}
|
|
366
|
-
</code>
|
|
367
|
-
</div>
|
|
368
|
-
)}
|
|
369
|
-
{details.minLength !== undefined && (
|
|
370
|
-
<div>
|
|
371
|
-
Min length:{' '}
|
|
372
|
-
<code className="bg-[rgb(var(--ec-content-hover))] px-1 rounded text-[rgb(var(--ec-page-text))] font-thin py-0.5">
|
|
373
|
-
{details.minLength}
|
|
374
|
-
</code>
|
|
375
|
-
</div>
|
|
376
|
-
)}
|
|
377
|
-
{details.maxLength !== undefined && (
|
|
336
|
+
<div className="-mx-1 rounded px-1 transition-colors hover:bg-[rgb(var(--ec-content-hover))]">
|
|
337
|
+
<div className="flex justify-between items-baseline">
|
|
378
338
|
<div>
|
|
379
|
-
|
|
380
|
-
<
|
|
381
|
-
{details.
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
</span>
|
|
395
|
-
))}
|
|
339
|
+
<span className="font-semibold text-[rgb(var(--ec-page-text))] text-sm">{name}</span>
|
|
340
|
+
<span className="ml-1.5 text-[rgb(var(--ec-accent))] font-mono text-xs">
|
|
341
|
+
{hasVariants ? (details.variantType === 'anyOf' ? 'anyOf' : 'oneOf') : formatType(details.type)}
|
|
342
|
+
{hasType(details.type, 'array') && details.items?.type ? `[${formatType(details.items.type)}]` : ''}
|
|
343
|
+
{details.format ? `<${details.format}>` : ''}
|
|
344
|
+
{details._refPath && (
|
|
345
|
+
<span className="text-blue-600 dark:text-blue-400 ml-1">→ {details._refName || details._refPath}</span>
|
|
346
|
+
)}
|
|
347
|
+
{details._refNotFound && <span className="text-red-600 dark:text-red-400 ml-1">❌ ref not found</span>}
|
|
348
|
+
{details.const !== undefined && (
|
|
349
|
+
<span>
|
|
350
|
+
constant: <code>{details.const}</code>
|
|
351
|
+
</span>
|
|
352
|
+
)}
|
|
353
|
+
</span>
|
|
396
354
|
</div>
|
|
355
|
+
{isRequired && <span className="text-red-600 dark:text-red-400 text-xs ml-3 flex-shrink-0">required</span>}
|
|
356
|
+
</div>
|
|
357
|
+
|
|
358
|
+
{details.description && <p className="text-[rgb(var(--ec-page-text-muted))] text-xs mt-0.5">{details.description}</p>}
|
|
359
|
+
{details.title && details.title !== details.description && (
|
|
360
|
+
<p className="text-[rgb(var(--ec-page-text-muted))] text-xs mt-0.5 italic">Title: {details.title}</p>
|
|
397
361
|
)}
|
|
362
|
+
|
|
363
|
+
<div className="text-xs text-[rgb(var(--ec-page-text-muted))] mt-0.5 space-y-0">
|
|
364
|
+
{details.pattern && (
|
|
365
|
+
<div>
|
|
366
|
+
Match pattern:{' '}
|
|
367
|
+
<code className="bg-[rgb(var(--ec-content-hover))] px-1 rounded text-[rgb(var(--ec-page-text))] font-thin py-0.5">
|
|
368
|
+
{details.pattern}
|
|
369
|
+
</code>
|
|
370
|
+
</div>
|
|
371
|
+
)}
|
|
372
|
+
{details.minimum !== undefined && (
|
|
373
|
+
<div>
|
|
374
|
+
Minimum:{' '}
|
|
375
|
+
<code className="bg-[rgb(var(--ec-content-hover))] px-1 rounded text-[rgb(var(--ec-page-text))] font-thin py-0.5">
|
|
376
|
+
{details.minimum}
|
|
377
|
+
</code>
|
|
378
|
+
</div>
|
|
379
|
+
)}
|
|
380
|
+
{details.maximum !== undefined && (
|
|
381
|
+
<div>
|
|
382
|
+
Maximum:{' '}
|
|
383
|
+
<code className="bg-[rgb(var(--ec-content-hover))] px-1 rounded text-[rgb(var(--ec-page-text))] font-thin py-0.5">
|
|
384
|
+
{details.maximum}
|
|
385
|
+
</code>
|
|
386
|
+
</div>
|
|
387
|
+
)}
|
|
388
|
+
{details.minLength !== undefined && (
|
|
389
|
+
<div>
|
|
390
|
+
Min length:{' '}
|
|
391
|
+
<code className="bg-[rgb(var(--ec-content-hover))] px-1 rounded text-[rgb(var(--ec-page-text))] font-thin py-0.5">
|
|
392
|
+
{details.minLength}
|
|
393
|
+
</code>
|
|
394
|
+
</div>
|
|
395
|
+
)}
|
|
396
|
+
{details.maxLength !== undefined && (
|
|
397
|
+
<div>
|
|
398
|
+
Max length:{' '}
|
|
399
|
+
<code className="bg-[rgb(var(--ec-content-hover))] px-1 rounded text-[rgb(var(--ec-page-text))] font-thin py-0.5">
|
|
400
|
+
{details.maxLength}
|
|
401
|
+
</code>
|
|
402
|
+
</div>
|
|
403
|
+
)}
|
|
404
|
+
{details.enum && (
|
|
405
|
+
<div>
|
|
406
|
+
<span className="text-xs inline-block">Allowed values:</span>
|
|
407
|
+
{details.enum.map((val: any, idx: number) => (
|
|
408
|
+
<span key={idx} className="text-xs">
|
|
409
|
+
{' '}
|
|
410
|
+
<code className="bg-[rgb(var(--ec-content-hover))] px-1 rounded text-[rgb(var(--ec-page-text))] font-thin py-0.5">
|
|
411
|
+
{val}
|
|
412
|
+
</code>
|
|
413
|
+
</span>
|
|
414
|
+
))}
|
|
415
|
+
</div>
|
|
416
|
+
)}
|
|
417
|
+
</div>
|
|
398
418
|
</div>
|
|
399
419
|
|
|
400
420
|
{(hasNestedProperties || hasArrayItems || hasVariants) && (
|
|
@@ -460,6 +480,7 @@ const SchemaProperty = ({ name, details, isRequired, level, isListItem = false,
|
|
|
460
480
|
<div className="mt-1 border-l border-dashed border-[rgb(var(--ec-page-border))] pl-3 ml-1.5">
|
|
461
481
|
<span className="text-xs italic text-[rgb(var(--ec-page-text-muted))] block mb-1">Item Details:</span>
|
|
462
482
|
{details.items.properties &&
|
|
483
|
+
!details.items.variants &&
|
|
463
484
|
Object.entries(details.items.properties).map(([itemPropName, itemPropDetails]: [string, any]) => (
|
|
464
485
|
<SchemaProperty
|
|
465
486
|
key={itemPropName}
|
|
@@ -821,7 +842,7 @@ export default function JSONSchemaViewer({
|
|
|
821
842
|
// Determine which properties to display
|
|
822
843
|
const propsToDisplay =
|
|
823
844
|
variants && variants.length > 0 && variants[selectedVariantIndex]?.properties
|
|
824
|
-
?
|
|
845
|
+
? variants[selectedVariantIndex].properties
|
|
825
846
|
: properties;
|
|
826
847
|
const requiredProps = variants && variants.length > 0 ? variants[selectedVariantIndex]?.required || [] : required;
|
|
827
848
|
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from 'react-dom/server';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { parseProtobufSchema } from '@utils/protobuf-schema';
|
|
4
|
+
import ProtobufSchemaViewer from './ProtobufSchemaViewer';
|
|
5
|
+
|
|
6
|
+
describe('ProtobufSchemaViewer', () => {
|
|
7
|
+
it('renders Protovalidate required and constraint annotations', () => {
|
|
8
|
+
const schema = parseProtobufSchema(`
|
|
9
|
+
syntax = "proto3";
|
|
10
|
+
|
|
11
|
+
enum Status {
|
|
12
|
+
STATUS_UNSPECIFIED = 0;
|
|
13
|
+
STATUS_ACTIVE = 1;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
message Journey {
|
|
17
|
+
string id = 1 [
|
|
18
|
+
(buf.validate.field).required = true,
|
|
19
|
+
(buf.validate.field).string.uuid = true
|
|
20
|
+
];
|
|
21
|
+
uint32 passenger_count = 2 [(buf.validate.field).uint32 = { gte: 1, lte: 20 }];
|
|
22
|
+
Status status = 3 [(buf.validate.field).enum = { defined_only: true, not_in: [0] }];
|
|
23
|
+
repeated string stops = 4 [(buf.validate.field).repeated = {
|
|
24
|
+
min_items: 2
|
|
25
|
+
unique: true
|
|
26
|
+
items: { string: { min_len: 3 } }
|
|
27
|
+
}];
|
|
28
|
+
planning.model.v1.TrainServiceReference train_service = 5;
|
|
29
|
+
optional string description = 6 [(datahub.v1.gdpr_rule).string = true];
|
|
30
|
+
}
|
|
31
|
+
`);
|
|
32
|
+
|
|
33
|
+
const html = renderToStaticMarkup(<ProtobufSchemaViewer schema={schema} />);
|
|
34
|
+
const statusField = html.slice(html.indexOf('>status</span>'), html.indexOf('>stops</span>'));
|
|
35
|
+
|
|
36
|
+
expect(html).toContain('required');
|
|
37
|
+
expect(html).toContain('Format');
|
|
38
|
+
expect(html).toContain('UUID');
|
|
39
|
+
expect(html).toContain('Minimum');
|
|
40
|
+
expect(html).toContain('Maximum');
|
|
41
|
+
expect(html).toContain('Defined enum values only');
|
|
42
|
+
expect(html).toContain('Excluded values');
|
|
43
|
+
expect(html).toContain('Minimum items');
|
|
44
|
+
expect(html).toContain('Unique items');
|
|
45
|
+
expect(html).toContain('Item minimum length');
|
|
46
|
+
expect(html).toContain('title="planning.model.v1.TrainServiceReference"');
|
|
47
|
+
expect(html).toContain('>TrainServiceReference</span>');
|
|
48
|
+
expect(statusField).not.toContain('STATUS_UNSPECIFIED');
|
|
49
|
+
expect(statusField).toContain('STATUS_ACTIVE');
|
|
50
|
+
expect(html).toContain('Annotation:');
|
|
51
|
+
expect(html).toContain('(datahub.v1.gdpr_rule).string');
|
|
52
|
+
expect(html).toContain('>true</code>');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('filters enum values using repeated-item and map-value constraints', () => {
|
|
56
|
+
const schema = parseProtobufSchema(`
|
|
57
|
+
syntax = "proto3";
|
|
58
|
+
|
|
59
|
+
enum Status {
|
|
60
|
+
STATUS_UNSPECIFIED = 0;
|
|
61
|
+
STATUS_ACTIVE = 1;
|
|
62
|
+
STATUS_ARCHIVED = 2;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
message Journey {
|
|
66
|
+
repeated Status statuses = 1 [(buf.validate.field).repeated = {
|
|
67
|
+
items: { enum: { in: [1] } }
|
|
68
|
+
}];
|
|
69
|
+
map<string, Status> status_by_key = 2 [(buf.validate.field).map = {
|
|
70
|
+
values: { enum: { not_in: [0, 2] } }
|
|
71
|
+
}];
|
|
72
|
+
Status status = 3 [(buf.validate.field).enum.const = 2];
|
|
73
|
+
}
|
|
74
|
+
`);
|
|
75
|
+
|
|
76
|
+
const html = renderToStaticMarkup(<ProtobufSchemaViewer schema={schema} />);
|
|
77
|
+
const repeatedField = html.slice(html.indexOf('>statuses</span>'), html.indexOf('>status_by_key</span>'));
|
|
78
|
+
const mapField = html.slice(html.indexOf('>status_by_key</span>'), html.indexOf('>status</span>'));
|
|
79
|
+
const directField = html.slice(html.indexOf('>status</span>'), html.indexOf('>Enum:</span>'));
|
|
80
|
+
|
|
81
|
+
expect(repeatedField).toContain('STATUS_ACTIVE');
|
|
82
|
+
expect(repeatedField).not.toContain('STATUS_UNSPECIFIED');
|
|
83
|
+
expect(repeatedField).not.toContain('STATUS_ARCHIVED');
|
|
84
|
+
expect(mapField).toContain('STATUS_ACTIVE');
|
|
85
|
+
expect(mapField).not.toContain('STATUS_UNSPECIFIED');
|
|
86
|
+
expect(mapField).not.toContain('STATUS_ARCHIVED');
|
|
87
|
+
expect(directField).toContain('STATUS_ARCHIVED');
|
|
88
|
+
expect(directField).not.toContain('STATUS_UNSPECIFIED');
|
|
89
|
+
expect(directField).not.toContain('STATUS_ACTIVE');
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { useState, useEffect, useRef, useMemo } from 'react';
|
|
2
2
|
import type { ProtobufSchema, ProtobufMessage, ProtobufEnum, ProtobufField } from '@utils/protobuf-schema';
|
|
3
|
+
import {
|
|
4
|
+
formatProtobufOptionValue,
|
|
5
|
+
formatProtovalidateRule,
|
|
6
|
+
getCustomFieldAnnotations,
|
|
7
|
+
getProtovalidateRules,
|
|
8
|
+
isProtovalidateRequired,
|
|
9
|
+
} from './protobuf-validation';
|
|
3
10
|
|
|
4
11
|
interface ProtobufSchemaViewerProps {
|
|
5
12
|
schema: ProtobufSchema;
|
|
@@ -63,9 +70,21 @@ function lookupType<T>(map: Map<string, T>, typeName: string, packageName?: stri
|
|
|
63
70
|
return undefined;
|
|
64
71
|
}
|
|
65
72
|
|
|
66
|
-
function
|
|
67
|
-
|
|
68
|
-
return
|
|
73
|
+
function compactTypeName(typeName: string): string {
|
|
74
|
+
const normalized = typeName.startsWith('.') ? typeName.slice(1) : typeName;
|
|
75
|
+
return normalized.split('.').pop() ?? normalized;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function formatProtobufType(field: ProtobufField, compact = false): string {
|
|
79
|
+
const type = field.map
|
|
80
|
+
? compact
|
|
81
|
+
? `map<${field.map.keyType}, ${compactTypeName(field.map.valueType)}>`
|
|
82
|
+
: field.type
|
|
83
|
+
: compact
|
|
84
|
+
? compactTypeName(field.type)
|
|
85
|
+
: field.type;
|
|
86
|
+
|
|
87
|
+
return field.label ? `${field.label} ${type}` : type;
|
|
69
88
|
}
|
|
70
89
|
|
|
71
90
|
function countFields(messages: ProtobufMessage[]): number {
|
|
@@ -92,7 +111,27 @@ const ProtobufFieldRow = ({ field, level, expand, showRequired, registry, packag
|
|
|
92
111
|
const nestedEnum = nestedMessage ? undefined : lookupType(registry.enums, targetTypeName, packageName);
|
|
93
112
|
const isCyclic = nestedMessage ? ancestors.includes(nestedMessage.name) : false;
|
|
94
113
|
const hasNested = !!nestedMessage && nestedMessage.fields.length > 0 && !isCyclic;
|
|
95
|
-
const
|
|
114
|
+
const hasRequiredAnnotation = isProtovalidateRequired(field);
|
|
115
|
+
const isRequired = hasRequiredAnnotation || field.label === 'required';
|
|
116
|
+
const validationRules = getProtovalidateRules(field).filter((rule) => rule.path !== 'required');
|
|
117
|
+
const customAnnotations = getCustomFieldAnnotations(field);
|
|
118
|
+
const fullType = formatProtobufType(field);
|
|
119
|
+
const displayType = formatProtobufType(field, true);
|
|
120
|
+
const enumRuleScope = field.map ? 'map.values.enum' : field.label === 'repeated' ? 'repeated.items.enum' : 'enum';
|
|
121
|
+
const enumConstant = validationRules.find((rule) => rule.path === `${enumRuleScope}.const`);
|
|
122
|
+
const enumIncludes = validationRules.find((rule) => rule.path === `${enumRuleScope}.in`);
|
|
123
|
+
const enumExcludes = validationRules.find((rule) => rule.path === `${enumRuleScope}.not_in`);
|
|
124
|
+
const normalizeRuleValues = (value: unknown) => (Array.isArray(value) ? value : value === undefined ? [] : [value]);
|
|
125
|
+
const includedEnumValues = normalizeRuleValues(enumIncludes?.value);
|
|
126
|
+
const excludedEnumValues = normalizeRuleValues(enumExcludes?.value);
|
|
127
|
+
const displayedEnumValues = nestedEnum?.values.filter((value) => {
|
|
128
|
+
const matches = (candidate: unknown) => String(candidate) === String(value.value) || candidate === value.name;
|
|
129
|
+
return (
|
|
130
|
+
(!enumConstant || matches(enumConstant.value)) &&
|
|
131
|
+
(includedEnumValues.length === 0 || includedEnumValues.some(matches)) &&
|
|
132
|
+
!excludedEnumValues.some(matches)
|
|
133
|
+
);
|
|
134
|
+
});
|
|
96
135
|
|
|
97
136
|
useEffect(() => {
|
|
98
137
|
setIsExpanded(expand);
|
|
@@ -116,40 +155,83 @@ const ProtobufFieldRow = ({ field, level, expand, showRequired, registry, packag
|
|
|
116
155
|
)}
|
|
117
156
|
|
|
118
157
|
<div className="flex-grow">
|
|
119
|
-
<div className="
|
|
120
|
-
<div>
|
|
121
|
-
<
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
oneof {field.oneof}
|
|
158
|
+
<div className="-mx-1 rounded px-1 transition-colors hover:bg-[rgb(var(--ec-content-hover))]">
|
|
159
|
+
<div className="flex justify-between items-baseline">
|
|
160
|
+
<div>
|
|
161
|
+
<span className="proto-field-name font-semibold text-[rgb(var(--ec-page-text))] text-sm">{field.name}</span>
|
|
162
|
+
<span
|
|
163
|
+
className="ml-1.5 text-[rgb(var(--ec-accent))] font-mono text-xs"
|
|
164
|
+
title={displayType === fullType ? undefined : fullType}
|
|
165
|
+
>
|
|
166
|
+
{displayType}
|
|
129
167
|
</span>
|
|
168
|
+
{field.number !== undefined && (
|
|
169
|
+
<span className="ml-1.5 text-[rgb(var(--ec-page-text-muted))] font-mono text-xs">= {field.number}</span>
|
|
170
|
+
)}
|
|
171
|
+
{field.oneof && (
|
|
172
|
+
<span className="ml-1.5 text-[rgb(var(--ec-page-text-muted))] text-xs bg-[rgb(var(--ec-content-hover))] px-1 rounded">
|
|
173
|
+
oneof {field.oneof}
|
|
174
|
+
</span>
|
|
175
|
+
)}
|
|
176
|
+
</div>
|
|
177
|
+
{(hasRequiredAnnotation || (showRequired && isRequired)) && (
|
|
178
|
+
<span className="text-red-600 dark:text-red-400 text-xs ml-3 flex-shrink-0">required</span>
|
|
130
179
|
)}
|
|
131
180
|
</div>
|
|
132
|
-
|
|
133
|
-
|
|
181
|
+
|
|
182
|
+
{field.doc && <p className="text-[rgb(var(--ec-page-text-muted))] text-xs mt-0.5">{field.doc}</p>}
|
|
183
|
+
|
|
184
|
+
{validationRules.length > 0 && (
|
|
185
|
+
<div className="text-xs text-[rgb(var(--ec-page-text-muted))] mt-0.5 space-y-0">
|
|
186
|
+
{validationRules.map((rule, index) => {
|
|
187
|
+
const display = formatProtovalidateRule(rule);
|
|
188
|
+
return (
|
|
189
|
+
<div key={`${rule.path}-${index}`}>
|
|
190
|
+
{display.label}
|
|
191
|
+
{display.value !== undefined && (
|
|
192
|
+
<>
|
|
193
|
+
:{' '}
|
|
194
|
+
<code className="bg-[rgb(var(--ec-content-hover))] px-1 rounded text-[rgb(var(--ec-page-text))] font-thin py-0.5">
|
|
195
|
+
{display.value}
|
|
196
|
+
</code>
|
|
197
|
+
</>
|
|
198
|
+
)}
|
|
199
|
+
</div>
|
|
200
|
+
);
|
|
201
|
+
})}
|
|
202
|
+
</div>
|
|
134
203
|
)}
|
|
135
|
-
</div>
|
|
136
204
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
205
|
+
{customAnnotations.length > 0 && (
|
|
206
|
+
<div className="mt-0.5 space-y-0 text-xs text-gray-400 dark:text-gray-500">
|
|
207
|
+
{customAnnotations.map((annotation, index) => (
|
|
208
|
+
<div key={`${annotation.name}-${index}`} className="flex max-w-full flex-wrap items-baseline gap-x-1">
|
|
209
|
+
<span>Annotation:</span>
|
|
210
|
+
<code className="break-all font-mono text-gray-500 dark:text-gray-400">{annotation.name}</code>
|
|
211
|
+
<span>=</span>
|
|
212
|
+
<code className="font-medium text-gray-500 dark:text-gray-400">
|
|
213
|
+
{formatProtobufOptionValue(annotation.value)}
|
|
214
|
+
</code>
|
|
215
|
+
</div>
|
|
216
|
+
))}
|
|
217
|
+
</div>
|
|
218
|
+
)}
|
|
219
|
+
|
|
220
|
+
{/* Show enum values if the field type resolves to an enum */}
|
|
221
|
+
{nestedEnum && displayedEnumValues && displayedEnumValues.length > 0 && (
|
|
222
|
+
<div className="text-xs text-[rgb(var(--ec-page-text-muted))] mt-0.5">
|
|
223
|
+
<span className="text-xs inline-block">Allowed values:</span>
|
|
224
|
+
{displayedEnumValues.map((value) => (
|
|
225
|
+
<span key={value.name} className="text-xs">
|
|
226
|
+
{' '}
|
|
227
|
+
<code className="bg-[rgb(var(--ec-content-hover))] px-1 rounded text-[rgb(var(--ec-page-text))] font-thin py-0.5">
|
|
228
|
+
{value.name}
|
|
229
|
+
</code>
|
|
230
|
+
</span>
|
|
231
|
+
))}
|
|
232
|
+
</div>
|
|
233
|
+
)}
|
|
234
|
+
</div>
|
|
153
235
|
|
|
154
236
|
{/* Nested fields for message types */}
|
|
155
237
|
{hasNested && nestedMessage && (
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from 'react-dom/server';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { parseProtobufSchema } from '@utils/protobuf-schema';
|
|
4
|
+
import AvroSchemaViewer from './AvroSchemaViewer';
|
|
5
|
+
import JSONSchemaViewer from './JSONSchemaViewer';
|
|
6
|
+
import ProtobufSchemaViewer from './ProtobufSchemaViewer';
|
|
7
|
+
|
|
8
|
+
const PROPERTY_HOVER_CLASS = 'hover:bg-[rgb(var(--ec-content-hover))]';
|
|
9
|
+
|
|
10
|
+
describe('schema viewer property hover states', () => {
|
|
11
|
+
it('adds a hover background to JSON Schema properties', () => {
|
|
12
|
+
const html = renderToStaticMarkup(
|
|
13
|
+
<JSONSchemaViewer schema={{ type: 'object', properties: { id: { type: 'string' } } }} search={false} />
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
expect(html).toContain(PROPERTY_HOVER_CLASS);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('adds a hover background to Avro fields', () => {
|
|
20
|
+
const html = renderToStaticMarkup(
|
|
21
|
+
<AvroSchemaViewer schema={{ type: 'record', name: 'Example', fields: [{ name: 'id', type: 'string' }] }} search={false} />
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
expect(html).toContain(PROPERTY_HOVER_CLASS);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('adds a hover background to Protobuf fields', () => {
|
|
28
|
+
const schema = parseProtobufSchema('syntax = "proto3"; message Example { string id = 1; }');
|
|
29
|
+
const html = renderToStaticMarkup(<ProtobufSchemaViewer schema={schema} search={false} />);
|
|
30
|
+
|
|
31
|
+
expect(html).toContain(PROPERTY_HOVER_CLASS);
|
|
32
|
+
});
|
|
33
|
+
});
|