@eventcatalog/core 3.48.4 → 4.0.0-beta.1

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 (109) 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/catalog-to-astro-content-directory.cjs +1 -1
  6. package/dist/chunk-BWN544LB.js +73 -0
  7. package/dist/{chunk-XNVSSL4W.js → chunk-JTZT2SDF.js} +1 -1
  8. package/dist/{chunk-JY4H7A4S.js → chunk-M2ZRT7KQ.js} +1 -1
  9. package/dist/{chunk-SY4XL6OQ.js → chunk-MBVTS76Q.js} +1 -1
  10. package/dist/{chunk-KDLSBQRP.js → chunk-TZTPGQGL.js} +1 -1
  11. package/dist/{chunk-FNDSHOV2.js → chunk-U5OTNDVQ.js} +1 -1
  12. package/dist/constants.cjs +1 -1
  13. package/dist/constants.js +1 -1
  14. package/dist/core-node-modules.cjs +108 -0
  15. package/dist/core-node-modules.d.cts +7 -0
  16. package/dist/core-node-modules.d.ts +7 -0
  17. package/dist/core-node-modules.js +8 -0
  18. package/dist/docs/development/components/components/15-prompt.md +5 -1
  19. package/dist/eventcatalog.cjs +124 -48
  20. package/dist/eventcatalog.js +26 -15
  21. package/dist/generate.cjs +1 -1
  22. package/dist/generate.js +3 -3
  23. package/dist/utils/cli-logger.cjs +1 -1
  24. package/dist/utils/cli-logger.js +2 -2
  25. package/eventcatalog/astro.config.mjs +27 -23
  26. package/eventcatalog/public/icons/languages/java.svg +1 -0
  27. package/eventcatalog/public/icons/payments/stripe.svg +1 -0
  28. package/eventcatalog/src/components/Badge.astro +10 -10
  29. package/eventcatalog/src/components/CopyAsMarkdown.tsx +154 -0
  30. package/eventcatalog/src/components/Grids/DomainGrid.tsx +207 -44
  31. package/eventcatalog/src/components/Grids/MessageGrid.tsx +0 -20
  32. package/eventcatalog/src/components/Grids/SystemGrid.tsx +185 -0
  33. package/eventcatalog/src/components/MDX/ADRTable/ADRTable.astro +99 -0
  34. package/eventcatalog/src/components/MDX/ADRTable/ADRTable.client.tsx +400 -0
  35. package/eventcatalog/src/components/MDX/ContextDiagram/ContextDiagramPortal.tsx +21 -0
  36. package/eventcatalog/src/components/MDX/NodeGraph/AstroNodeGraph.tsx +1 -0
  37. package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.astro +42 -6
  38. package/eventcatalog/src/components/MDX/Prompt/Prompt.astro +62 -9
  39. package/eventcatalog/src/components/MDX/ResourceRef/ResourceRef.astro +8 -1
  40. package/eventcatalog/src/components/MDX/SystemContextMap/SystemContextMapPortal.tsx +22 -0
  41. package/eventcatalog/src/components/MDX/components.tsx +7 -0
  42. package/eventcatalog/src/components/SchemaExplorer/SchemaExplorer.tsx +1 -1
  43. package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +67 -37
  44. package/eventcatalog/src/components/Tables/Discover/DiscoverTable.tsx +47 -0
  45. package/eventcatalog/src/components/Tables/Discover/FilterComponents.tsx +10 -2
  46. package/eventcatalog/src/components/Tables/Discover/columns.tsx +115 -27
  47. package/eventcatalog/src/components/Tables/Discover/index.ts +7 -1
  48. package/eventcatalog/src/components/Tables/SystemResources/SystemResourcesTable.tsx +267 -0
  49. package/eventcatalog/src/content.config.ts +96 -8
  50. package/eventcatalog/src/enterprise/collections/resource-docs-utils.ts +4 -1
  51. package/eventcatalog/src/enterprise/custom-documentation/pages/docs/custom/index.astro +12 -13
  52. package/eventcatalog/src/enterprise/custom-documentation/utils/custom-docs.ts +6 -0
  53. package/eventcatalog/src/enterprise/feature.ts +3 -1
  54. package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +96 -140
  55. package/eventcatalog/src/layouts/VisualiserLayout.astro +66 -0
  56. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.ts +4 -2
  57. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/index.astro +2 -0
  58. package/eventcatalog/src/pages/diagrams/[id]/[version]/index.astro +15 -15
  59. package/eventcatalog/src/pages/discover/[type]/_index.data.ts +1 -0
  60. package/eventcatalog/src/pages/discover/[type]/index.astro +47 -2
  61. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/[docVersion]/index.astro +15 -14
  62. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/index.astro +13 -14
  63. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/_index.data.ts +1 -0
  64. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/changelog/_index.data.ts +11 -1
  65. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +81 -26
  66. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_index.data.ts +89 -0
  67. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/index.astro +85 -0
  68. package/eventcatalog/src/pages/docs/[type]/[id]/[version].md.ts +2 -0
  69. package/eventcatalog/src/pages/docs/[type]/[id]/[version].mdx.ts +2 -0
  70. package/eventcatalog/src/pages/docs/[type]/[id]/_index.data.ts +1 -0
  71. package/eventcatalog/src/pages/docs/teams/[id]/index.astro +24 -1
  72. package/eventcatalog/src/pages/docs/users/[id]/index.astro +24 -1
  73. package/eventcatalog/src/pages/index.astro +30 -7
  74. package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/_index.data.ts +1 -0
  75. package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/index.astro +1 -1
  76. package/eventcatalog/src/pages/visualiser/[type]/[id]/[version].mermaid.ts +3 -0
  77. package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/systems-context/_index.data.ts +94 -0
  78. package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/systems-context/index.astro +51 -0
  79. package/eventcatalog/src/pages/visualiser/system-context-map/index.astro +41 -0
  80. package/eventcatalog/src/pages/visualiser/systems/[id]/[version]/context/_index.data.ts +91 -0
  81. package/eventcatalog/src/pages/visualiser/systems/[id]/[version]/context/index.astro +51 -0
  82. package/eventcatalog/src/stores/sidebar-store/builders/adr.ts +3 -1
  83. package/eventcatalog/src/stores/sidebar-store/builders/agent.ts +3 -1
  84. package/eventcatalog/src/stores/sidebar-store/builders/container.ts +1 -1
  85. package/eventcatalog/src/stores/sidebar-store/builders/data-product.ts +1 -1
  86. package/eventcatalog/src/stores/sidebar-store/builders/domain.ts +127 -49
  87. package/eventcatalog/src/stores/sidebar-store/builders/message.ts +1 -1
  88. package/eventcatalog/src/stores/sidebar-store/builders/service.ts +4 -2
  89. package/eventcatalog/src/stores/sidebar-store/builders/shared.ts +2 -1
  90. package/eventcatalog/src/stores/sidebar-store/builders/system.ts +166 -0
  91. package/eventcatalog/src/stores/sidebar-store/state.ts +99 -6
  92. package/eventcatalog/src/types/index.ts +2 -0
  93. package/eventcatalog/src/utils/collection-colors.ts +6 -1
  94. package/eventcatalog/src/utils/collections/domains.ts +35 -0
  95. package/eventcatalog/src/utils/collections/icons.ts +4 -5
  96. package/eventcatalog/src/utils/collections/systems.ts +110 -0
  97. package/eventcatalog/src/utils/collections/teams.ts +15 -10
  98. package/eventcatalog/src/utils/collections/users.ts +7 -4
  99. package/eventcatalog/src/utils/collections/util.ts +2 -0
  100. package/eventcatalog/src/utils/icon-map.ts +20 -0
  101. package/eventcatalog/src/utils/icon.ts +11 -0
  102. package/eventcatalog/src/utils/node-graphs/container-node-graph.ts +12 -2
  103. package/eventcatalog/src/utils/node-graphs/domains-node-graph.ts +0 -170
  104. package/eventcatalog/src/utils/node-graphs/system-context-node-graph.ts +400 -0
  105. package/eventcatalog/src/utils/node-graphs/systems-node-graph.ts +208 -0
  106. package/eventcatalog/src/utils/node-graphs/utils/utils.ts +12 -0
  107. package/eventcatalog/src/utils/page-loaders/page-data-loader.ts +2 -0
  108. package/package.json +17 -14
  109. package/eventcatalog/src/pages/visualiser/context-map/index.astro +0 -30
