@eventcatalog/core 4.10.12 → 4.10.14

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 (179) 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-7URQ754Q.js → chunk-HERAHLM2.js} +1 -1
  6. package/dist/{chunk-Y5XU3YLT.js → chunk-LITOQH56.js} +1 -1
  7. package/dist/{chunk-S4LLGOO4.js → chunk-LUW6RDA7.js} +1 -1
  8. package/dist/{chunk-VFSEVDCQ.js → chunk-ONDFEA5R.js} +1 -1
  9. package/dist/{chunk-A53VCEEA.js → chunk-V2SKOQYF.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 +55 -1
  13. package/dist/docs/development/authentication/providers/03-setting-up-github.md +29 -0
  14. package/dist/docs/development/bring-your-own-documentation/custom-pages/02-adding-custom-docs.md +2 -2
  15. package/dist/docs/development/bring-your-own-documentation/custom-pages/03-components.md +17 -0
  16. package/dist/docs/development/components/07-resource-references.md +7 -1
  17. package/dist/docs/development/components/components/05-architecture-graph.md +65 -0
  18. package/dist/docs/development/components/components/{05-attachments.md → 06-attachments.md} +1 -1
  19. package/dist/docs/development/components/components/{06-channel-information.md → 07-channel-information.md} +1 -1
  20. package/dist/docs/development/components/components/{07-custom-properties.md → 08-custom-properties.md} +1 -1
  21. package/dist/docs/development/components/components/{08-design.md → 09-design.md} +1 -1
  22. package/dist/docs/development/components/components/{09-entitymap.md → 10-entitymap.md} +1 -1
  23. package/dist/docs/development/components/components/{10-flow.md → 11-flow.md} +1 -1
  24. package/dist/docs/development/components/components/{11-link.md → 12-link.md} +1 -1
  25. package/dist/docs/development/components/components/{12-mermaid-file-loader.md → 13-mermaid-file-loader.md} +1 -1
  26. package/dist/docs/development/components/components/{13-message-table.md → 14-message-table.md} +1 -1
  27. package/dist/docs/development/components/components/{14-nodegraph.md → 15-nodegraph.md} +1 -1
  28. package/dist/docs/development/components/components/{15-openapi.md → 16-openapi.md} +1 -1
  29. package/dist/docs/development/components/components/{16-prompt.md → 17-prompt.md} +1 -1
  30. package/dist/docs/development/components/components/{17-remote-schema.md → 18-remote-schema.md} +1 -1
  31. package/dist/docs/development/components/components/{18-resource-group-table.md → 19-resource-group-table.md} +1 -1
  32. package/dist/docs/development/components/components/{19-resource-link.md → 20-resource-link.md} +1 -1
  33. package/dist/docs/development/components/components/{20-schema.md → 21-schema.md} +1 -1
  34. package/dist/docs/development/components/components/{21-schema-viewer.md → 22-schema-viewer.md} +1 -1
  35. package/dist/docs/development/components/components/{22-steps.md → 23-steps.md} +1 -1
  36. package/dist/docs/development/components/components/{23-tabs.md → 24-tabs.md} +1 -1
  37. package/dist/docs/development/components/components/{24-tiles.md → 25-tiles.md} +1 -1
  38. package/dist/docs/development/components/components/{25-visibility.md → 26-visibility.md} +1 -1
  39. package/dist/docs/development/customization/04-documentation-sidebar.md +3 -3
  40. package/dist/docs/development/customization/05-resource-sidebar.md +235 -0
  41. package/dist/docs/development/customization/09-embed-eventcatalog.md +179 -0
  42. package/dist/docs/development/customization/_category_.json +1 -1
  43. package/dist/docs/development/deployment/build-and-deploy.md +4 -0
  44. package/dist/docs/development/deployment/hosting-options.md +1 -1
  45. package/dist/docs/development/deployment/link-validation.md +105 -0
  46. package/dist/docs/development/developer-tools/eventcatalog-linter/00-overview.md +76 -0
  47. package/dist/docs/development/developer-tools/eventcatalog-linter/01-setup.md +144 -0
  48. package/dist/docs/development/developer-tools/eventcatalog-linter/_category_.json +10 -0
  49. package/dist/docs/development/developer-tools/eventcatalog-linter/how-to/01-configure-rules.md +130 -0
  50. package/dist/docs/development/developer-tools/eventcatalog-linter/how-to/02-run-in-ci.md +93 -0
  51. package/dist/docs/development/developer-tools/eventcatalog-linter/how-to/03-use-custom-frontmatter.md +81 -0
  52. package/dist/docs/development/developer-tools/eventcatalog-linter/how-to/04-reference-external-catalogs.md +51 -0
  53. package/dist/docs/development/developer-tools/eventcatalog-linter/how-to/05-fix-common-problems.md +120 -0
  54. package/dist/docs/development/developer-tools/eventcatalog-linter/how-to/_category_.json +12 -0
  55. package/dist/docs/development/developer-tools/eventcatalog-linter/reference/01-cli.md +109 -0
  56. package/dist/docs/development/developer-tools/eventcatalog-linter/reference/02-configuration.md +120 -0
  57. package/dist/docs/development/developer-tools/eventcatalog-linter/reference/03-rules.md +287 -0
  58. package/dist/docs/development/developer-tools/eventcatalog-linter/reference/04-versions.md +77 -0
  59. package/dist/docs/development/developer-tools/eventcatalog-linter/reference/05-supported-resources.md +75 -0
  60. package/dist/docs/development/developer-tools/eventcatalog-linter/reference/_category_.json +12 -0
  61. package/dist/docs/development/guides/97-architecture-graph.md +78 -0
  62. package/dist/docs/development/guides/domains/02-create-domain.md +1 -1
  63. package/dist/docs/development/guides/domains/03-ownership-and-language/02-adding-ubiquitous-language.md +29 -0
  64. package/dist/docs/development/guides/domains/09-reference.md +10 -0
  65. package/dist/docs/development/guides/resources/messages/04-message-channels/02-adding-channels.md +2 -2
  66. package/dist/docs/development/guides/resources/messages/04-message-channels/04-adding-messages-to-services.md +8 -0
  67. package/dist/docs/development/guides/resources/schemas/03-add-specifications-to-services/01-add-openapi-specifications.md +2 -2
  68. package/dist/docs/development/guides/resources/schemas/03-add-specifications-to-services/02-add-asyncapi-specifications.md +3 -3
  69. package/dist/docs/development/guides/systems/05-system-context-maps.md +2 -0
  70. package/dist/docs/development/license-keys/license-validation.md +7 -3
  71. package/dist/docs/development/license-keys/overview.md +7 -3
  72. package/dist/docs/development/upgrading/upgrading.md +3 -3
  73. package/dist/docs/development/upgrading/v2.md +3 -3
  74. package/dist/docs/federation/00-overview.md +158 -0
  75. package/dist/docs/federation/01-first-federation.md +199 -0
  76. package/dist/docs/federation/_category_.json +12 -0
  77. package/dist/docs/federation/explanation/_category_.json +12 -0
  78. package/dist/docs/federation/explanation/how-it-works.md +129 -0
  79. package/dist/docs/federation/explanation/lockfile-and-cache.md +134 -0
  80. package/dist/docs/federation/explanation/mvp-status-and-feedback.md +86 -0
  81. package/dist/docs/federation/explanation/ownership-and-references.md +134 -0
  82. package/dist/docs/federation/how-to/_category_.json +12 -0
  83. package/dist/docs/federation/how-to/configure-github-sources.md +156 -0
  84. package/dist/docs/federation/how-to/configure-validation-rules.md +127 -0
  85. package/dist/docs/federation/how-to/run-in-ci.md +138 -0
  86. package/dist/docs/federation/how-to/share-assets-and-components.md +125 -0
  87. package/dist/docs/federation/how-to/use-local-sources.md +138 -0
  88. package/dist/docs/{plugins/eventcatalog-federation → federation/legacy-federation}/00-introduction.md +11 -2
  89. package/dist/docs/{plugins/eventcatalog-federation → federation/legacy-federation}/01-installation.md +11 -3
  90. package/dist/docs/{plugins/eventcatalog-federation → federation/legacy-federation}/02-plugin-configuration.md +11 -2
  91. package/dist/docs/{plugins/eventcatalog-federation → federation/legacy-federation}/03-examples.md +11 -1
  92. package/dist/docs/{plugins/eventcatalog-federation → federation/legacy-federation}/04-configuration.md +11 -1
  93. package/dist/docs/{plugins/eventcatalog-federation → federation/legacy-federation}/05-setup-team-catalog.md +11 -2
  94. package/dist/docs/federation/legacy-federation/_category_.json +12 -0
  95. package/dist/docs/federation/reference/_category_.json +12 -0
  96. package/dist/docs/federation/reference/cli.md +122 -0
  97. package/dist/docs/federation/reference/configuration.md +192 -0
  98. package/dist/docs/federation/reference/diagnostic-rules.md +173 -0
  99. package/dist/docs/federation/reference/generated-output.md +118 -0
  100. package/dist/docs/federation/reference/supported-content.md +86 -0
  101. package/dist/docs/federation/reference/troubleshooting.md +208 -0
  102. package/dist/docs/plugins/01-intro.md +3 -3
  103. package/dist/docs/plugins/03-all-plugins.md +2 -2
  104. package/dist/docs/plugins/asyncapi/00-intro.md +4 -4
  105. package/dist/docs/plugins/asyncapi/02-plugin-configuration.md +4 -4
  106. package/dist/docs/plugins/backstage/00-overview.md +53 -0
  107. package/dist/docs/plugins/backstage/01-get-started.md +95 -0
  108. package/dist/docs/plugins/backstage/02-embed-entity-tabs.md +109 -0
  109. package/dist/docs/plugins/backstage/03-add-overview-cards.md +79 -0
  110. package/dist/docs/plugins/backstage/04-control-theme-and-size.md +54 -0
  111. package/dist/docs/plugins/backstage/05-migrate-to-annotations.md +49 -0
  112. package/dist/docs/plugins/backstage/06-configuration.md +85 -0
  113. package/dist/docs/plugins/backstage/07-components.md +129 -0
  114. package/dist/docs/plugins/backstage/08-how-embeds-work.md +52 -0
  115. package/dist/docs/plugins/backstage/_category_.json +4 -3
  116. package/dist/docs/plugins/eventbridge/00-intro.md +5 -5
  117. package/dist/docs/plugins/eventbridge/03-api.md +3 -3
  118. package/dist/docs/plugins/graphql/00-intro.md +2 -2
  119. package/dist/docs/plugins/graphql/02-plugin-configuration.md +2 -2
  120. package/dist/docs/plugins/hookdeck/01-intro.md +1 -1
  121. package/dist/docs/plugins/openapi/00-intro.md +1 -1
  122. package/dist/eventcatalog.cjs +1 -1
  123. package/dist/eventcatalog.js +5 -5
  124. package/dist/generate.cjs +1 -1
  125. package/dist/generate.js +3 -3
  126. package/dist/utils/cli-logger.cjs +1 -1
  127. package/dist/utils/cli-logger.js +2 -2
  128. package/eventcatalog/src/components/ChatPanel/ChatPanel.tsx +208 -119
  129. package/eventcatalog/src/components/ChatPanel/ChatPanelButton.tsx +28 -8
  130. package/eventcatalog/src/components/ChatPanel/OfflineReply.tsx +45 -0
  131. package/eventcatalog/src/components/Header.astro +11 -6
  132. package/eventcatalog/src/components/MDX/Design/Design.astro +2 -2
  133. package/eventcatalog/src/components/MDX/EntityMap/EntityMap.astro +2 -2
  134. package/eventcatalog/src/components/MDX/Flow/Flow.astro +2 -2
  135. package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.astro +2 -2
  136. package/eventcatalog/src/components/SchemaExplorer/AvroSchemaViewer.tsx +21 -19
  137. package/eventcatalog/src/components/SchemaExplorer/JSONSchemaViewer.tsx +79 -77
  138. package/eventcatalog/src/components/SchemaExplorer/ProtobufSchemaViewer.test.tsx +91 -0
  139. package/eventcatalog/src/components/SchemaExplorer/ProtobufSchemaViewer.tsx +115 -33
  140. package/eventcatalog/src/components/SchemaExplorer/SchemaViewerHover.test.tsx +33 -0
  141. package/eventcatalog/src/components/SchemaExplorer/protobuf-validation.test.ts +84 -0
  142. package/eventcatalog/src/components/SchemaExplorer/protobuf-validation.ts +149 -0
  143. package/eventcatalog/src/components/Search/Search.astro +11 -4
  144. package/eventcatalog/src/components/Settings/AssistantSettingsForm.tsx +29 -24
  145. package/eventcatalog/src/enterprise/custom-documentation/pages/docs/custom/index.astro +3 -3
  146. package/eventcatalog/src/pages/diagrams/[id]/[version]/index.astro +2 -2
  147. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/[docVersion]/index.astro +5 -3
  148. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/index.astro +10 -4
  149. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/asyncapi/[filename].astro +2 -2
  150. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +3 -3
  151. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/spec/[filename].astro +2 -2
  152. package/eventcatalog/src/pages/visualiser/designs/[id]/index.astro +2 -2
  153. package/eventcatalog/src/utils/feature.ts +1 -0
  154. package/eventcatalog/src/utils/protobuf-schema.ts +211 -16
  155. package/package.json +3 -3
  156. package/dist/docs/cli/channels.md +0 -180
  157. package/dist/docs/cli/commands.md +0 -183
  158. package/dist/docs/cli/custom-docs.md +0 -78
  159. package/dist/docs/cli/data-products.md +0 -177
  160. package/dist/docs/cli/data-stores.md +0 -166
  161. package/dist/docs/cli/diagrams.md +0 -147
  162. package/dist/docs/cli/domains.md +0 -280
  163. package/dist/docs/cli/entities.md +0 -138
  164. package/dist/docs/cli/events.md +0 -186
  165. package/dist/docs/cli/export.md +0 -27
  166. package/dist/docs/cli/governance.md +0 -24
  167. package/dist/docs/cli/messages.md +0 -69
  168. package/dist/docs/cli/queries.md +0 -183
  169. package/dist/docs/cli/services.md +0 -266
  170. package/dist/docs/cli/snapshots.md +0 -44
  171. package/dist/docs/cli/teams.md +0 -75
  172. package/dist/docs/cli/users.md +0 -75
  173. package/dist/docs/cli/utilities.md +0 -43
  174. package/dist/docs/development/developer-tools/eventcatalog-linter.md +0 -732
  175. package/dist/docs/plugins/backstage/00-intro.md +0 -67
  176. package/dist/docs/plugins/backstage/01-installation.md +0 -242
  177. package/dist/docs/plugins/backstage/02-api.md +0 -51
  178. package/dist/docs/plugins/backstage/03-examples.md +0 -12
  179. package/dist/docs/plugins/eventcatalog-federation/_category_.json +0 -11
@@ -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
+ });
@@ -0,0 +1,84 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { ProtobufField } from '@utils/protobuf-schema';
3
+ import {
4
+ formatProtobufOptionValue,
5
+ formatProtovalidateRule,
6
+ getCustomFieldAnnotations,
7
+ getProtovalidateRules,
8
+ isProtovalidateRequired,
9
+ } from './protobuf-validation';
10
+
11
+ describe('protobuf validation helpers', () => {
12
+ it('flattens direct and aggregate Protovalidate options', () => {
13
+ const field: ProtobufField = {
14
+ name: 'status',
15
+ type: 'Status',
16
+ options: [
17
+ { name: '(buf.validate.field).required', value: true },
18
+ {
19
+ name: '(buf.validate.field).enum',
20
+ value: { defined_only: true, not_in: [0] },
21
+ },
22
+ ],
23
+ };
24
+
25
+ expect(getProtovalidateRules(field)).toEqual([
26
+ { path: 'required', value: true },
27
+ { path: 'enum.defined_only', value: true },
28
+ { path: 'enum.not_in', value: [0] },
29
+ ]);
30
+ expect(isProtovalidateRequired(field)).toBe(true);
31
+ });
32
+
33
+ it('ignores non-Protovalidate custom options', () => {
34
+ const field: ProtobufField = {
35
+ name: 'description',
36
+ type: 'string',
37
+ options: [{ name: '(datahub.v1.gdpr_rule).string', value: true }],
38
+ };
39
+
40
+ expect(getProtovalidateRules(field)).toEqual([]);
41
+ expect(isProtovalidateRequired(field)).toBe(false);
42
+ expect(getCustomFieldAnnotations(field)).toEqual([{ name: '(datahub.v1.gdpr_rule).string', value: true }]);
43
+ });
44
+
45
+ it('only treats extension options as custom annotations and formats aggregate values', () => {
46
+ const field: ProtobufField = {
47
+ name: 'description',
48
+ type: 'string',
49
+ options: [
50
+ { name: 'deprecated', value: true },
51
+ { name: '(buf.validate.field).string.min_len', value: 1 },
52
+ { name: '(datahub.v1.classification)', value: { level: 'personal', encrypted: true } },
53
+ ],
54
+ };
55
+
56
+ expect(getCustomFieldAnnotations(field)).toEqual([
57
+ {
58
+ name: '(datahub.v1.classification)',
59
+ value: { level: 'personal', encrypted: true },
60
+ },
61
+ ]);
62
+ expect(formatProtobufOptionValue(getCustomFieldAnnotations(field)[0].value)).toBe('{"level":"personal","encrypted":true}');
63
+ });
64
+
65
+ it('formats common rules for display', () => {
66
+ expect(formatProtovalidateRule({ path: 'string.email', value: true })).toEqual({ label: 'Format', value: 'Email' });
67
+ expect(formatProtovalidateRule({ path: 'uint32.gte', value: 1 })).toEqual({ label: 'Minimum', value: '1' });
68
+ expect(formatProtovalidateRule({ path: 'enum.defined_only', value: true })).toEqual({
69
+ label: 'Defined enum values only',
70
+ });
71
+ expect(formatProtovalidateRule({ path: 'enum.not_in', value: [0] })).toEqual({
72
+ label: 'Excluded values',
73
+ value: '0',
74
+ });
75
+ expect(formatProtovalidateRule({ path: 'repeated.items.string.min_len', value: 3 })).toEqual({
76
+ label: 'Item minimum length',
77
+ value: '3',
78
+ });
79
+ expect(formatProtovalidateRule({ path: 'map.values.string.max_len', value: 100 })).toEqual({
80
+ label: 'Value maximum length',
81
+ value: '100',
82
+ });
83
+ });
84
+ });
@@ -0,0 +1,149 @@
1
+ import type { ProtobufField, ProtobufOptionValue } from '@utils/protobuf-schema';
2
+
3
+ const PROTOVALIDATE_FIELD_OPTION = '(buf.validate.field)';
4
+
5
+ export interface ProtovalidateRule {
6
+ path: string;
7
+ value: ProtobufOptionValue;
8
+ }
9
+
10
+ export interface ProtovalidateRuleDisplay {
11
+ label: string;
12
+ value?: string;
13
+ }
14
+
15
+ export interface ProtobufCustomAnnotation {
16
+ name: string;
17
+ value: ProtobufOptionValue;
18
+ }
19
+
20
+ const isOptionObject = (value: ProtobufOptionValue): value is { [key: string]: ProtobufOptionValue } =>
21
+ typeof value === 'object' && !Array.isArray(value);
22
+
23
+ const flattenOptionValue = (path: string, value: ProtobufOptionValue): ProtovalidateRule[] => {
24
+ if (!isOptionObject(value)) return [{ path, value }];
25
+
26
+ return Object.entries(value).flatMap(([name, nestedValue]) => flattenOptionValue(path ? `${path}.${name}` : name, nestedValue));
27
+ };
28
+
29
+ export const getProtovalidateRules = (field: ProtobufField): ProtovalidateRule[] =>
30
+ (field.options ?? []).flatMap((option) => {
31
+ if (option.name !== PROTOVALIDATE_FIELD_OPTION && !option.name.startsWith(`${PROTOVALIDATE_FIELD_OPTION}.`)) return [];
32
+
33
+ const path = option.name.slice(PROTOVALIDATE_FIELD_OPTION.length).replace(/^\./, '');
34
+ return flattenOptionValue(path, option.value);
35
+ });
36
+
37
+ export const isProtovalidateRequired = (field: ProtobufField) =>
38
+ getProtovalidateRules(field).some((rule) => rule.path === 'required' && rule.value === true);
39
+
40
+ export const getCustomFieldAnnotations = (field: ProtobufField): ProtobufCustomAnnotation[] =>
41
+ (field.options ?? [])
42
+ .filter(
43
+ (option) =>
44
+ option.name.startsWith('(') &&
45
+ option.name !== PROTOVALIDATE_FIELD_OPTION &&
46
+ !option.name.startsWith(`${PROTOVALIDATE_FIELD_OPTION}.`)
47
+ )
48
+ .map(({ name, value }) => ({ name, value }));
49
+
50
+ export const formatProtobufOptionValue = (value: ProtobufOptionValue): string => {
51
+ if (Array.isArray(value)) return value.map(formatProtobufOptionValue).join(', ');
52
+ if (isOptionObject(value)) return JSON.stringify(value);
53
+ return String(value);
54
+ };
55
+
56
+ const humanize = (value: string) => value.replace(/_/g, ' ').replace(/\b\w/g, (character) => character.toUpperCase());
57
+
58
+ const FORMAT_NAMES: Record<string, string> = {
59
+ address: 'Address',
60
+ email: 'Email',
61
+ hostname: 'Hostname',
62
+ ip: 'IP',
63
+ ipv4: 'IPv4',
64
+ ipv6: 'IPv6',
65
+ protobuf_fqn: 'Protobuf FQN',
66
+ tuuid: 'TUUID',
67
+ uri: 'URI',
68
+ uri_ref: 'URI reference',
69
+ uuid: 'UUID',
70
+ };
71
+
72
+ const getRuleScope = (path: string) => {
73
+ if (path.includes('repeated.items')) return 'Item';
74
+ if (path.includes('map.keys')) return 'Key';
75
+ if (path.includes('map.values')) return 'Value';
76
+ return '';
77
+ };
78
+
79
+ const withScope = (scope: string, label: string) =>
80
+ scope ? `${scope} ${label.charAt(0).toLowerCase()}${label.slice(1)}` : label;
81
+
82
+ export const formatProtovalidateRule = ({ path, value }: ProtovalidateRule): ProtovalidateRuleDisplay => {
83
+ const name = path.split('.').pop() ?? path;
84
+ const scope = getRuleScope(path);
85
+ const formattedValue = formatProtobufOptionValue(value);
86
+
87
+ switch (name) {
88
+ case 'required':
89
+ return value === true ? { label: 'Required' } : { label: 'Required', value: formattedValue };
90
+ case 'const':
91
+ return { label: withScope(scope, 'Constant'), value: formattedValue };
92
+ case 'len':
93
+ return { label: withScope(scope, 'Length'), value: formattedValue };
94
+ case 'min_len':
95
+ return { label: withScope(scope, 'Minimum length'), value: formattedValue };
96
+ case 'max_len':
97
+ return { label: withScope(scope, 'Maximum length'), value: formattedValue };
98
+ case 'len_bytes':
99
+ return { label: withScope(scope, 'Byte length'), value: formattedValue };
100
+ case 'min_bytes':
101
+ return { label: withScope(scope, 'Minimum byte length'), value: formattedValue };
102
+ case 'max_bytes':
103
+ return { label: withScope(scope, 'Maximum byte length'), value: formattedValue };
104
+ case 'pattern':
105
+ return { label: withScope(scope, 'Match pattern'), value: formattedValue };
106
+ case 'gt':
107
+ return { label: withScope(scope, 'Greater than'), value: formattedValue };
108
+ case 'gte':
109
+ return { label: withScope(scope, 'Minimum'), value: formattedValue };
110
+ case 'lt':
111
+ return { label: withScope(scope, 'Less than'), value: formattedValue };
112
+ case 'lte':
113
+ return { label: withScope(scope, 'Maximum'), value: formattedValue };
114
+ case 'in':
115
+ return { label: withScope(scope, 'Allowed values'), value: formattedValue };
116
+ case 'not_in':
117
+ return { label: withScope(scope, 'Excluded values'), value: formattedValue };
118
+ case 'defined_only':
119
+ return value === true
120
+ ? { label: 'Defined enum values only' }
121
+ : { label: 'Defined enum values only', value: formattedValue };
122
+ case 'min_items':
123
+ return { label: 'Minimum items', value: formattedValue };
124
+ case 'max_items':
125
+ return { label: 'Maximum items', value: formattedValue };
126
+ case 'unique':
127
+ return value === true ? { label: 'Unique items' } : { label: 'Unique items', value: formattedValue };
128
+ case 'min_pairs':
129
+ return { label: 'Minimum map entries', value: formattedValue };
130
+ case 'max_pairs':
131
+ return { label: 'Maximum map entries', value: formattedValue };
132
+ case 'email':
133
+ case 'hostname':
134
+ case 'ip':
135
+ case 'ipv4':
136
+ case 'ipv6':
137
+ case 'uri':
138
+ case 'uri_ref':
139
+ case 'address':
140
+ case 'uuid':
141
+ case 'tuuid':
142
+ case 'protobuf_fqn':
143
+ return value === true
144
+ ? { label: withScope(scope, 'Format'), value: FORMAT_NAMES[name] }
145
+ : { label: withScope(scope, humanize(name)), value: formattedValue };
146
+ default:
147
+ return { label: withScope(scope, humanize(name)), value: formattedValue };
148
+ }
149
+ };
@@ -4,18 +4,25 @@ import SearchModal from './SearchModal.tsx';
4
4
  ---
