@eventcatalog/core 5.0.0-beta.2 → 5.0.0-beta.4

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 (65) 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-HGRFXTFC.js → chunk-M7OVJHPV.js} +1 -1
  6. package/dist/{chunk-KIDISU6X.js → chunk-PL3T5TWF.js} +1 -1
  7. package/dist/{chunk-62EV4IJA.js → chunk-Q7FBYUCZ.js} +1 -1
  8. package/dist/{chunk-4J2ZM2HX.js → chunk-TOMV5YJH.js} +1 -1
  9. package/dist/{chunk-XARPIXWF.js → chunk-VA77ENFQ.js} +1 -1
  10. package/dist/constants.cjs +1 -1
  11. package/dist/constants.js +1 -1
  12. package/dist/eventcatalog.cjs +1 -1
  13. package/dist/eventcatalog.js +5 -5
  14. package/dist/generate.cjs +1 -1
  15. package/dist/generate.js +3 -3
  16. package/dist/runtime-dependencies/manifest.json +0 -1
  17. package/dist/utils/cli-logger.cjs +1 -1
  18. package/dist/utils/cli-logger.js +2 -2
  19. package/eventcatalog/src/components/Grids/utils.tsx +0 -23
  20. package/eventcatalog/src/components/MDX/CodeGroup/CodeGroup.tsx +6 -1
  21. package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsPanel.tsx +12 -4
  22. package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +16 -7
  23. package/eventcatalog/src/components/SideNav/NestedSideBar/utils.ts +10 -0
  24. package/eventcatalog/src/components/Tables/Discover/DiscoverTable.tsx +4 -62
  25. package/eventcatalog/src/components/Tables/Discover/columns.tsx +17 -150
  26. package/eventcatalog/src/components/Tables/Resources/ResourcesTable.tsx +425 -0
  27. package/eventcatalog/src/components/Tables/RowActionsMenu.tsx +157 -0
  28. package/eventcatalog/src/components/Tables/TablePagination.tsx +82 -0
  29. package/eventcatalog/src/features/ai/chat-api.ts +3 -3
  30. package/eventcatalog/src/features/mcp/apps/generated/viewer.html +75 -74
  31. package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +0 -2
  32. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.spec.ts +52 -63
  33. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.ts +34 -46
  34. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/index.astro +11 -29
  35. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_ResourcesPage.astro +79 -0
  36. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_index.data.spec.ts +32 -14
  37. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_index.data.ts +63 -58
  38. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/index.astro +2 -62
  39. package/eventcatalog/src/pages/docs/services/[id]/[version]/resources/index.astro +13 -0
  40. package/eventcatalog/src/stores/sidebar-store/builders/domain.ts +24 -42
  41. package/eventcatalog/src/stores/sidebar-store/builders/flow.ts +2 -0
  42. package/eventcatalog/src/stores/sidebar-store/builders/message.ts +2 -0
  43. package/eventcatalog/src/stores/sidebar-store/builders/service.ts +23 -23
  44. package/eventcatalog/src/stores/sidebar-store/builders/shared.ts +13 -0
  45. package/eventcatalog/src/stores/sidebar-store/builders/system.ts +18 -27
  46. package/eventcatalog/src/stores/sidebar-store/state.ts +5 -0
  47. package/eventcatalog/src/styles/tailwind.css +2 -2
  48. package/eventcatalog/src/utils/collections/channels.ts +56 -1
  49. package/eventcatalog/src/utils/collections/flows.ts +50 -1
  50. package/eventcatalog/src/utils/collections/message-connections.ts +60 -0
  51. package/eventcatalog/src/utils/collections/message-triggers.ts +2 -1
  52. package/eventcatalog/src/utils/collections/resource-owners.ts +193 -0
  53. package/eventcatalog/src/utils/collections/resources.ts +310 -0
  54. package/eventcatalog/src/utils/collections/util.ts +31 -0
  55. package/eventcatalog/src/utils/git.ts +11 -5
  56. package/package.json +8 -10
  57. package/dist/runtime-dependencies/shelljs.mjs +0 -3
  58. package/eventcatalog/src/components/Grids/DomainGrid.tsx +0 -838
  59. package/eventcatalog/src/components/Grids/MessageGrid.tsx +0 -344
  60. package/eventcatalog/src/components/Grids/SystemGrid.tsx +0 -185
  61. package/eventcatalog/src/components/Grids/components.tsx +0 -170
  62. package/eventcatalog/src/components/Grids/message-link.ts +0 -38
  63. package/eventcatalog/src/components/Tables/SystemResources/SystemResourcesTable.tsx +0 -294
  64. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_resources.spec.ts +0 -65
  65. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_resources.ts +0 -46