@@ -143,6 +143,72 @@ const getColor = (collection: string) => {
143
143
  document.addEventListener('astro:page-load', setupSidebarToggle);
144
144
  </script>
145
145
 
146
+ <!--
147
+ Bridge Astro's view-transition router onto `window` so the standalone
148
+ visualiser package (which can't import `astro:*`) can trigger soft, animated
149
+ client navigations instead of hard page reloads. Used by node "drill-in"
150
+ clicks (e.g. a system node on the System Diagram -> that system's Map).
151
+ -->
152
+ <script>
153
+ import { navigate } from 'astro:transitions/client';
154
+
155
+ function bridgeNavigate() {
156
+ // @ts-ignore - intentional global bridge for the decoupled visualiser island
157
+ window.__ecNavigate = (url: string) => navigate(url);
158
+ }
159
+
160
+ bridgeNavigate();
161
+ document.addEventListener('astro:page-load', bridgeNavigate);
162
+ </script>
163
+
164
+ <style is:global>
165
+ /*
166
+ * "Drill into detail" transition for the visualiser graph container.
167
+ *
168
+ * Both the System Diagram and a system's Map render their graph inside
169
+ * a portal tagged with `transition:name="visualiser-graph"`. When navigating
170
+ * between them, Astro morphs that shared container: the outgoing graph fades
171
+ * and scales back, the incoming graph fades and scales up into place — reading
172
+ * as descending one level deeper rather than a flat cross-fade.
173
+ */
174
+ @keyframes ec-visualiser-drill-out {
175
+ from {
176
+ opacity: 1;
177
+ transform: scale(1);
178
+ }
179
+ to {
180
+ opacity: 0;
181
+ transform: scale(1.04);
182
+ }
183
+ }
184
+
185
+ @keyframes ec-visualiser-drill-in {
186
+ from {
187
+ opacity: 0;
188
+ transform: scale(0.96);
189
+ }
190
+ to {
191
+ opacity: 1;
192
+ transform: scale(1);
193
+ }
194
+ }
195
+
196
+ ::view-transition-old(visualiser-graph) {
197
+ animation: ec-visualiser-drill-out 0.28s ease both;
198
+ }
199
+
200
+ ::view-transition-new(visualiser-graph) {
201
+ animation: ec-visualiser-drill-in 0.28s ease both;
202
+ }
203
+
204
+ @media (prefers-reduced-motion: reduce) {
205
+ ::view-transition-old(visualiser-graph),
206
+ ::view-transition-new(visualiser-graph) {
207
+ animation-duration: 0.01ms;
208
+ }
209
+ }
210
+ </style>
211
+
146
212
  <style>
147
213
  .visualiser-full-bleed {
148
214
  margin-left: calc(var(--ec-app-content-padding-left, 5rem) * -1);
@@ -4,8 +4,9 @@ import type { PageTypes } from '@types';
4
4
  import { pageDataLoader } from '@utils/page-loaders/page-data-loader';
5
5
  import { getDomains } from '@utils/collections/domains';
6
6
  import { getServices } from '@utils/collections/services';
7
+ import { getSystems } from '@utils/collections/systems';
7
8
 
8
- const architecturePageTypes: PageTypes[] = ['services', 'domains'];
9
+ const architecturePageTypes: PageTypes[] = ['services', 'domains', 'systems'];
9
10
 
10
11
  /**
11
12
  * Documentation page class for all collection types with versioning
@@ -18,8 +19,9 @@ export class Page extends HybridPage {
18
19
 
19
20
  const domains = await getDomains({ enrichServices: true });
20
21
  const services = await getServices();
22
+ const systems = await getSystems();
21
23
 
22
- const pageData = [services, domains];
24
+ const pageData = [services, domains, systems];
23
25
 
24
26
  return pageData.flatMap((items, index) =>
25
27
  items.map((item) => ({
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  import DomainGrid from '@components/Grids/DomainGrid';
3
+ import SystemGrid from '@components/Grids/SystemGrid';
3
4
  import MessageGrid from '@components/Grids/MessageGrid';
4
5
  import { getSpecificationsForService } from '@utils/collections/services';
5
6
 
@@ -25,6 +26,7 @@ const specifications = type === 'services' ? getSpecificationsForService(props)
25
26
  <div class="flex docs-layout w-full">
26
27
  <div class="w-full lg:mr-2 pr-24 py-8 bg-[rgb(var(--ec-page-bg))]">
27
28
  {type === 'domains' && <DomainGrid domain={domain} client:load />}
29
+ {type === 'systems' && <SystemGrid system={props} client:load />}
28
30
  {
29
31
  (type === 'agents' || type === 'services') && (
30
32
  <MessageGrid service={props} specifications={specifications} client:load />
@@ -35,7 +35,7 @@ const chatQuery = `Tell me about the "${props.data.name}" diagram (version ${pro
35
35
  >
36
36
  <main class="diagram-content w-full lg:mr-2 pr-24 py-8 bg-[rgb(var(--ec-page-bg))]">
37
37
  <div class="border-b border-[rgb(var(--ec-page-border))] md:pb-2">
38
- <div class="flex items-start justify-between gap-4">
38
+ <div class="flex items-start gap-4">
39
39
  <div class="min-w-0">
40
40
  <h1 class="text-2xl md:text-4xl font-bold text-[rgb(var(--ec-page-text))] truncate">
41
41
  {props.data.name}
@@ -45,20 +45,20 @@ const chatQuery = `Tell me about the "${props.data.name}" diagram (version ${pro
45
45
  <p class="pt-2 text-base font-light text-[rgb(var(--ec-page-text-muted))]">{props.data.summary}</p>
46
46
  )
47
47
  }
48
- </div>
49
-
50
- <div class="flex items-center gap-2">
51
- <CopyAsMarkdown
52
- client:only="react"
53
- schemas={[]}
54
- chatQuery={chatQuery}
55
- chatEnabled={chatEnabled}
56
- editUrl=""
57
- markdownDownloadEnabled={markdownDownloadEnabled}
58
- rssFeedEnabled={false}
59
- preferChatAsDefault={chatEnabled}
60
- chatButtonText="Ask about this diagram"
61
- />
48
+ <div class="pt-4">
49
+ <CopyAsMarkdown
50
+ client:only="react"
51
+ variant="toolbar"
52
+ schemas={[]}
53
+ chatQuery={chatQuery}
54
+ chatEnabled={chatEnabled}
55
+ editUrl=""
56
+ markdownDownloadEnabled={markdownDownloadEnabled}
57
+ rssFeedEnabled={false}
58
+ preferChatAsDefault={chatEnabled}
59
+ chatButtonText="Ask about this diagram"
60
+ />
61
+ </div>
62
62
  </div>
63
63
  </div>
64
64
  </div>
@@ -35,6 +35,7 @@ export class Page extends HybridPage {
35
35
  'commands',
36
36
  'queries',
37
37
  'domains',
38
+ 'systems',
38
39
  'services',
39
40
  'external-systems',
40
41
  'flows',
@@ -8,12 +8,20 @@ import { getServices } from '@utils/collections/services';
8
8
  import { getQueries } from '@utils/collections/queries';
9
9
  import { getContainers } from '@utils/collections/containers';
10
10
  import { getDataProducts } from '@utils/collections/data-products';
11
+ import { getFlows } from '@utils/collections/flows';
11
12
  import { getUsers } from '@utils/collections/users';
12
13
  import { getTeams } from '@utils/collections/teams';
13
14
  import { getChannels } from '@utils/collections/channels';
15
+ import { getSystems } from '@utils/collections/systems';
14
16
  import VerticalSideBarLayout from '@layouts/VerticalSideBarLayout.astro';
15
- import { DiscoverTable, type DiscoverTableData, type CollectionType } from '@components/Tables/Discover';
17
+ import {
18
+ DiscoverTable,
19
+ type CollectionKindOption,
20
+ type DiscoverTableData,
21
+ type CollectionType,
22
+ } from '@components/Tables/Discover';
16
23
  import config from '@config';
24
+ import { buildUrl } from '@utils/url-builder';
17
25
  import { Page } from './_index.data';
18
26
 
19
27
  export const prerender = Page.prerender;
@@ -28,8 +36,10 @@ const agents = await getAgents();
28
36
  const adrs = await getAdrs();
29
37
  const services = await getServices();
30
38
  const domains = await getDomains({ getAllVersions: false });
39
+ const systems = await getSystems({ getAllVersions: false });
31
40
  const containers = await getContainers();
32
41
  const dataProducts = await getDataProducts();
42
+ const flows = await getFlows();
33
43
  const channels = await getChannels();
34
44
  const users = await getUsers();
35
45
  const teams = await getTeams();
@@ -87,6 +97,16 @@ const typeConfig: Record<
87
97
  { id: 'isDeprecated', label: 'Is Deprecated' },
88
98
  ],
89
99
  },
100
+ systems: {
101
+ label: 'Systems',
102
+ description: 'Explore the systems that group services and flows across your architecture, independently of domains.',
103
+ propertyOptions: [
104
+ { id: 'hasServices', label: 'Has Services' },
105
+ { id: 'hasFlows', label: 'Has Flows' },
106
+ { id: 'hasOwners', label: 'Has Owners' },
107
+ { id: 'isDeprecated', label: 'Is Deprecated' },
108
+ ],
109
+ },
90
110
  services: {
91
111
  label: 'Services',
92
112
  description: 'Browse the internal services in your catalog and inspect the capabilities they expose.',
@@ -161,6 +181,28 @@ const typeConfig: Record<
161
181
  };
162
182
 
163
183
  const currentTypeConfig = typeConfig[type] || typeConfig.events;
184
+ const internalServices = services.filter((service) => !service.data.externalSystem);
185
+ const externalSystems = services.filter((service) => service.data.externalSystem);
186
+
187
+ const collectionKinds = [
188
+ { id: 'domains', label: typeConfig.domains.label, count: domains.length },
189
+ { id: 'systems', label: typeConfig.systems.label, count: systems.length },
190
+ { id: 'services', label: typeConfig.services.label, count: internalServices.length },
191
+ { id: 'external-systems', label: typeConfig['external-systems'].label, count: externalSystems.length },
192
+ { id: 'agents', label: typeConfig.agents.label, count: agents.length },
193
+ { id: 'adrs', label: typeConfig.adrs.label, count: adrs.length },
194
+ { id: 'data-products', label: typeConfig['data-products'].label, count: dataProducts.length },
195
+ { id: 'containers', label: typeConfig.containers.label, count: containers.length },
196
+ { id: 'events', label: typeConfig.events.label, count: events.length },
197
+ { id: 'commands', label: typeConfig.commands.label, count: commands.length },
198
+ { id: 'queries', label: typeConfig.queries.label, count: queries.length },
199
+ { id: 'flows', label: typeConfig.flows.label, count: flows.length },
200
+ ]
201
+ .filter((kind) => kind.count > 0)
202
+ .map((kind) => ({
203
+ ...kind,
204
+ href: buildUrl(`/discover/${kind.id}`),
205
+ })) as CollectionKindOption[];
164
206
 
165
207
  // External systems reuse the user's `services.tableConfiguration` so customisations apply to both tabs
166
208
  const tableConfigurationKey = type === 'external-systems' ? 'services' : type;
@@ -269,7 +311,8 @@ const tableData = enrichedData.map((d: any) => ({
269
311
  collection: d.collection,
270
312
  owners: (d.data?.owners || []).map(mapOwner).filter(Boolean),
271
313
  hasOwners: (d.data?.owners || []).length > 0,
272
- hasServices: type === 'domains' ? (d.data?.services || []).length > 0 : false,
314
+ hasServices: type === 'domains' || type === 'systems' ? (d.data?.services || []).length > 0 : false,
315
+ hasFlows: type === 'systems' ? (d.data?.flows || []).length > 0 : false,
273
316
  isSubdomain: type === 'domains' ? allSubdomainIds.has(d.data.id) : false,
274
317
  // Service-specific properties
275
318
  domains: isServiceOrAgentLike ? d.enrichedDomains : undefined,
@@ -304,6 +347,7 @@ const tableData = enrichedData.map((d: any) => ({
304
347
  tools: d.data?.tools ?? [],
305
348
  agents: d.data?.agents?.map(mapToItem) ?? [],
306
349
  services: d.data?.services?.map(mapToItem) ?? [],
350
+ flows: d.data?.flows?.map(mapToItem) ?? [],
307
351
  servicesThatWriteToContainer: d.data?.servicesThatWriteToContainer?.map(mapToItem) ?? [],
308
352
  servicesThatReadFromContainer: d.data?.servicesThatReadFromContainer?.map(mapToItem) ?? [],
309
353
  inputs: d.data?.inputs?.map(resolvePointer).filter(Boolean) ?? [],
@@ -395,6 +439,7 @@ const title = `${currentTypeConfig.label} (${data.length})`;
395
439
  collectionType={type as CollectionType}
396
440
  collectionLabel={currentTypeConfig.label}
397
441
  collectionDescription={currentTypeConfig.description}
442
+ collectionKinds={collectionKinds}
398
443
  domains={uniqueDomains}
399
444
  owners={uniqueOwners as Array<{ id: string; name: string; type?: 'user' | 'team' }>}
400
445
  producers={uniqueProducers}
@@ -54,22 +54,10 @@ const pagefindAttributes =
54
54
  <div class="w-full lg:mr-2 pr-24 py-8 bg-[rgb(var(--ec-page-bg))]">
55
55
  <div class="border-b border-[rgb(var(--ec-page-border))] md:pb-2">
56
56
  <div>
57
- <div class="flex justify-between items-center">
57
+ <div class="flex items-center">
58
58
  <div class="flex items-center gap-2">
59
59
  <h2 id="doc-page-header" class="text-2xl md:text-4xl font-bold text-[rgb(var(--ec-page-text))]">{title}</h2>
60
60
  </div>
61
- <div class="hidden lg:block">
62
- <CopyAsMarkdown
63
- client:only="react"
64
- schemas={[]}
65
- chatQuery={chatQuery}
66
- chatEnabled={chatEnabled}
67
- editUrl=""
68
- markdownDownloadEnabled={true}
69
- rssFeedEnabled={false}
70
- preferChatAsDefault={false}
71
- />
72
- </div>
73
61
  </div>
74
62
  </div>
75
63
  {
@@ -79,11 +67,24 @@ const pagefindAttributes =
79
67
  }
80
68
  {
81
69
  badges.length > 0 && (
82
- <div class="flex flex-wrap gap-3 py-4">
70
+ <div class="flex flex-wrap gap-1.5 py-3">
83
71
  {badges.map((badge: any) => <Badge badge={badge} />)}
84
72
  </div>
85
73
  )
86
74
  }
75
+ <div class="pt-4">
76
+ <CopyAsMarkdown
77
+ client:only="react"
78
+ variant="toolbar"
79
+ schemas={[]}
80
+ chatQuery={chatQuery}
81
+ chatEnabled={chatEnabled}
82
+ editUrl=""
83
+ markdownDownloadEnabled={true}
84
+ rssFeedEnabled={false}
85
+ preferChatAsDefault={false}
86
+ />
87
+ </div>
87
88
  </div>
88
89
  <div data-pagefind-ignore>
89
90
  {
@@ -54,29 +54,28 @@ const pagefindAttributes =
54
54
  <div class="w-full lg:mr-2 pr-24 py-8 bg-[rgb(var(--ec-page-bg))]">
55
55
  <div class="border-b border-[rgb(var(--ec-page-border))] md:pb-2">
56
56
  <div class="flex flex-col gap-4">
57
- <div class="flex justify-between items-center gap-4">
57
+ <div class="flex items-center gap-4">
58
58
  <h2 id="doc-page-header" class="text-2xl md:text-4xl font-bold text-[rgb(var(--ec-page-text))]">{title}</h2>
59
- <div class="hidden lg:block shrink-0">
60
- <CopyAsMarkdown
61
- client:only="react"
62
- schemas={[]}
63
- chatQuery={chatQuery}
64
- chatEnabled={chatEnabled}
65
- editUrl=""
66
- markdownDownloadEnabled={true}
67
- rssFeedEnabled={false}
68
- preferChatAsDefault={false}
69
- />
70
- </div>
71
59
  </div>
72
60
  {props.data.summary && <p class="text-base text-[rgb(var(--ec-page-text-muted))] font-light">{props.data.summary}</p>}
73
61
  {
74
62
  badges.length > 0 && (
75
- <div class="flex flex-wrap gap-3 py-4">
63
+ <div class="flex flex-wrap gap-1.5 py-1">
76
64
  {badges.map((badge: any) => <Badge badge={badge} />)}
77
65
  </div>
78
66
  )
79
67
  }
68
+ <CopyAsMarkdown
69
+ client:only="react"
70
+ variant="toolbar"
71
+ schemas={[]}
72
+ chatQuery={chatQuery}
73
+ chatEnabled={chatEnabled}
74
+ editUrl=""
75
+ markdownDownloadEnabled={true}
76
+ rssFeedEnabled={false}
77
+ preferChatAsDefault={false}
78
+ />
80
79
  </div>
81
80
  </div>
82
81
  <div data-pagefind-ignore>
@@ -20,6 +20,7 @@ export class Page extends HybridPage {
20
20
  'queries',
21
21
  'services',
22
22
  'domains',
23
+ 'systems',
23
24
  'flows',
24
25
  'channels',
25
26
  'entities',
@@ -14,7 +14,17 @@ export class Page extends HybridPage {
14
14
 
15
15
  const { pageDataLoader } = await import('@utils/page-loaders/page-data-loader');
16
16
 
17
- const itemTypes: PageTypes[] = ['agents', 'events', 'commands', 'queries', 'services', 'domains', 'flows', 'containers'];
17
+ const itemTypes: PageTypes[] = [
18
+ 'agents',
19
+ 'events',
20
+ 'commands',
21
+ 'queries',
22
+ 'services',
23
+ 'domains',
24
+ 'systems',
25
+ 'flows',
26
+ 'containers',
27
+ ];
18
28
  const allItems = await Promise.all(itemTypes.map((type) => pageDataLoader[type]()));
19
29
 
20
30
  return allItems.flatMap((items, index) =>
@@ -33,13 +33,14 @@ import {
33
33
  Bot,
34
34
  Box,
35
35
  Boxes,
36
+ Group,
36
37
  SquarePenIcon,
37
38
  DatabaseIcon,
38
39
  DatabaseZapIcon,
39
40
  ShieldCheckIcon,
40
41
  AlignLeft,
41
42
  Package,
42
- BookText,
43
+ ClipboardList,
43
44
  CalendarDays,
44
45
  } from 'lucide-react';
45
46
 
@@ -148,7 +149,7 @@ const getBadge = () => {
148
149
 
149
150
  if (isAdrPage) {
150
151
  return [
151
- createResourceBadge('Decision Record', BookText),
152
+ createResourceBadge('Decision Record', ClipboardList),
152
153
  createResourceBadge(formatAdrDate(props.data.date), CalendarDays),
153
154
  createAdrStatusBadge(props.data.status),
154
155
  ];
@@ -170,6 +171,10 @@ const getBadge = () => {
170
171
  return [createResourceBadge('Domain', RectangleGroupIcon)];
171
172
  }
172
173
 
174
+ if (props.collection === 'systems') {
175
+ return [createResourceBadge('System', Group)];
176
+ }
177
+
173
178
  if (props.collection === 'flows') {
174
179
  return [createResourceBadge('Flow', QueueListIcon)];
175
180
  }
@@ -326,6 +331,33 @@ let nodeGraphs =
326
331
  }) || [];
327
332
  const flowEmbeds = getMDXComponentsByName(props.body || '', 'Flow') || [];
328
333
 
334
+ // Context Diagrams embedded via <ContextDiagram />. When no id is passed we assume
335
+ // the current page. Valid on system pages (renders the system's context) and domain
336
+ // pages (renders the context across the domain's systems). Each renders into its own
337
+ // portal so it doesn't collide with the page's default diagram.
338
+ //
339
+ // The underlying graph + visualiser link differ per page type:
340
+ // - systems → `systems-context`, links to /visualiser/systems/:id/:version/context
341
+ // - domains → `domains-systems-context`, links to /visualiser/domains/:id/:version/systems-context
342
+ const contextDiagramSupported = props.collection === 'systems' || props.collection === 'domains';
343
+ const contextDiagrams = contextDiagramSupported
344
+ ? (getMDXComponentsByName(props.body || '', 'ContextDiagram') || []).map((diagram: any) => {
345
+ const id = diagram.id ?? props.data.id;
346
+ const version = diagram.version ?? props.data.version;
347
+ return {
348
+ id,
349
+ version,
350
+ search: parseMdxBooleanProp(diagram.search, true),
351
+ legend: parseMdxBooleanProp(diagram.legend, true),
352
+ graphCollection: props.collection === 'domains' ? 'domains-systems-context' : 'systems-context',
353
+ visualiserUrl:
354
+ props.collection === 'domains'
355
+ ? buildUrl(`/visualiser/domains/${id}/${version}/systems-context`)
356
+ : buildUrl(`/visualiser/systems/${id}/${version}/context`),
357
+ };
358
+ })
359
+ : [];
360
+
329
361
  // Get props for the node graph (when no id is passed, we assume its the current page)
330
362
  const hasCurrentFlowEmbed =
331
363
  props.collection === 'flows' &&
@@ -376,7 +408,7 @@ if (!isAdrPage && !hasCurrentFlowEmbed && !hasCurrentPageNodeGraph) {
376
408
  <div class="w-full lg:mr-2 pr-24 py-8 bg-[rgb(var(--ec-page-bg))]">
377
409
  <div class="border-b border-[rgb(var(--ec-page-border))] md:pb-6 pt-4">
378
410
  <div>
379
- <div class="flex justify-between items-end">
411
+ <div>
380
412
  <div class="flex flex-col gap-4">
381
413
  <span class="text-xs md:text-sm font-semibold text-[rgb(var(--ec-accent))] capitalize">
382
414
  {friendlyCollectionName}
@@ -410,22 +442,32 @@ if (!isAdrPage && !hasCurrentFlowEmbed && !hasCurrentPageNodeGraph) {
410
442
  </div>
411
443
  </div>
412
444
  </div>
413
- <div class="hidden lg:block mb-2">
414
- <CopyAsMarkdown
415
- client:only="react"
416
- schemas={schemasForResource}
417
- chatQuery={generatePromptForResource(props)}
418
- chatEnabled={isEventCatalogChatEnabled()}
419
- markdownDownloadEnabled={isMarkdownDownloadEnabled()}
420
- rssFeedEnabled={isRSSEnabled()}
421
- editUrl={editUrl}
422
- preferChatAsDefault={isEventCatalogChatEnabled()}
423
- printUrl={printUrl}
424
- />
425
- </div>
426
445
  </div>
427
446
 
428
447
  <h2 class="text-base pt-4 text-[rgb(var(--ec-page-text-muted))] font-light">{props.data.summary}</h2>
448
+ {
449
+ badges && (
450
+ <div class="flex flex-wrap gap-1.5 pt-4">
451
+ {badges.map((badge: any) => {
452
+ return <Badge badge={badge} />;
453
+ })}
454
+ </div>
455
+ )
456
+ }
457
+ <div class="pt-5">
458
+ <CopyAsMarkdown
459
+ client:only="react"
460
+ variant="toolbar"
461
+ schemas={schemasForResource}
462
+ chatQuery={generatePromptForResource(props)}
463
+ chatEnabled={isEventCatalogChatEnabled()}
464
+ markdownDownloadEnabled={isMarkdownDownloadEnabled()}
465
+ rssFeedEnabled={isRSSEnabled()}
466
+ editUrl={editUrl}
467
+ preferChatAsDefault={isEventCatalogChatEnabled()}
468
+ printUrl={printUrl}
469
+ />
470
+ </div>
429
471
  {
430
472
  httpOperation && (
431
473
  <div class="pt-4">
@@ -446,16 +488,6 @@ if (!isAdrPage && !hasCurrentFlowEmbed && !hasCurrentPageNodeGraph) {
446
488
  </div>
447
489
  )
448
490
  }
449
- {
450
- badges && (
451
- <div class="flex flex-wrap gap-3 pt-6 pb-2">
452
- {badges.map((badge: any) => {
453
- return <Badge badge={badge} />;
454
- })}
455
- </div>
456
- )
457
- }
458
-
459
491
  {
460
492
  isSupersededAdr(props) && (
461
493
  <Admonition type="warning" title="This architecture decision has been superseded">
@@ -593,6 +625,29 @@ if (!isAdrPage && !hasCurrentFlowEmbed && !hasCurrentPageNodeGraph) {
593
625
  );
594
626
  })
595
627
  }
628
+ {
629
+ contextDiagrams.length > 0 &&
630
+ contextDiagrams.map((diagram: any) => (
631
+ <NodeGraph
632
+ id={diagram.id}
633
+ version={diagram.version}
634
+ collection={diagram.graphCollection}
635
+ mode="simple"
636
+ linksToVisualiser={true}
637
+ showSearch={diagram.search ?? true}
638
+ showLegend={diagram.legend ?? true}
639
+ portalId={`${diagram.id}-context-diagram-portal`}
640
+ href={
641
+ isVisualiserEnabled()
642
+ ? {
643
+ label: 'Open Context Diagram',
644
+ url: diagram.visualiserUrl,
645
+ }
646
+ : undefined
647
+ }
648
+ />
649
+ ))
650
+ }
596
651
  </div>
597
652
  <Footer />
598
653
  <!-- Add edit this page button with icon and text-->
@@ -0,0 +1,89 @@
1
+ import { isSSR } from '@utils/feature';
2
+ import { HybridPage } from '@utils/page-loaders/hybrid-page';
3
+
4
+ // The Resources page lists the top-level resources (services, flows, entities and
5
+ // data stores) attached to a single system or domain. It is only available for
6
+ // systems and domains — every other resource type 404s.
7
+ const SUPPORTED_TYPES = ['systems', 'domains'] as const;
8
+ type SupportedType = (typeof SUPPORTED_TYPES)[number];
9
+
10
+ const isSupportedType = (type: string): type is SupportedType => SUPPORTED_TYPES.includes(type as SupportedType);
11
+
12
+ const loadResourceOwner = async (type: SupportedType) => {
13
+ if (type === 'systems') {
14
+ const { getSystems } = await import('@utils/collections/systems');
15
+ return getSystems();
16
+ }
17
+ const { getDomains } = await import('@utils/collections/domains');
18
+ return getDomains();
19
+ };
20
+
21
+ // A system/domain only gets a Resources page when it actually has resources attached.
22
+ // Mirrors what the page renders: services, flows, entities, data stores and (domains
23
+ // only) the domain's own messages.
24
+ const hasResources = (owner: any): boolean => {
25
+ const data = owner?.data ?? {};
26
+ return (
27
+ (data.services || []).length > 0 ||
28
+ (data.flows || []).length > 0 ||
29
+ (data.entities || []).length > 0 ||
30
+ (data.containers || []).length > 0 ||
31
+ (data.sends || []).length > 0 ||
32
+ (data.receives || []).length > 0
33
+ );
34
+ };
35
+
36
+ export class Page extends HybridPage {
37
+ static get prerender(): boolean {
38
+ return !isSSR();
39
+ }
40
+
41
+ static async getStaticPaths(): Promise<Array<{ params: any; props: any }>> {
42
+ if (isSSR()) {
43
+ return [];
44
+ }
45
+
46
+ const owners = await Promise.all(SUPPORTED_TYPES.map((type) => loadResourceOwner(type)));
47
+
48
+ return SUPPORTED_TYPES.flatMap((type, index) =>
49
+ owners[index]
50
+ .filter((owner) => hasResources(owner))
51
+ .map((owner) => ({
52
+ params: {
53
+ type,
54
+ id: owner.data.id,
55
+ version: owner.data.version,
56
+ },
57
+ props: {
58
+ ...owner,
59
+ },
60
+ }))
61
+ );
62
+ }
63
+
64
+ protected static async fetchData(params: any) {
65
+ const { type, id, version } = params;
66
+
67
+ if (!type || !id || !version || !isSupportedType(type)) {
68
+ return null;
69
+ }
70
+
71
+ const owners = await loadResourceOwner(type);
72
+ const owner = owners.find((o) => o.data.id === id && o.data.version === version);
73
+
74
+ if (!owner || !hasResources(owner)) {
75
+ return null;
76
+ }
77
+
78
+ return {
79
+ ...owner,
80
+ };
81
+ }
82
+
83
+ protected static createNotFoundResponse(): Response {
84
+ return new Response(null, {
85
+ status: 404,
86
+ statusText: 'Resources not found',
87
+ });
88
+ }
89
+ }