5
5
 
6
6
  <div>
7
- <div class="relative flex items-center w-full pr-4 ml-1">
7
+ <div class="relative flex min-w-0 items-center w-full">
8
8
  <input
9
9
  id="search-dummy-input"
10
10
  type="text"
11
11
  name="search"
12
12
  placeholder="Search EventCatalog"
13
13
  autocomplete="off"
14
- class="block w-full rounded-md caret-transparent border-0 py-1.5 pr-14 pl-10! text-[rgb(var(--ec-header-text))] bg-[rgb(var(--ec-header-bg))] shadow-xs ring-1 ring-inset ring-[rgb(var(--ec-dropdown-border))] placeholder:text-[rgb(var(--ec-icon-color))] font-light sm:text-sm sm:leading-6"
14
+ class="block h-9 min-w-0 w-full rounded-md caret-transparent border-0 py-1.5 pr-20 pl-10! text-[rgb(var(--ec-header-text))] bg-[rgb(var(--ec-header-bg))] shadow-xs ring-1 ring-inset ring-[rgb(var(--ec-dropdown-border))] placeholder:text-gray-400 font-light sm:text-sm sm:leading-6"
15
15
  />
16
16
  <MagnifyingGlassIcon className="absolute inset-y-0 left-0 h-9 w-8 flex items-center pl-4 text-[rgb(var(--ec-icon-color))]" />