@@ -1,71 +1,11 @@
1
1
  ---
2
- import Footer from '@layouts/Footer.astro';
3
- import VerticalSideBarLayout from '@layouts/VerticalSideBarLayout.astro';
4
- import { buildUrl } from '@utils/url-builder';
5
- import {
6
- SystemResourcesTable,
7
- type SystemResourceItem,
8
- type SystemResourceCollection,
9
- } from '@components/Tables/SystemResources/SystemResourcesTable';
10
- import { getResourceGroups, type ResourceOwnerCollection } from './_resources';
11
-
2
+ import ResourcesPage from './_ResourcesPage.astro';
12
3
  import { Page } from './_index.data';
13
4
 
14
5
  export const prerender = Page.prerender;
15
6
  export const getStaticPaths = Page.getStaticPaths;
16
7
 
17
8
  const resourceOwner = await Page.getData(Astro);
18
- const { data } = resourceOwner;
19
- const ownerCollection = resourceOwner.collection as ResourceOwnerCollection;
20
-
21
- // Only show resources directly attached to this system/domain. Domain child resources
22
- // are not flattened, so services inside a child system remain on that system's page.
23
- const resourceGroups = getResourceGroups(data, ownerCollection);
24
-
25
- const hrefForResource = (collection: SystemResourceCollection, id: string, version: string) => {
26
- if (collection === 'entities') {
27
- return buildUrl(`/docs/entities/${id}/${version}`);
28
- }
29
- return buildUrl(`/docs/${collection}/${id}/${version}`);
30
- };
31
-
32
- const ownerTypeLabel = ownerCollection === 'domains' ? 'Domain' : 'System';
33
-
34
- const resources: SystemResourceItem[] = resourceGroups.flatMap(({ collection, items }) =>
35
- items.map((item: any) => ({
36
- collection,
37
- id: item.data.id,
38
- name: item.data.name || item.data.id,
39
- version: item.data.version,
40
- summary: item.data.summary,
41
- icon: item.data?.styles?.icon,
42
- href: hrefForResource(collection, item.data.id, item.data.version),
43
- }))
44
- );
45
9
  ---
46
10
 
47
- <VerticalSideBarLayout title={`${data.name} | Resources`}>
48
- <main class="flex docs-layout min-h-full bg-[rgb(var(--ec-page-bg))]">
49
- <div class="flex docs-layout w-full">
50
- <div class="w-full min-w-0 lg:mr-2 xl:pr-12 py-8 bg-[rgb(var(--ec-page-bg))]">
51
- {/* Header mirrors the resource page (docs/[type]/[id]/[version]/index.astro). */}
52
- <div class="border-b border-[rgb(var(--ec-page-border))] md:pb-3 pt-4">
53
- <div class="flex flex-col gap-4">
54
- <span class="text-xs md:text-sm font-semibold text-[rgb(var(--ec-accent))] capitalize">{ownerTypeLabel}</span>
55
- <h2 id="doc-page-header" class="text-2xl md:text-4xl font-bold text-[rgb(var(--ec-page-text))]">
56
- {data.name} Resources
57
- </h2>
58
- </div>
59
- <p class="text-base pt-4 pb-1 text-[rgb(var(--ec-page-text-muted))] font-light">
60
- {resources.length} {resources.length === 1 ? 'resource' : 'resources'} mapped to the {data.name}{' '}
61
- {ownerTypeLabel.toLowerCase()}.
62
- </p>
63
- </div>
64
-
65
- <div class="pt-8 pb-4 w-full">
66
- <SystemResourcesTable resources={resources} client:only="react" />
67
- </div>
68
- </div>
69
- </div>
70
- </main>
71
- </VerticalSideBarLayout>
11
+ <ResourcesPage resourceOwner={resourceOwner} />
@@ -0,0 +1,13 @@
1
+ ---
2
+ // Services need their own Resources route: /docs/services/[id]/[docType]/[docId] outranks
3
+ // the generic /docs/[type]/[id]/[version]/resources route and would catch these URLs.
4
+ import ResourcesPage from '../../../../[type]/[id]/[version]/resources/_ResourcesPage.astro';
5
+ import { ServicePage } from '../../../../[type]/[id]/[version]/resources/_index.data';
6
+
7
+ export const prerender = ServicePage.prerender;
8
+ export const getStaticPaths = ServicePage.getStaticPaths;
9
+
10
+ const resourceOwner = await ServicePage.getData(Astro);
11
+ ---
12
+
13
+ <ResourcesPage resourceOwner={resourceOwner} />
@@ -11,6 +11,7 @@ import {
11
11
  buildDiagramNavItems,
12
12
  buildResourceDocsSection,
13
13
  buildArchitectureDecisionsSection,
14
+ buildResourcesLink,
14
15
  } from './shared';
