@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.
Files changed (164) hide show
  1. package/dist/analytics/analytics.cjs +1 -1
  2. package/dist/analytics/analytics.js +2 -2
  3. package/dist/analytics/log-build.cjs +1 -1
  4. package/dist/analytics/log-build.js +3 -3
  5. package/dist/{chunk-SSSN5FXC.js → chunk-7LWOCX4R.js} +1 -1
  6. package/dist/{chunk-IIECZFXN.js → chunk-A4JRWRCV.js} +1 -1
  7. package/dist/{chunk-JHUICVBT.js → chunk-HBVTU6ZP.js} +1 -1
  8. package/dist/{chunk-PADMH2RJ.js → chunk-I6HUOX4R.js} +1 -1
  9. package/dist/{chunk-34RMFKFB.js → chunk-Q2ATWE7N.js} +1 -1
  10. package/dist/constants.cjs +1 -1
  11. package/dist/constants.js +1 -1
  12. package/dist/docs/api/02-config.md +60 -6
  13. package/dist/docs/development/ask-your-architecture/02-eventcatalog-assistant/02-configuration.md +18 -11
  14. package/dist/docs/development/authentication/providers/03-setting-up-github.md +29 -0
  15. package/dist/docs/development/bring-your-own-documentation/custom-pages/02-adding-custom-docs.md +2 -2
  16. package/dist/docs/development/bring-your-own-documentation/custom-pages/03-components.md +17 -0
  17. package/dist/docs/development/components/07-resource-references.md +7 -1
  18. package/dist/docs/development/components/components/05-architecture-graph.md +65 -0
  19. package/dist/docs/development/components/components/{05-attachments.md → 06-attachments.md} +1 -1
  20. package/dist/docs/development/components/components/{06-channel-information.md → 07-channel-information.md} +1 -1
  21. package/dist/docs/development/components/components/{07-custom-properties.md → 08-custom-properties.md} +1 -1
  22. package/dist/docs/development/components/components/{08-design.md → 09-design.md} +1 -1
  23. package/dist/docs/development/components/components/{09-entitymap.md → 10-entitymap.md} +1 -1
  24. package/dist/docs/development/components/components/{10-flow.md → 11-flow.md} +1 -1
  25. package/dist/docs/development/components/components/{11-link.md → 12-link.md} +1 -1
  26. package/dist/docs/development/components/components/{12-mermaid-file-loader.md → 13-mermaid-file-loader.md} +13 -1
  27. package/dist/docs/development/components/components/{13-message-table.md → 14-message-table.md} +1 -1
  28. package/dist/docs/development/components/components/{14-nodegraph.md → 15-nodegraph.md} +1 -1
  29. package/dist/docs/development/components/components/{15-openapi.md → 16-openapi.md} +1 -1
  30. package/dist/docs/development/components/components/{16-prompt.md → 17-prompt.md} +1 -1
  31. package/dist/docs/development/components/components/{17-remote-schema.md → 18-remote-schema.md} +1 -1
  32. package/dist/docs/development/components/components/{18-resource-group-table.md → 19-resource-group-table.md} +1 -1
  33. package/dist/docs/development/components/components/{19-resource-link.md → 20-resource-link.md} +1 -1
  34. package/dist/docs/development/components/components/{20-schema.md → 21-schema.md} +1 -1
  35. package/dist/docs/development/components/components/{21-schema-viewer.md → 22-schema-viewer.md} +1 -1
  36. package/dist/docs/development/components/components/{22-steps.md → 23-steps.md} +1 -1
  37. package/dist/docs/development/components/components/{23-tabs.md → 24-tabs.md} +1 -1
  38. package/dist/docs/development/components/components/{24-tiles.md → 25-tiles.md} +1 -1
  39. package/dist/docs/development/components/components/{25-visibility.md → 26-visibility.md} +1 -1
  40. package/dist/docs/development/components/diagram-syntax/06-mermaid.md +45 -6
  41. package/dist/docs/development/customization/04-documentation-sidebar.md +3 -3
  42. package/dist/docs/development/customization/05-resource-sidebar.md +235 -0
  43. package/dist/docs/development/customization/09-embed-eventcatalog.md +179 -0
  44. package/dist/docs/development/customization/_category_.json +1 -1
  45. package/dist/docs/development/deployment/build-and-deploy.md +4 -0
  46. package/dist/docs/development/deployment/hosting-options.md +1 -1
  47. package/dist/docs/development/deployment/link-validation.md +105 -0
  48. package/dist/docs/development/developer-tools/eventcatalog-linter/00-overview.md +76 -0
  49. package/dist/docs/development/developer-tools/eventcatalog-linter/01-setup.md +144 -0
  50. package/dist/docs/development/developer-tools/eventcatalog-linter/_category_.json +10 -0
  51. package/dist/docs/development/developer-tools/eventcatalog-linter/how-to/01-configure-rules.md +130 -0
  52. package/dist/docs/development/developer-tools/eventcatalog-linter/how-to/02-run-in-ci.md +93 -0
  53. package/dist/docs/development/developer-tools/eventcatalog-linter/how-to/03-use-custom-frontmatter.md +81 -0
  54. package/dist/docs/development/developer-tools/eventcatalog-linter/how-to/04-reference-external-catalogs.md +51 -0
  55. package/dist/docs/development/developer-tools/eventcatalog-linter/how-to/05-fix-common-problems.md +120 -0
  56. package/dist/docs/development/developer-tools/eventcatalog-linter/how-to/_category_.json +12 -0
  57. package/dist/docs/development/developer-tools/eventcatalog-linter/reference/01-cli.md +109 -0
  58. package/dist/docs/development/developer-tools/eventcatalog-linter/reference/02-configuration.md +120 -0
  59. package/dist/docs/development/developer-tools/eventcatalog-linter/reference/03-rules.md +287 -0
  60. package/dist/docs/development/developer-tools/eventcatalog-linter/reference/04-versions.md +77 -0
  61. package/dist/docs/development/developer-tools/eventcatalog-linter/reference/05-supported-resources.md +75 -0
  62. package/dist/docs/development/developer-tools/eventcatalog-linter/reference/_category_.json +12 -0
  63. package/dist/docs/development/guides/97-architecture-graph.md +78 -0
  64. package/dist/docs/development/guides/domains/02-create-domain.md +1 -1
  65. package/dist/docs/development/guides/domains/03-ownership-and-language/02-adding-ubiquitous-language.md +29 -0
  66. package/dist/docs/development/guides/domains/09-reference.md +10 -0
  67. package/dist/docs/development/guides/resources/messages/04-message-channels/02-adding-channels.md +2 -2
  68. package/dist/docs/development/guides/resources/messages/04-message-channels/04-adding-messages-to-services.md +8 -0
  69. package/dist/docs/development/guides/resources/schemas/03-add-specifications-to-services/01-add-openapi-specifications.md +2 -2
  70. package/dist/docs/development/guides/resources/schemas/03-add-specifications-to-services/02-add-asyncapi-specifications.md +3 -3
  71. package/dist/docs/development/guides/systems/05-system-context-maps.md +2 -0
  72. package/dist/docs/development/license-keys/license-validation.md +7 -3
  73. package/dist/docs/development/license-keys/overview.md +7 -3
  74. package/dist/docs/development/upgrading/upgrading.md +3 -3
  75. package/dist/docs/development/upgrading/v2.md +3 -3
  76. package/dist/docs/federation/00-overview.md +158 -0
  77. package/dist/docs/federation/01-first-federation.md +199 -0
  78. package/dist/docs/federation/_category_.json +12 -0
  79. package/dist/docs/federation/explanation/_category_.json +12 -0
  80. package/dist/docs/federation/explanation/how-it-works.md +129 -0
  81. package/dist/docs/federation/explanation/lockfile-and-cache.md +134 -0
  82. package/dist/docs/federation/explanation/mvp-status-and-feedback.md +86 -0
  83. package/dist/docs/federation/explanation/ownership-and-references.md +134 -0
  84. package/dist/docs/federation/how-to/_category_.json +12 -0
  85. package/dist/docs/federation/how-to/configure-github-sources.md +156 -0
  86. package/dist/docs/federation/how-to/configure-validation-rules.md +127 -0
  87. package/dist/docs/federation/how-to/run-in-ci.md +138 -0
  88. package/dist/docs/federation/how-to/share-assets-and-components.md +125 -0
  89. package/dist/docs/federation/how-to/use-local-sources.md +138 -0
  90. package/dist/docs/{plugins/eventcatalog-federation → federation/legacy-federation}/00-introduction.md +11 -2
  91. package/dist/docs/{plugins/eventcatalog-federation → federation/legacy-federation}/01-installation.md +11 -3
  92. package/dist/docs/{plugins/eventcatalog-federation → federation/legacy-federation}/02-plugin-configuration.md +11 -2
  93. package/dist/docs/{plugins/eventcatalog-federation → federation/legacy-federation}/03-examples.md +11 -1
  94. package/dist/docs/{plugins/eventcatalog-federation → federation/legacy-federation}/04-configuration.md +11 -1
  95. package/dist/docs/{plugins/eventcatalog-federation → federation/legacy-federation}/05-setup-team-catalog.md +11 -2
  96. package/dist/docs/federation/legacy-federation/_category_.json +12 -0
  97. package/dist/docs/federation/reference/_category_.json +12 -0
  98. package/dist/docs/federation/reference/cli.md +122 -0
  99. package/dist/docs/federation/reference/configuration.md +192 -0
  100. package/dist/docs/federation/reference/diagnostic-rules.md +173 -0
  101. package/dist/docs/federation/reference/generated-output.md +118 -0
  102. package/dist/docs/federation/reference/supported-content.md +86 -0
  103. package/dist/docs/federation/reference/troubleshooting.md +208 -0
  104. package/dist/docs/plugins/01-intro.md +3 -3
  105. package/dist/docs/plugins/03-all-plugins.md +2 -2
  106. package/dist/docs/plugins/asyncapi/00-intro.md +4 -4
  107. package/dist/docs/plugins/asyncapi/02-plugin-configuration.md +4 -4
  108. package/dist/docs/plugins/backstage/00-overview.md +53 -0
  109. package/dist/docs/plugins/backstage/01-get-started.md +95 -0
  110. package/dist/docs/plugins/backstage/02-embed-entity-tabs.md +109 -0
  111. package/dist/docs/plugins/backstage/03-add-overview-cards.md +79 -0
  112. package/dist/docs/plugins/backstage/04-control-theme-and-size.md +54 -0
  113. package/dist/docs/plugins/backstage/05-migrate-to-annotations.md +49 -0
  114. package/dist/docs/plugins/backstage/06-configuration.md +85 -0
  115. package/dist/docs/plugins/backstage/07-components.md +129 -0
  116. package/dist/docs/plugins/backstage/08-how-embeds-work.md +52 -0
  117. package/dist/docs/plugins/backstage/_category_.json +4 -3
  118. package/dist/docs/plugins/eventbridge/00-intro.md +5 -5
  119. package/dist/docs/plugins/eventbridge/03-api.md +3 -3
  120. package/dist/docs/plugins/graphql/00-intro.md +2 -2
  121. package/dist/docs/plugins/graphql/02-plugin-configuration.md +2 -2
  122. package/dist/docs/plugins/hookdeck/01-intro.md +1 -1
  123. package/dist/docs/plugins/openapi/00-intro.md +1 -1
  124. package/dist/eventcatalog.cjs +1 -1
  125. package/dist/eventcatalog.js +5 -5
  126. package/dist/generate.cjs +1 -1
  127. package/dist/generate.js +3 -3
  128. package/dist/utils/cli-logger.cjs +1 -1
  129. package/dist/utils/cli-logger.js +2 -2
  130. package/eventcatalog/src/components/SchemaExplorer/AvroSchemaViewer.tsx +21 -19
  131. package/eventcatalog/src/components/SchemaExplorer/JSONSchemaViewer.test.tsx +114 -0
  132. package/eventcatalog/src/components/SchemaExplorer/JSONSchemaViewer.tsx +107 -86
  133. package/eventcatalog/src/components/SchemaExplorer/ProtobufSchemaViewer.test.tsx +91 -0
  134. package/eventcatalog/src/components/SchemaExplorer/ProtobufSchemaViewer.tsx +115 -33
  135. package/eventcatalog/src/components/SchemaExplorer/SchemaViewerHover.test.tsx +33 -0
  136. package/eventcatalog/src/components/SchemaExplorer/protobuf-validation.test.ts +84 -0
  137. package/eventcatalog/src/components/SchemaExplorer/protobuf-validation.ts +149 -0
  138. package/eventcatalog/src/utils/collections/glob-loader.ts +2 -2
  139. package/eventcatalog/src/utils/protobuf-schema.ts +211 -16
  140. package/package.json +8 -8
  141. package/dist/docs/cli/channels.md +0 -180
  142. package/dist/docs/cli/commands.md +0 -183
  143. package/dist/docs/cli/custom-docs.md +0 -78
  144. package/dist/docs/cli/data-products.md +0 -177
  145. package/dist/docs/cli/data-stores.md +0 -166
  146. package/dist/docs/cli/diagrams.md +0 -147
  147. package/dist/docs/cli/domains.md +0 -280
  148. package/dist/docs/cli/entities.md +0 -138
  149. package/dist/docs/cli/events.md +0 -186
  150. package/dist/docs/cli/export.md +0 -27
  151. package/dist/docs/cli/governance.md +0 -24
  152. package/dist/docs/cli/messages.md +0 -69
  153. package/dist/docs/cli/queries.md +0 -183
  154. package/dist/docs/cli/services.md +0 -266
  155. package/dist/docs/cli/snapshots.md +0 -44
  156. package/dist/docs/cli/teams.md +0 -75
  157. package/dist/docs/cli/users.md +0 -75
  158. package/dist/docs/cli/utilities.md +0 -43
  159. package/dist/docs/development/developer-tools/eventcatalog-linter.md +0 -732
  160. package/dist/docs/plugins/backstage/00-intro.md +0 -67
  161. package/dist/docs/plugins/backstage/01-installation.md +0 -242
  162. package/dist/docs/plugins/backstage/02-api.md +0 -51
  163. package/dist/docs/plugins/backstage/03-examples.md +0 -12
  164. 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: processedVariant.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: schema.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="flex justify-between items-baseline">
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
- Max length:{' '}
380
- <code className="bg-[rgb(var(--ec-content-hover))] px-1 rounded text-[rgb(var(--ec-page-text))] font-thin py-0.5">
381
- {details.maxLength}
382
- </code>
383
- </div>
384
- )}
385
- {details.enum && (
386
- <div>
387
- <span className="text-xs inline-block">Allowed values:</span>
388
- {details.enum.map((val: any, idx: number) => (
389
- <span key={idx} className="text-xs">
390
- {' '}
391
- <code className="bg-[rgb(var(--ec-content-hover))] px-1 rounded text-[rgb(var(--ec-page-text))] font-thin py-0.5">
392
- {val}
393
- </code>
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
- ? { ...properties, ...variants[selectedVariantIndex].properties }
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 formatProtobufType(field: ProtobufField): string {
67
- if (field.map) return field.type;
68
- return field.label ? `${field.label} ${field.type}` : field.type;
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 isRequired = field.label === 'required';
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="flex justify-between items-baseline">
120
- <div>
121
- <span className="proto-field-name font-semibold text-[rgb(var(--ec-page-text))] text-sm">{field.name}</span>
122
- <span className="ml-1.5 text-[rgb(var(--ec-accent))] font-mono text-xs">{formatProtobufType(field)}</span>
123
- {field.number !== undefined && (
124
- <span className="ml-1.5 text-[rgb(var(--ec-page-text-muted))] font-mono text-xs">= {field.number}</span>
125
- )}
126
- {field.oneof && (
127
- <span className="ml-1.5 text-[rgb(var(--ec-page-text-muted))] text-xs bg-[rgb(var(--ec-content-hover))] px-1 rounded">
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
- {showRequired && isRequired && (
133
- <span className="text-red-600 dark:text-red-400 text-xs ml-3 flex-shrink-0">required</span>
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
- {field.doc && <p className="text-[rgb(var(--ec-page-text-muted))] text-xs mt-0.5">{field.doc}</p>}
138
-
139
- {/* Show enum values if the field type resolves to an enum */}
140
- {nestedEnum && nestedEnum.values.length > 0 && (
141
- <div className="text-xs text-[rgb(var(--ec-page-text-muted))] mt-0.5">
142
- <span className="text-xs inline-block">Allowed values:</span>
143
- {nestedEnum.values.map((value) => (
144
- <span key={value.name} className="text-xs">
145
- {' '}
146
- <code className="bg-[rgb(var(--ec-content-hover))] px-1 rounded text-[rgb(var(--ec-page-text))] font-thin py-0.5">
147
- {value.name}
148
- </code>
149
- </span>
150
- ))}
151
- </div>
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
+ });