17
- <div class="absolute inset-y-0 right-0 flex py-1.5 pr-6">
18
- <kbd class="inline-flex items-center rounded px-1 font-sans text-xs text-[rgb(var(--ec-icon-color))]">⌘K</kbd>
17
+ <div class="absolute inset-y-0 right-0 flex items-center gap-1 pr-2 pointer-events-none">
18
+ <kbd
19
+ class="inline-flex h-6 min-w-6 items-center justify-center rounded-lg border border-[rgb(var(--ec-dropdown-border))] bg-[rgb(var(--ec-card-bg))] px-1 font-sans text-xs font-normal text-[rgb(var(--ec-icon-color))] shadow-xs"
20
+ >⌘</kbd
21
+ >
22
+ <kbd
23
+ class="inline-flex h-6 min-w-6 items-center justify-center rounded-lg border border-[rgb(var(--ec-dropdown-border))] bg-[rgb(var(--ec-card-bg))] px-1 font-sans text-xs font-normal text-[rgb(var(--ec-icon-color))] shadow-xs"
24
+ >K</kbd
25
+ >
19
26
  </div>
20
27
  </div>
21
28
  </div>
@@ -76,34 +76,39 @@ export const AssistantSettingsForm = ({ canEdit, initial, chatAvailable, hasPlan
76
76
  <Row
77
77
  title="Assistant Agent"
78
78
  description="Assistant agent that answers questions about your architecture directly in your catalog."
79
- canEdit={canEdit && chatAvailable}
79
+ canEdit={canEdit}
80
80
  dirty={dirty}
81
81
  saving={saving}
82
- onSave={chatAvailable ? save : undefined}
82
+ onSave={save}
83
83
  >
84
- {chatAvailable ? (
85
- <div className="space-y-3">
86
- <ToggleRow
87
- icon={<MessageSquare className="h-4 w-4" aria-hidden />}
88
- label={chatEnabled ? 'Enabled' : 'Disabled'}
89
- hint={chatEnabled ? 'Chat is available to readers of this catalog.' : 'Chat is hidden from the catalog.'}
90
- checked={chatEnabled}
91
- disabled={!canEdit}
92
- onChange={setChatEnabled}
93
- />
94
- {chatEnabled && <ConfigurationRequired />}
95
- </div>
96
- ) : !hasPlan ? (
97
- <UpgradeRequired
98
- tier="Starter and Scale"
99
- blurb="The EventCatalog Assistant is part of our paid plans. Upgrade to give your team a built-in AI agent that answers questions about your architecture."
100
- docsUrl={ASSISTANT_DOCS_URL}
84
+ <div className="space-y-3">
85
+ <ToggleRow
86
+ icon={<MessageSquare className="h-4 w-4" aria-hidden />}
87
+ label={chatEnabled ? 'Enabled' : 'Disabled'}
88
+ hint={
89
+ chatEnabled
90
+ ? 'Event Catalog Assistant is visible in this catalog.'
91
+ : 'Event Catalog Assistant is hidden from this catalog.'
92
+ }
93
+ checked={chatEnabled}
94
+ disabled={!canEdit}
95
+ onChange={setChatEnabled}
101
96
  />
102
- ) : !inSSR ? (
103
- <AssistantNeedsSSR />
104
- ) : !hasChatConfigFile ? (
105
- <AssistantNeedsConfigFile />
106
- ) : null}
97
+ {chatEnabled &&
98
+ (chatAvailable ? (
99
+ <ConfigurationRequired />
100
+ ) : !hasPlan ? (
101
+ <UpgradeRequired
102
+ tier="Starter and Scale"
103
+ blurb="The EventCatalog Assistant is part of our paid plans. Upgrade to give your team a built-in AI agent that answers questions about your architecture."
104
+ docsUrl={ASSISTANT_DOCS_URL}
105
+ />
106
+ ) : !inSSR ? (
107
+ <AssistantNeedsSSR />
108
+ ) : !hasChatConfigFile ? (
109
+ <AssistantNeedsConfigFile />
110
+ ) : null)}
111
+ </div>
107
112
  </Row>
108
113
  </form>
109
114
  );
@@ -21,7 +21,7 @@ import {
21
21
  isVisualiserEnabled,
22
22
  isMarkdownDownloadEnabled,
23
23
  isRSSEnabled,
24
- isEventCatalogChatEnabled,
24
+ isEventCatalogChatVisible,
25
25
  isArchitectureGraphEnabled,
26
26
  } from '@utils/feature';
27
27
 
@@ -152,11 +152,11 @@ const editUrl =
152
152
  client:only="react"
153
153
  variant="toolbar"
154
154
  schemas={[]}
155
- chatEnabled={isEventCatalogChatEnabled()}
155
+ chatEnabled={isEventCatalogChatVisible()}
156
156
  markdownDownloadEnabled={isMarkdownDownloadEnabled()}
157
157
  rssFeedEnabled={isRSSEnabled()}
158
158
  editUrl={editUrl}
159
- preferChatAsDefault={isEventCatalogChatEnabled()}
159
+ preferChatAsDefault={isEventCatalogChatVisible()}
160
160
  />
161
161
  </div>
162
162
  </div>
@@ -7,7 +7,7 @@ import config from '@config';
7
7
  import { buildUrl } from '@utils/url-builder';
8
8
  import { GitCompare, X, AlignLeft, HistoryIcon } from 'lucide-react';
9
9
  import CopyAsMarkdown from '@components/CopyAsMarkdown';
10
- import { isLLMSTxtEnabled, isEventCatalogChatEnabled } from '@utils/feature';
10
+ import { isLLMSTxtEnabled, isEventCatalogChatVisible } from '@utils/feature';
11
11
 
12
12
  import { Page } from './_index.data';
13
13
 
@@ -21,7 +21,7 @@ const pageTitle = `Diagram | ${props.data.name}`;
21
21
  const currentVersion = props.data.version;
22
22
  const allVersions = props.allVersions || [currentVersion];
23
23
  const hasMultipleVersions = allVersions.length > 1;
24
- const chatEnabled = isEventCatalogChatEnabled();
24
+ const chatEnabled = isEventCatalogChatVisible();
25
25
  const markdownDownloadEnabled = isLLMSTxtEnabled();
26
26
  const chatQuery = `Tell me about the "${props.data.name}" diagram (version ${props.data.version})`;
27
27
  ---
@@ -8,7 +8,7 @@ import CopyAsMarkdown from '@components/CopyAsMarkdown';
8
8
  import Badge from '@components/Badge.astro';
9
9
  import { buildUrl } from '@utils/url-builder';
10
10
  import { AlignLeftIcon, HistoryIcon } from 'lucide-react';
11
- import { isEventCatalogChatEnabled, isMarkdownDownloadEnabled, isResourceDocsEnabled } from '@utils/feature';
11
+ import { isEventCatalogChatVisible, isMarkdownDownloadEnabled, isResourceDocsEnabled } from '@utils/feature';
12
12
  import { getResourceDocTypeLabel } from '@utils/collections/resource-docs';
13
13
  import { getIcon } from '@utils/badges';
14
14
  import { collectionToResourceMap } from '@utils/collections/util';
@@ -37,7 +37,7 @@ const docsBasePath = `/docs/${props.data.resourceCollection}/${props.data.resour
37
37
  const singularResourceName =
38
38
  collectionToResourceMap[props.data.resourceCollection as keyof typeof collectionToResourceMap] ??
39
39
  props.data.resourceCollection.slice(0, props.data.resourceCollection.length - 1);
40
- const chatEnabled = isEventCatalogChatEnabled();
40
+ const chatEnabled = isEventCatalogChatVisible();
41
41
  const chatQuery = `Tell me about the ${props.data.type} doc "${title}" for ${props.data.resourceId} (version ${props.data.version})`;
42
42
 
43
43
  const pagefindAttributes =
@@ -69,7 +69,9 @@ const pagefindAttributes =
69
69
  {
70
70
  badges.length > 0 && (
71
71
  <div class="flex flex-wrap gap-1.5 pt-4">
72
- {badges.map((badge: any) => <Badge badge={badge} />)}
72
+ {badges.map((badge: any) => (
73
+ <Badge badge={badge} />
74
+ ))}
73
75
  </div>
74
76
  )
75
77
  }
@@ -8,7 +8,7 @@ import CopyAsMarkdown from '@components/CopyAsMarkdown';
8
8
  import Badge from '@components/Badge.astro';
9
9
  import { buildUrl } from '@utils/url-builder';
10
10
  import { AlignLeftIcon, HistoryIcon } from 'lucide-react';
11
- import { isEventCatalogChatEnabled, isMarkdownDownloadEnabled, isResourceDocsEnabled } from '@utils/feature';
11
+ import { isEventCatalogChatVisible, isMarkdownDownloadEnabled, isResourceDocsEnabled } from '@utils/feature';
12
12
  import { getIcon } from '@utils/badges';
13
13
  import { collectionToResourceMap } from '@utils/collections/util';
14
14
  import { getResourceDocTypeLabel } from '@utils/collections/resource-docs';
@@ -38,7 +38,7 @@ const singularResourceName =
38
38
  collectionToResourceMap[props.data.resourceCollection as keyof typeof collectionToResourceMap] ??
39
39
  props.data.resourceCollection.slice(0, props.data.resourceCollection.length - 1);
40
40
  const typeLabel = getResourceDocTypeLabel(props.data.type);
41
- const chatEnabled = isEventCatalogChatEnabled();
41
+ const chatEnabled = isEventCatalogChatVisible();
42
42
  const chatQuery = `Tell me about the ${props.data.type} doc "${title}" for ${props.data.resourceId} (version ${props.data.version})`;
43
43
 
44
44
  const pagefindAttributes =
@@ -60,11 +60,17 @@ const pagefindAttributes =
60
60
  <span class="text-xs md:text-sm font-semibold text-[rgb(var(--ec-accent))] capitalize">{typeLabel}</span>
61
61
  <h2 id="doc-page-header" class="text-2xl md:text-4xl font-bold text-[rgb(var(--ec-page-text))]">{title}</h2>
62
62
  </div>
63
- {props.data.summary && <p class="text-base pt-4 text-[rgb(var(--ec-page-text-muted))] font-light">{props.data.summary}</p>}
63
+ {
64
+ props.data.summary && (
65
+ <p class="text-base pt-4 text-[rgb(var(--ec-page-text-muted))] font-light">{props.data.summary}</p>
66
+ )
67
+ }
64
68
  {
65
69
  badges.length > 0 && (
66
70
  <div class="flex flex-wrap gap-1.5 pt-4">
67
- {badges.map((badge: any) => <Badge badge={badge} />)}
71
+ {badges.map((badge: any) => (
72
+ <Badge badge={badge} />
73
+ ))}
68
74
  </div>
69
75
  )
70
76
  }