15
16
  import { isVisualiserEnabled, isChangelogEnabled } from '@utils/feature';
16
17
  import { pluralizeMessageType } from '@utils/collections/messages';
@@ -131,19 +132,6 @@ export const buildDomainSections = (
131
132
  const sortedReceivesMessages = [...receivesMessages].sort(byResourceName);
132
133
  const renderMessages = shouldRenderSideBarSection(domain, 'messages');
133
134
 
134
- // The Resources page/link only makes sense when the domain has direct resources.
135
- // Keep this aligned with the resource groups rendered by the page.
136
- const hasResources =
137
- subDomains.length > 0 ||
138
- systemsInDomain.length > 0 ||
139
- agentsInDomain.length > 0 ||
140
- dataProductsInDomain.length > 0 ||
141
- allServicesInDomain.length > 0 ||
142
- domainFlows.length > 0 ||
143
- entitiesInDomain.length > 0 ||
144
- sendsMessages.length > 0 ||
145
- receivesMessages.length > 0;
146
-
147
135
  // Diagrams
148
136
  const domainDiagrams = domain.data.diagrams || [];
149
137
  const diagramNavItems = buildDiagramNavItems(domainDiagrams, context.diagrams);
@@ -223,10 +211,7 @@ export const buildDomainSections = (
223
211
  'quick-reference': buildQuickReferenceSection(
224
212
  [
225
213
  { title: 'Overview', href: buildUrl(`/docs/domains/${domain.data.id}/${domain.data.version}`) },
226
- hasResources && {
227
- title: 'Domain Resources',
228
- href: buildUrl(`/docs/domains/${domain.data.id}/${domain.data.version}/resources`),
229
- },
214
+ buildResourcesLink(context, 'domains', domain.data.id, domain.data.version),
230
215
  renderUbiquitousLanguage && {
231
216
  title: 'Ubiquitous Language',
232
217
  href: buildUrl(`/docs/domains/${domain.data.id}/language`),
@@ -239,31 +224,28 @@ export const buildDomainSections = (
239
224
  ].filter(Boolean) as { title: string; href: string }[]
240
225
  ),
241
226
  documentation: docsSection,
242
- architecture: {
243
- type: 'group',
244
- title: 'Architecture',
245
- icon: 'Workflow',
246
- pages: [
247
- {
248
- type: 'item',
249
- title: 'Overview',
250
- href: buildUrl(`/architecture/domains/${domain.data.id}/${domain.data.version}`),
251
- },
252
- // The domain's diagram, with levels from the domains it talks to down to
253
- // its messages
254
- renderVisualiser && {
255
- type: 'item',
256
- title: 'Diagram',
257
- href: buildUrl(`/visualiser/domains/${domain.data.id}/${domain.data.version}`),
258
- },
259
- renderEntities &&
260
- renderVisualiser && {
261
- type: 'item',
262
- title: 'Entity Diagram',
263
- href: buildUrl(`/visualiser/domains/${domain.data.id}/${domain.data.version}/entity-map`),
264
- },
265
- ].filter(Boolean) as ChildRef[],
266
- },
227
+ // Every Architecture link is a visualiser view, so the group is left out when the visualiser is off.
228
+ architecture: renderVisualiser
229
+ ? {
230
+ type: 'group',
231
+ title: 'Architecture',
232
+ icon: 'Workflow',
233
+ pages: [
234
+ // The domain's diagram, with levels from the domains it talks to down to
235
+ // its messages
236
+ {
237
+ type: 'item',
238
+ title: 'Diagram',
239
+ href: buildUrl(`/visualiser/domains/${domain.data.id}/${domain.data.version}`),
240
+ },
241
+ renderEntities && {
242
+ type: 'item',
243
+ title: 'Entity Diagram',
244
+ href: buildUrl(`/visualiser/domains/${domain.data.id}/${domain.data.version}/entity-map`),
245
+ },
246
+ ].filter(Boolean) as ChildRef[],
247
+ }
248
+ : null,
267
249
  diagrams: hasDiagrams
268
250
  ? {
269
251
  type: 'group',
@@ -6,6 +6,7 @@ import {
6
6
  buildResourceDocsSection,
7
7
  shouldRenderSideBarSection,
8
8
  buildArchitectureDecisionsSection,
9
+ buildResourcesLink,
9
10
  } from './shared';
10
11
  import { isChangelogEnabled } from '@utils/feature';
11
12
  import { createVersionedMap, findInMap } from '@utils/collections/util';
@@ -157,6 +158,7 @@ export const buildFlowSections = (flow: CollectionEntry<'flows'>, context: Resou
157
158
  'quick-reference': buildQuickReferenceSection(
158
159
  [
159
160
  { title: 'Overview', href: buildUrl(`/docs/flows/${flow.data.id}/${flow.data.version}`) },
161
+ buildResourcesLink(context, 'flows', flow.data.id, flow.data.version),
160
162
  isChangelogEnabled() &&
161
163
  shouldRenderSideBarSection(flow, 'changelog') && {
162
164
  title: 'Changelog',
@@ -11,6 +11,7 @@ import {
11
11
  buildDiagramNavItems,
12
12
  buildResourceDocsSection,
13
13
  buildArchitectureDecisionsSection,
14
+ buildResourcesLink,
14
15
  } from './shared';
15
16
  import { isVisualiserEnabled, isChangelogEnabled } from '@utils/feature';
16
17
  import { iconFieldsForResource } from '@utils/icon';
@@ -137,6 +138,7 @@ export const buildMessageSections = (
137
138
  title: 'Overview',
138
139
  href: buildUrl(`/docs/${collection}/${message.data.id}/${message.data.version}`),
139
140
  },
141
+ buildResourcesLink(context, collection, message.data.id, message.data.version),
140
142
  isChangelogEnabled() &&
141
143
  shouldRenderSideBarSection(message, 'changelog') && {
142
144
  title: 'Changelog',
@@ -13,6 +13,7 @@ import {
13
13
  buildDiagramNavItems,
14
14
  buildResourceDocsSection,
15
15
  buildArchitectureDecisionsSection,
16
+ buildResourcesLink,
16
17
  } from './shared';
17
18
  import { isVisualiserEnabled, isChangelogEnabled } from '@utils/feature';
18
19
  import { pluralizeMessageType } from '@utils/collections/messages';
@@ -98,6 +99,7 @@ export const buildServiceSections = (
98
99
  const hasAttachments = service.data.attachments && service.data.attachments.length > 0;
99
100
 
100
101
  const hasDataStores = dataStoresInService.length > 0;
102
+
101
103
  const resourceGroups = service.data.resourceGroups || [];
102
104
  const hasResourceGroups = resourceGroups.length > 0;
103
105
 
@@ -126,6 +128,7 @@ export const buildServiceSections = (
126
128
  'quick-reference': buildQuickReferenceSection(
127
129
  [
128
130
  { title: 'Overview', href: buildUrl(docsBasePath) },
131
+ buildResourcesLink(context, 'services', service.data.id, service.data.version),
129
132
  isChangelogEnabled() &&
130
133
  shouldRenderSideBarSection(service, 'changelog') && {
131
134
  title: 'Changelog',
@@ -134,29 +137,26 @@ export const buildServiceSections = (
134
137
  ].filter(Boolean) as { title: string; href: string }[]
135
138
  ),
136
139
  documentation: docsSection,
137
- architecture: {
138
- type: 'group',
139
- title: 'Architecture',
140
- icon: 'Workflow',
141
- pages: [
142
- {
143
- type: 'item',
144
- title: 'Overview',
145
- href: buildUrl(`/architecture/services/${service.data.id}/${service.data.version}`),
146
- },
147
- renderVisualiser && {
148
- type: 'item',
149
- title: 'Diagram',
150
- href: buildUrl(`/visualiser/services/${service.data.id}/${service.data.version}`),
151
- },
152
- renderVisualiser &&
153
- renderEntities && {
154
- type: 'item',
155
- title: 'Entity Diagram',
156
- href: buildUrl(`/visualiser/services/${service.data.id}/${service.data.version}/entity-map`),
157
- },
158
- ].filter(Boolean) as ChildRef[],
159
- },
140
+ // Every Architecture link is a visualiser view, so the group is left out when the visualiser is off.
141
+ architecture: renderVisualiser
142
+ ? {
143
+ type: 'group',
144
+ title: 'Architecture',
145
+ icon: 'Workflow',
146
+ pages: [
147
+ {
148
+ type: 'item',
149
+ title: 'Diagram',
150
+ href: buildUrl(`/visualiser/services/${service.data.id}/${service.data.version}`),
151
+ },
152
+ renderEntities && {
153
+ type: 'item',
154
+ title: 'Entity Diagram',
155
+ href: buildUrl(`/visualiser/services/${service.data.id}/${service.data.version}/entity-map`),
156
+ },
157
+ ].filter(Boolean) as ChildRef[],
158
+ }
159
+ : null,
160
160
  diagrams: hasDiagrams
161
161
  ? {
162
162
  type: 'group',
@@ -79,8 +79,21 @@ export type ResourceGroupContext = {
79
79
  teams?: CollectionEntry<'teams'>[];
80
80
  resourceDocs: ResourceDocEntry[];
81
81
  resourceDocCategories: ResourceDocCategoryEntry[];
82
+ /** `collection:id:version` of every resource with a Resources page (see getResourcesPageKeys). */
83
+ resourcesPages?: Set<string>;
82
84
  };
83
85
 
86
+ /** The Quick Reference link to a resource's Resources page, when it has one. */
87
+ export const buildResourcesLink = (
88
+ context: Pick<ResourceGroupContext, 'resourcesPages'>,
89
+ collection: string,
90
+ id: string,
91
+ version: string
92
+ ) =>
93
+ context.resourcesPages?.has(`${collection}:${id}:${version}`)
94
+ ? { title: 'Resources', href: buildUrl(`/docs/${collection}/${id}/${version}/resources`) }
95
+ : false;
96
+
84
97
  export const buildQuickReferenceSection = (items: { title: string; href: string }[]): NavNode => ({
85
98
  type: 'group',
86
99
  title: 'Quick Reference',
@@ -10,6 +10,7 @@ import {
10
10
  buildDiagramNavItems,
11
11
  buildResourceDocsSection,
12
12
  buildArchitectureDecisionsSection,
13
+ buildResourcesLink,
13
14
  } from './shared';
14
15
  import { isChangelogEnabled, isVisualiserEnabled } from '@utils/feature';
15
16
  import { customIconFieldsForResource } from '@utils/icon';
@@ -74,11 +75,6 @@ export const buildSystemSections = (
74
75
  const containersInSystem = [...(system.data.containers || [])].sort(byResourceName);
75
76
  const renderContainers = containersInSystem.length > 0 && shouldRenderSideBarSection(system, 'containers');
76
77
 
77
- // The Resources page/link only makes sense when the system actually has resources
78
- // attached (services, flows, entities or data stores). Mirrors what the page renders.
79
- const hasResources =
80
- servicesInSystem.length > 0 || flowsInSystem.length > 0 || entitiesInSystem.length > 0 || containersInSystem.length > 0;
81
-
82
78
  const systemDiagrams = system.data.diagrams || [];
83
79
  const diagramNavItems = buildDiagramNavItems(systemDiagrams, context.diagrams);
84
80
  const hasDiagrams = diagramNavItems.length > 0 && shouldRenderSideBarSection(system, 'diagrams');
@@ -138,10 +134,7 @@ export const buildSystemSections = (
138
134
  title: 'Overview',
139
135
  href: buildUrl(`/docs/systems/${system.data.id}/${system.data.version}`),
140
136
  },
141
- hasResources && {
142
- title: 'System Resources',
143
- href: buildUrl(`/docs/systems/${system.data.id}/${system.data.version}/resources`),
144
- },
137
+ buildResourcesLink(context, 'systems', system.data.id, system.data.version),
145
138
  isChangelogEnabled() &&
146
139
  shouldRenderSideBarSection(system, 'changelog') && {
147
140
  title: 'Changelog',
@@ -150,24 +143,22 @@ export const buildSystemSections = (
150
143
  ].filter(Boolean) as { title: string; href: string }[]
151
144
  ),
152
145
  documentation: docsSection,
153
- architecture: {
154
- type: 'group',
155
- title: 'Architecture',
156
- icon: 'Workflow',
157
- pages: [
158
- {
159
- type: 'item',
160
- title: 'Overview',
161
- href: buildUrl(`/architecture/systems/${system.data.id}/${system.data.version}`),
162
- },
163
- // The system's diagram, with levels from its context down to its messages
164
- renderVisualiser && {
165
- type: 'item',
166
- title: 'Diagram',
167
- href: buildUrl(`/visualiser/systems/${system.data.id}/${system.data.version}`),
168
- },
169
- ].filter(Boolean) as ChildRef[],
170
- },
146
+ // Every Architecture link is a visualiser view, so the group is left out when the visualiser is off.
147
+ architecture: renderVisualiser
148
+ ? {
149
+ type: 'group',
150
+ title: 'Architecture',
151
+ icon: 'Workflow',
152
+ pages: [
153
+ // The system's diagram, with levels from its context down to its messages
154
+ {
155
+ type: 'item',
156
+ title: 'Diagram',
157
+ href: buildUrl(`/visualiser/systems/${system.data.id}/${system.data.version}`),
158
+ },
159
+ ],
160
+ }
161
+ : null,
171
162
  diagrams: hasDiagrams
172
163
  ? {
173
164
  type: 'group',
@@ -38,9 +38,11 @@ import { iconFieldsForResource } from '@utils/icon';
38
38
  import {
39
39
  buildQuickReferenceSection,
40
40
  buildResourceDocsSection,
41
+ buildResourcesLink,
41
42
  shouldRenderSideBarSection,
42
43
  withArchitectureDecisionsSection,
43
44
  } from './builders/shared';
45
+ import { getResourcesPageKeys } from '@utils/collections/resource-owners';
44
46
  import { isArchitectureGraphEnabled, isChangelogEnabled } from '@utils/feature';
45
47
  import { collectMessageIdsWithFieldUsage } from '@utils/collections/field-usage';
46
48
 
@@ -355,6 +357,8 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
355
357
  teams,
356
358
  resourceDocs,
357
359
  resourceDocCategories,
360
+ // Same rule the Resources pages are built from, so a link never points at a missing page.
361
+ resourcesPages: await getResourcesPageKeys(),
358
362
  };
359
363
 
360
364
  // Process all domains with their owners first (async)
@@ -801,6 +805,7 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
801
805
  title: 'Overview',
802
806
  href: buildUrl(`/docs/${channel.collection}/${channel.data.id}/${channel.data.version}`),
803
807
  },
808
+ buildResourcesLink(context, 'channels', channel.data.id, channel.data.version),
804
809
  isChangelogEnabled() &&
805
810
  shouldRenderSideBarSection(channel, 'changelog') && {
806
811
  title: 'Changelog',
@@ -171,7 +171,7 @@
171
171
  * scanning, so we explicitly declare them here.
172
172
  *
173
173
  * Colors used: orange, blue, green, emerald, amber, violet, pink, purple, gray, yellow, teal, cyan, indigo
174
- * Used in: SchemaListItem, Grids/components, SchemaExplorer, MessageGrid
174
+ * Used in: SchemaListItem, SchemaExplorer
175
175
  */
176
176
  @source inline("bg-{orange,blue,green,emerald,amber,violet,pink,purple,gray,yellow,teal,cyan,indigo}-{50,100}");
177
177
  @source inline("bg-{orange,blue,green,emerald,amber,violet,pink,purple,gray,yellow,teal,cyan,indigo}-500/{10,20}");
@@ -180,7 +180,7 @@
180
180
  @source inline("ring-2");
181
181
  @source inline("ring-{orange,blue,green,emerald,amber,violet,pink,purple,gray,yellow,teal,cyan,indigo}-500");
182
182
 
183
- /* Border color variants for MessageGrid cards */
183
+ /* Border color variants built at runtime (e.g. hover borders in MessageTableColumns) */
184
184
  @source inline("border-{orange,blue,green,emerald,amber,violet,pink,purple,gray,yellow,teal,cyan,indigo}-{200,300}");
185
185
  @source inline("{dark:,}border-{orange,blue,green,emerald,amber,violet,pink,purple,gray,yellow,teal,cyan,indigo}-500/{30,50}");
186
186
  @source inline("{hover:,dark:hover:,}border-{orange,blue,green,emerald,amber,violet,pink,purple,gray,yellow,teal,cyan,indigo}-{300,400,500,500/50}");
@@ -1,6 +1,13 @@
1
1
  import { getCollection } from 'astro:content';
2
2
  import type { CollectionEntry } from 'astro:content';
3
- import { getItemsFromCollectionByIdAndSemverOrLatest, createVersionedMap, satisfies } from './util';
3
+ import {
4
+ getItemsFromCollectionByIdAndSemverOrLatest,
5
+ createVersionedMap,
6
+ findInMap,
7
+ getVersionedMap,
8
+ satisfies,
9
+ uniqueResources,
10
+ } from './util';
4
11
  import type { CollectionMessageTypes } from '@types';
5
12
 
6
13
  const CACHE_ENABLED = process.env.DISABLE_EVENTCATALOG_CACHE !== 'true';
@@ -195,3 +202,51 @@ export const getChannelChain = (
195
202
  }
196
203
  return [];
197
204
  };
205
+
206
+ /**
207
+ * What a channel connects: the services and agents that send to it (`sends[].to`) or receive
208
+ * from it (`receives[].from`), and the messages that travel through it, from those pointers or
209
+ * from messages that list the channel (`data.messages` from getChannels). Pass raw entries:
210
+ * hydration resolves sends/receives to messages and drops the channel pointers. Pointers
211
+ * without a version mean the latest channel or message.
212
+ */
213
+ export const getChannelConnections = (
214
+ channel: Channel,
215
+ catalog: {
216
+ endpoints: Array<CollectionEntry<'services'> | CollectionEntry<'agents'>>;
217
+ channels: Channel[];
218
+ messages: CollectionEntry<CollectionMessageTypes>[];
219
+ }
220
+ ) => {
221
+ const channelMap = getVersionedMap(catalog.channels);
222
+ const messageMap = getVersionedMap(catalog.messages);
223
+ const isThisChannel = (pointer: { id: string; version?: string }) => {
224
+ const match = findInMap(channelMap, pointer.id, pointer.version);
225
+ return match?.data.id === channel.data.id && match?.data.version === channel.data.version;
226
+ };
227
+
228
+ const producers: Array<CollectionEntry<'services'> | CollectionEntry<'agents'>> = [];
229
+ const consumers: Array<CollectionEntry<'services'> | CollectionEntry<'agents'>> = [];
230
+ const messages: CollectionEntry<CollectionMessageTypes>[] = [];
231
+ const addMessage = (pointer: { id: string; version?: string }) => {
232
+ const message = findInMap(messageMap, pointer.id, pointer.version);
233
+ if (message) messages.push(message);
234
+ };
235
+
236
+ for (const endpoint of catalog.endpoints) {
237
+ for (const send of (endpoint.data.sends || []) as any[]) {
238
+ if (!(send.to || []).some(isThisChannel)) continue;
239
+ producers.push(endpoint);
240
+ addMessage(send);
241
+ }
242
+ for (const receive of (endpoint.data.receives || []) as any[]) {
243
+ if (!(receive.from || []).some(isThisChannel)) continue;
244
+ consumers.push(endpoint);
245
+ addMessage(receive);
246
+ }
247
+ }
248
+
249
+ for (const message of (channel.data as any).messages || []) addMessage(message);
250
+
251
+ return { producers: uniqueResources(producers), consumers: uniqueResources(consumers), messages: uniqueResources(messages) };
252
+ };
@@ -1,6 +1,6 @@
1
1
  import { getCollection } from 'astro:content';
2
2
  import type { CollectionEntry } from 'astro:content';
3
- import { createVersionedMap, findInMap } from '@utils/collections/util';
3
+ import { createVersionedMap, findInMap, getVersionedMap, uniqueResources } from '@utils/collections/util';
4
4
  import { getDomains } from './domains';
5
5
  import { getServices } from './services';
6
6
  import { getAgents } from './agents';
@@ -147,6 +147,55 @@ export const getFlowsNotInAnyResource = async (): Promise<Flow[]> => {
147
147
  return flowsNotInAnyResource;
148
148
  };
149
149
 
150
+ /**
151
+ * Finds the flows with a step that points at the service. A step without a version points
152
+ * at the latest service version, so `services` must include every version.
153
+ */
154
+ export const getFlowsWithServiceStep = (
155
+ service: CollectionEntry<'services'>,
156
+ flows: Flow[],
157
+ services: CollectionEntry<'services'>[]
158
+ ): Flow[] => {
159
+ const serviceMap = getVersionedMap(services);
160
+ return flows.filter((flow) =>
161
+ (flow.data.steps || []).some((step: any) => {
162
+ if (!step.service) return false;
163
+ const match = findInMap(serviceMap, step.service.id, step.service.version);
164
+ return match?.data.id === service.data.id && match?.data.version === service.data.version;
165
+ })
166
+ );
167
+ };
168
+
169
+ /** A catalog entry a flow step points at (a service, message, agent, data store, data product or flow). */
170
+ type FlowStepResource = { collection: string; data: { id: string; version: string; [key: string]: any } };
171
+
172
+ /**
173
+ * The catalog resources a flow's steps point at, each listed once in step order: messages,
174
+ * services, agents, data stores, data products and sub-flows (the same steps the flow's
175
+ * sidebar lists). Expects steps as getFlows returns them, where messages, agents, data stores
176
+ * and data products are already resolved; services and sub-flows are resolved here, with a
177
+ * pointer without a version meaning the latest.
178
+ */
179
+ export const getFlowStepResources = (
180
+ flow: Flow,
181
+ catalog: { services: CollectionEntry<'services'>[]; flows: Flow[] }
182
+ ): FlowStepResource[] => {
183
+ const serviceMap = getVersionedMap(catalog.services);
184
+ const flowMap = getVersionedMap(catalog.flows);
185
+ const resources: FlowStepResource[] = [];
186
+
187
+ for (const step of (flow.data.steps || []) as any[]) {
188
+ const resource = step.service
189
+ ? findInMap(serviceMap, step.service.id, step.service.version)
190
+ : step.flow
191
+ ? findInMap(flowMap, step.flow.id, step.flow.version)
192
+ : [step.message, step.agent, step.container, step.dataProduct].find(Array.isArray)?.[0];
193
+ if (resource?.data) resources.push(resource);
194
+ }
195
+
196
+ return uniqueResources(resources);
197
+ };
198
+
150
199
  /** A flow that includes a message in one of its steps, reduced to what the schema pages display. */
151
200
  export interface FlowReference {
152
201
  id: string;
@@ -0,0 +1,60 @@
1
+ import type { CollectionEntry } from 'astro:content';
2
+ import type { CollectionMessageTypes } from '@types';
3
+ import { findInMap, getVersionedMap, uniqueResources } from './util';
4
+ import { getTriggeredByOfMessage, getTriggersOfMessage, messageMatchesPointer, type MessageReceiver } from './message-triggers';
5
+
6
+ type Message = CollectionEntry<CollectionMessageTypes>;
7
+ type Pointer = { id: string; version?: string };
8
+
9
+ /**
10
+ * What a message connects to, beyond the producers and consumers getEvents/getCommands/getQueries
11
+ * already attach: the channels it travels on (its own `channels`, and the `to`/`from` channels of
12
+ * services, agents and domains that send or receive it), the flows with a step for it, and the
13
+ * messages it triggers or is triggered by. Receivers must be raw collection entries, because
14
+ * hydration drops the channel and trigger pointers on sends/receives.
15
+ */
16
+ export const getMessageConnections = (
17
+ message: Message,
18
+ catalog: {
19
+ receivers: MessageReceiver[];
20
+ messages: Message[];
21
+ channels: CollectionEntry<'channels'>[];
22
+ flows: CollectionEntry<'flows'>[];
23
+ }
24
+ ) => {
25
+ const channelMap = getVersionedMap(catalog.channels);
26
+ const resolveChannels = (pointers: Pointer[] = []) =>
27
+ pointers.map((pointer) => findInMap(channelMap, pointer.id, pointer.version)).filter((channel) => !!channel);
28
+
29
+ const channels = [...resolveChannels((message.data as any).channels)];
30
+ for (const receiver of catalog.receivers) {
31
+ for (const send of ((receiver.data as any).sends || []) as Array<Pointer & { to?: Pointer[] }>) {
32
+ if (messageMatchesPointer(message, send)) channels.push(...resolveChannels(send.to));
33
+ }
34
+ for (const receive of ((receiver.data as any).receives || []) as Array<Pointer & { from?: Pointer[] }>) {
35
+ if (messageMatchesPointer(message, receive)) channels.push(...resolveChannels(receive.from));
36
+ }
37
+ }
38
+
39
+ const flows = catalog.flows.filter((flow) =>
40
+ ((flow.data.steps || []) as any[]).some((step) => {
41
+ const stepMessage = Array.isArray(step.message) ? step.message[0] : undefined;
42
+ return (
43
+ stepMessage?.collection === message.collection &&
44
+ stepMessage?.data.id === message.data.id &&
45
+ stepMessage?.data.version === message.data.version
46
+ );
47
+ })
48
+ );
49
+
50
+ return {
51
+ channels: uniqueResources(channels as CollectionEntry<'channels'>[]),
52
+ flows: uniqueResources(flows),
53
+ triggers: uniqueResources(
54
+ getTriggersOfMessage(catalog.receivers, message, catalog.messages).map((trigger) => trigger.message)
55
+ ),
56
+ triggeredBy: uniqueResources(
57
+ getTriggeredByOfMessage(catalog.receivers, message, catalog.messages).map((trigger) => trigger.message)
58
+ ),
59
+ };
60
+ };
@@ -23,7 +23,8 @@ export interface MessageTrigger {
23
23
  condition?: string;
24
24
  }
25
25
 
26
- const messageMatchesPointer = (message: Message, pointer: { id: string; version?: string }) => {
26
+ /** Whether a pointer points at the message; a pointer without a version (or `latest`) means the latest version. */
27
+ export const messageMatchesPointer = (message: Message, pointer: { id: string; version?: string }) => {
27
28
  if (pointer.id !== message.data.id) return false;
28
29
 
29
30
  if (!pointer.version || pointer.version === 'latest') {