@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
@@ -65,6 +65,7 @@ export function CopyPageMenu({
65
65
  preferChatAsDefault = false,
66
66
  chatButtonText = 'Ask',
67
67
  printUrl,
68
+ variant = 'menu',
68
69
  }: {
69
70
  schemas: Schema[];
70
71
  chatQuery?: string;
@@ -75,6 +76,7 @@ export function CopyPageMenu({
75
76
  preferChatAsDefault?: boolean;
76
77
  chatButtonText?: string;
77
78
  printUrl?: string;
79
+ variant?: 'menu' | 'toolbar';
78
80
  }) {
79
81
  // Define available actions
80
82
  const availableActions = {
@@ -164,12 +166,14 @@ export function CopyPageMenu({
164
166
  const defaultAction = getDefaultAction();
165
167
  const [open, setOpen] = useState(false);
166
168
  const [buttonText, setButtonText] = useState(defaultAction?.text || 'Action');
169
+ const [copyButtonText, setCopyButtonText] = useState('Copy page as markdown');
167
170
 
168
171
  // Fetch the markdown from the url + .mdx
169
172
  const copyMarkdownToClipboard = async () => {
170
173
  console.log('Copying markdown to clipboard');
171
174
  try {
172
175
  setButtonText('Copied');
176
+ setCopyButtonText('Copied');
173
177
  const response = await fetch(markdownUrl);
174
178
  if (!response.ok) {
175
179
  throw new Error(`HTTP error! status: ${response.status}`);
@@ -177,10 +181,13 @@ export function CopyPageMenu({
177
181
  const markdown = await response.text();
178
182
  await navigator.clipboard.writeText(markdown);
179
183
  setTimeout(() => setButtonText(defaultAction?.text || 'Action'), 3000); // Revert after 3 seconds
184
+ setTimeout(() => setCopyButtonText('Copy page as markdown'), 3000);
180
185
  } catch (error) {
181
186
  console.error('Failed to copy markdown:', error);
182
187
  setButtonText('Copy failed'); // Provide feedback on failure
188
+ setCopyButtonText('Copy failed');
183
189
  setTimeout(() => setButtonText(defaultAction?.text || 'Action'), 3000);
190
+ setTimeout(() => setCopyButtonText('Copy page as markdown'), 3000);
184
191
  }
185
192
  };
186
193
 
@@ -236,6 +243,153 @@ export function CopyPageMenu({
236
243
  return null;
237
244
  }
238
245
 
246
+ const actionButtonClass =
247
+ 'group inline-flex items-center gap-1.5 whitespace-nowrap text-xs font-medium text-[rgb(var(--ec-page-text-muted))] transition-colors duration-150 hover:text-[rgb(var(--ec-page-text))] focus:outline-hidden focus:ring-2 focus:ring-[rgb(var(--ec-accent))] focus:ring-offset-2 focus:ring-offset-[rgb(var(--ec-page-bg))] rounded-md';
248
+ const actionIconClass = 'h-3.5 w-3.5 shrink-0 text-[rgb(var(--ec-icon-color))] group-hover:text-[rgb(var(--ec-page-text))]';
249
+
250
+ const hasToolbarActions =
251
+ availableActions.chat ||
252
+ availableActions.copyMarkdown ||
253
+ availableActions.viewMarkdown ||
254
+ availableActions.copySchemas ||
255
+ availableActions.rssFeed ||
256
+ availableActions.exportPDF ||
257
+ availableActions.editPage;
258
+
259
+ if (variant === 'toolbar') {
260
+ if (!hasToolbarActions) return null;
261
+
262
+ const moreActionsAvailable =
263
+ availableActions.copySchemas || availableActions.rssFeed || availableActions.exportPDF || availableActions.editPage;
264
+
265
+ return (
266
+ <div className="not-prose flex flex-wrap items-center gap-x-3 gap-y-1.5 text-xs text-[rgb(var(--ec-page-text-muted))]">
267
+ {availableActions.chat && (
268
+ <button
269
+ type="button"
270
+ onClick={() => window.dispatchEvent(new CustomEvent('eventcatalog:open-chat'))}
271
+ className={actionButtonClass}
272
+ >
273
+ <MessageCircleQuestion className={actionIconClass} />
274
+ Ask a question
275
+ </button>
276
+ )}
277
+
278
+ {availableActions.chat && (availableActions.copyMarkdown || availableActions.viewMarkdown || moreActionsAvailable) && (
279
+ <span className="h-4 w-px bg-[rgb(var(--ec-page-border))]" aria-hidden="true" />
280
+ )}
281
+
282
+ {availableActions.copyMarkdown && (
283
+ <button type="button" onClick={copyMarkdownToClipboard} className={actionButtonClass}>
284
+ <Copy className={actionIconClass} />
285
+ {copyButtonText}
286
+ </button>
287
+ )}
288
+
289
+ {availableActions.copyMarkdown && (availableActions.viewMarkdown || moreActionsAvailable) && (
290
+ <span className="h-4 w-px bg-[rgb(var(--ec-page-border))]" aria-hidden="true" />
291
+ )}
292
+
293
+ {availableActions.viewMarkdown && (
294
+ <button type="button" onClick={() => window.open(markdownUrl, '_blank')} className={actionButtonClass}>
295
+ <FileText className={actionIconClass} />
296
+ View as Markdown
297
+ </button>
298
+ )}
299
+
300
+ {availableActions.viewMarkdown && moreActionsAvailable && (
301
+ <span className="h-4 w-px bg-[rgb(var(--ec-page-border))]" aria-hidden="true" />
302
+ )}
303
+
304
+ {moreActionsAvailable && (
305
+ <DropdownMenu.Root open={open} onOpenChange={setOpen}>
306
+ <DropdownMenu.Trigger asChild>
307
+ <button type="button" className={actionButtonClass}>
308
+ More actions
309
+ <ChevronDownIcon className={`h-3.5 w-3.5 transition-transform duration-200 ${open ? 'rotate-180' : ''}`} />
310
+ </button>
311
+ </DropdownMenu.Trigger>
312
+ <DropdownMenu.Content
313
+ className="z-50 w-72 max-w-[calc(100vw-1.5rem)] rounded-2xl border border-[rgb(var(--ec-dropdown-border)/0.8)] bg-[rgb(var(--ec-page-bg))] px-1.5 py-1.5 shadow-[0_24px_64px_rgb(0_0_0/0.35)]"
314
+ sideOffset={10}
315
+ align="start"
316
+ >
317
+ {availableActions.copySchemas &&
318
+ schemas.map((schema) => {
319
+ const title =
320
+ schema.format === 'asyncapi'
321
+ ? 'Copy AsyncAPI specification'
322
+ : schema.format === 'openapi'
323
+ ? 'Copy OpenAPI specification'
324
+ : 'Copy schema';
325
+ const type =
326
+ schema.format === 'asyncapi' || schema.format === 'openapi'
327
+ ? 'specification'
328
+ : `${schema.format.toUpperCase()} schema`;
329
+
330
+ const Icon =
331
+ schema.format === 'asyncapi' ? (
332
+ <img src={buildUrl('/icons/asyncapi.svg', true)} className="w-4 h-4" />
333
+ ) : schema.format === 'openapi' ? (
334
+ <img src={buildUrl('/icons/openapi.svg', true)} className="w-4 h-4" />
335
+ ) : (
336
+ FileText
337
+ );
338
+
339
+ return (
340
+ <DropdownMenu.Item
341
+ key={schema.url}
342
+ className="cursor-pointer rounded-2xl outline-hidden transition-colors duration-150 hover:bg-[rgb(var(--ec-dropdown-hover))] data-[highlighted]:bg-[rgb(var(--ec-dropdown-hover))]"
343
+ onSelect={() => copySchemaToClipboard(schema)}
344
+ >
345
+ <MenuItemContent icon={Icon} title={title} description={`Copy ${type} to clipboard`} />
346
+ </DropdownMenu.Item>
347
+ );
348
+ })}
349
+
350
+ {availableActions.rssFeed && (
351
+ <DropdownMenu.Item
352
+ className="cursor-pointer rounded-2xl outline-hidden transition-colors duration-150 hover:bg-[rgb(var(--ec-dropdown-hover))] data-[highlighted]:bg-[rgb(var(--ec-dropdown-hover))]"
353
+ onSelect={() => window.open(buildUrl(`/rss/all/rss.xml`), '_blank')}
354
+ >
355
+ <MenuItemContent icon={RssIcon} title="RSS Feed" description="View this page as RSS feed" external={true} />
356
+ </DropdownMenu.Item>
357
+ )}
358
+
359
+ {availableActions.exportPDF && (
360
+ <DropdownMenu.Item
361
+ className="cursor-pointer rounded-2xl outline-hidden transition-colors duration-150 hover:bg-[rgb(var(--ec-dropdown-hover))] data-[highlighted]:bg-[rgb(var(--ec-dropdown-hover))]"
362
+ onSelect={() => window.open(printUrl, '_blank')}
363
+ >
364
+ <MenuItemContent
365
+ icon={PrinterIcon}
366
+ title="Export to PDF"
367
+ description="Open print-friendly version"
368
+ external={true}
369
+ />
370
+ </DropdownMenu.Item>
371
+ )}
372
+
373
+ {availableActions.editPage && (
374
+ <DropdownMenu.Item
375
+ className="cursor-pointer rounded-2xl outline-hidden transition-colors duration-150 hover:bg-[rgb(var(--ec-dropdown-hover))] data-[highlighted]:bg-[rgb(var(--ec-dropdown-hover))]"
376
+ onSelect={() => window.open(editUrl, '_blank')}
377
+ >
378
+ <MenuItemContent
379
+ icon={PenSquareIcon}
380
+ title="Edit page"
381
+ description="Edit the contents of this page"
382
+ external={true}
383
+ />
384
+ </DropdownMenu.Item>
385
+ )}
386
+ </DropdownMenu.Content>
387
+ </DropdownMenu.Root>
388
+ )}
389
+ </div>
390
+ );
391
+ }
392
+
239
393
  return (
240
394
  <DropdownMenu.Root open={open} onOpenChange={setOpen}>
241
395
  {/* Container for the split button */}
@@ -12,9 +12,10 @@ import {
12
12
  ArrowLongRightIcon,
13
13
  ArrowLongLeftIcon,
14
14
  CubeIcon,
15
+ GlobeAltIcon,
15
16
  } from '@heroicons/react/24/outline';
16
17
  import { buildUrl } from '@utils/url-builder';
17
- import { BoxIcon } from 'lucide-react';
18
+ import { BoxIcon, Group as GroupIcon } from 'lucide-react';
18
19
  import { getSpecUrl, getSpecIcon, getSpecLabel, getServiceSpecifications } from './specification-utils';
19
20
 
20
21
  // ============================================
@@ -46,7 +47,7 @@ const getMessageIcon = (collection: string) => {
46
47
  // Sub-components
47
48
  // ============================================
48
49
 
49
- const EntityBadge = memo(({ entity }: { entity: any }) => {
50
+ export const EntityBadge = memo(({ entity }: { entity: any }) => {
50
51
  const id = entity?.data?.id || entity?.id;
51
52
  const name = entity?.data?.name || entity?.name || id;
52
53
  const version = entity?.data?.version || entity?.version || 'latest';
@@ -120,6 +121,26 @@ const ContainerLink = memo(({ container, type }: { container: any; type: 'reads'
120
121
  );
121
122
  });
122
123
 
124
+ // Standalone data store (container) link, used to list a system's data stores
125
+ const DataStoreLink = memo(({ container }: { container: any }) => {
126
+ const data = container?.data || container;
127
+ const id = data?.id || container?.id;
128
+ const name = data?.name || id;
129
+ const version = data?.version || 'latest';
130
+ const technology = data?.technology;
131
+
132
+ return (
133
+ <a
134
+ href={buildUrl(`/docs/containers/${id}/${version}`)}
135
+ className="inline-flex items-center gap-2 px-3 py-2 bg-[rgb(var(--ec-card-bg,var(--ec-page-bg)))] border border-[rgb(var(--ec-page-border))] rounded-lg text-sm font-medium text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-content-hover))] hover:border-[rgb(var(--ec-accent)/0.5)] transition-all shadow-xs"
136
+ >
137
+ <CircleStackIcon className="h-4 w-4 text-amber-500" />
138
+ <span>{name}</span>
139
+ {technology && <span className="text-xs text-[rgb(var(--ec-icon-color))]">{technology}</span>}
140
+ </a>
141
+ );
142
+ });
143
+
123
144
  // Searchable scrollable box component
124
145
  const SearchableBox = memo(
125
146
  ({
@@ -245,9 +266,9 @@ const ServiceExpandedContent = memo(
245
266
  }
246
267
  );
247
268
 
248
- const ServiceCard = memo(({ service }: { service: any }) => {
269
+ export const ServiceCard = memo(({ service, defaultOpen = false }: { service: any; defaultOpen?: boolean }) => {
249
270
  const data = service?.data || service;
250
- const [isCollapsed, setIsCollapsed] = useState(true);
271
+ const [isCollapsed, setIsCollapsed] = useState(!defaultOpen);
251
272
 
252
273
  if (!data?.id) return null;
253
274
 
@@ -363,9 +384,9 @@ const DataProductCard = memo(({ dataProduct }: { dataProduct: any }) => {
363
384
  );
364
385
  });
365
386
 
366
- const SubdomainSection = memo(({ subdomain }: { subdomain: any }) => {
387
+ const SubdomainSection = memo(({ subdomain, defaultOpen = false }: { subdomain: any; defaultOpen?: boolean }) => {
367
388
  const data = subdomain?.data || subdomain;
368
- const [isCollapsed, setIsCollapsed] = useState(true);
389
+ const [isCollapsed, setIsCollapsed] = useState(!defaultOpen);
369
390
 
370
391
  if (!data?.id) return null;
371
392
 
@@ -489,6 +510,109 @@ const SubdomainSection = memo(({ subdomain }: { subdomain: any }) => {
489
510
  );
490
511
  });
491
512
 
513
+ const SystemSection = memo(({ system, defaultOpen = false }: { system: any; defaultOpen?: boolean }) => {
514
+ const data = system?.data || system;
515
+ const [isCollapsed, setIsCollapsed] = useState(!defaultOpen);
516
+
517
+ if (!data?.id) return null;
518
+
519
+ const services = data.services || [];
520
+ const containers = data.containers || [];
521
+
522
+ return (
523
+ <div className="bg-[rgb(var(--ec-card-bg,var(--ec-page-bg)))] border border-[rgb(var(--ec-page-border))] rounded-xl overflow-hidden shadow-xs">
524
+ {/* System Header - Clickable */}
525
+ <div
526
+ onClick={() => setIsCollapsed(!isCollapsed)}
527
+ className={`flex items-center justify-between px-5 py-4 cursor-pointer hover:bg-[rgb(var(--ec-content-hover))] transition-colors ${!isCollapsed ? 'border-b border-[rgb(var(--ec-page-border))]' : ''}`}
528
+ >
529
+ <div className="flex items-center gap-3">
530
+ <div className="flex items-center justify-center w-9 h-9 bg-violet-100 dark:bg-violet-500/20 rounded-lg">
531
+ <GroupIcon className="h-5 w-5 text-violet-600 dark:text-violet-400" />
532
+ </div>
533
+ <div>
534
+ <div className="flex items-center gap-2">
535
+ <h3 className="text-base font-semibold text-[rgb(var(--ec-page-text))]">{data.name || data.id}</h3>
536
+ <span className="text-[11px] text-[rgb(var(--ec-page-text-muted))] font-medium bg-[rgb(var(--ec-content-hover))] px-1.5 py-0.5 rounded border border-[rgb(var(--ec-page-border))]">
537
+ v{data.version}
538
+ </span>
539
+ {/* Show counts when collapsed */}
540
+ {isCollapsed && (services.length > 0 || containers.length > 0) && (
541
+ <span className="text-[11px] text-[rgb(var(--ec-icon-color))] ml-1">
542
+ {[
543
+ services.length > 0 && `${services.length} service${services.length > 1 ? 's' : ''}`,
544
+ containers.length > 0 && `${containers.length} data store${containers.length > 1 ? 's' : ''}`,
545
+ ]
546
+ .filter(Boolean)
547
+ .join(' · ')}
548
+ </span>
549
+ )}
550
+ </div>
551
+ <span className="text-[11px] text-[rgb(var(--ec-page-text-muted))] font-medium">System</span>
552
+ </div>
553
+ </div>
554
+ <div className="flex items-center gap-1">
555
+ <div className="p-2 text-[rgb(var(--ec-icon-color))]">
556
+ {isCollapsed ? <ChevronDownIcon className="h-5 w-5" /> : <ChevronUpIcon className="h-5 w-5" />}
557
+ </div>
558
+ <a
559
+ href={buildUrl(`/visualiser/systems/${data.id}/${data.version}`)}
560
+ onClick={(e) => e.stopPropagation()}
561
+ className="p-2 text-[rgb(var(--ec-icon-color))] hover:text-violet-500 hover:bg-violet-500/10 rounded-lg transition-colors"
562
+ title="View system map"
563
+ >
564
+ <ArrowTopRightOnSquareIcon className="h-5 w-5" />
565
+ </a>
566
+ </div>
567
+ </div>
568
+
569
+ {!isCollapsed && (
570
+ <div className="p-5 space-y-5">
571
+ {services.length > 0 && (
572
+ <div>
573
+ <div className="flex items-center gap-2 mb-3">
574
+ <ServerIcon className="h-4 w-4 text-pink-600 dark:text-pink-400" />
575
+ <h4 className="text-sm font-semibold text-[rgb(var(--ec-page-text))]">Services</h4>
576
+ <span className="text-xs text-[rgb(var(--ec-page-text-muted))] bg-[rgb(var(--ec-content-hover))] px-2 py-0.5 rounded-full font-medium">
577
+ {services.length}
578
+ </span>
579
+ </div>
580
+ <div className="space-y-3">
581
+ {services.map((service: any) => {
582
+ const serviceId = service?.data?.id || service?.id;
583
+ return serviceId ? <ServiceCard key={serviceId} service={service} /> : null;
584
+ })}
585
+ </div>
586
+ </div>
587
+ )}
588
+
589
+ {containers.length > 0 && (
590
+ <div>
591
+ <div className="flex items-center gap-2 mb-3">
592
+ <CircleStackIcon className="h-4 w-4 text-amber-500" />
593
+ <h4 className="text-sm font-semibold text-[rgb(var(--ec-page-text))]">Data Stores</h4>
594
+ <span className="text-xs text-[rgb(var(--ec-page-text-muted))] bg-[rgb(var(--ec-content-hover))] px-2 py-0.5 rounded-full font-medium">
595
+ {containers.length}
596
+ </span>
597
+ </div>
598
+ <div className="flex flex-wrap gap-2">
599
+ {containers.map((container: any) => {
600
+ const containerId = container?.data?.id || container?.id;
601
+ return containerId ? <DataStoreLink key={containerId} container={container} /> : null;
602
+ })}
603
+ </div>
604
+ </div>
605
+ )}
606
+
607
+ {services.length === 0 && containers.length === 0 && (
608
+ <p className="text-sm text-[rgb(var(--ec-icon-color))] italic text-center py-4">Nothing in this system yet</p>
609
+ )}
610
+ </div>
611
+ )}
612
+ </div>
613
+ );
614
+ });
615
+
492
616
  // ============================================
493
617
  // Main Component
494
618
  // ============================================
@@ -498,6 +622,7 @@ export default function DomainGrid({ domain }: DomainGridProps) {
498
622
  if (!data) return <div className="text-[rgb(var(--ec-page-text-muted))]">No domain data</div>;
499
623
 
500
624
  const subdomains = data.domains || [];
625
+ const systems = data.systems || [];
501
626
  const entities = data.entities || [];
502
627
  const services = data.services || [];
503
628
  const dataProducts = data['data-products'] || [];
@@ -535,6 +660,18 @@ export default function DomainGrid({ domain }: DomainGridProps) {
535
660
  [services, subdomainServiceIds]
536
661
  );
537
662
 
663
+ // Split top-level services into internal services and external integrations
664
+ // (mirrors the sidebar, where externalSystem services live under "External Integrations")
665
+ const topLevelInternalServices = useMemo(
666
+ () => topLevelServices.filter((s: any) => !(s?.data || s)?.externalSystem),
667
+ [topLevelServices]
668
+ );
669
+
670
+ const topLevelExternalServices = useMemo(
671
+ () => topLevelServices.filter((s: any) => (s?.data || s)?.externalSystem),
672
+ [topLevelServices]
673
+ );
674
+
538
675
  const topLevelDataProducts = useMemo(
539
676
  () =>
540
677
  dataProducts.filter((dp: any) => {
@@ -553,22 +690,6 @@ export default function DomainGrid({ domain }: DomainGridProps) {
553
690
  <h2 className="text-2xl md:text-4xl font-bold text-[rgb(var(--ec-page-text))]">{data.name || data.id}</h2>
554
691
  {data.summary && <p className="text-lg pt-2 text-[rgb(var(--ec-page-text-muted))] font-light">{data.summary}</p>}
555
692
  </div>
556
- <div className="flex items-center gap-2 flex-shrink-0">
557
- <a
558
- href={buildUrl(`/docs/domains/${data.id}/${data.version}`)}
559
- className="inline-flex items-center gap-1.5 px-3 py-2 text-sm font-medium text-[rgb(var(--ec-page-text))] bg-[rgb(var(--ec-card-bg,var(--ec-page-bg)))] border border-[rgb(var(--ec-page-border))] rounded-lg hover:bg-[rgb(var(--ec-content-hover))] hover:border-[rgb(var(--ec-page-text-muted))] transition-all"
560
- >
561
- View docs
562
- <ArrowTopRightOnSquareIcon className="h-4 w-4 text-[rgb(var(--ec-icon-color))]" />
563
- </a>
564
- <a
565
- href={buildUrl(`/visualiser/domains/${data.id}/${data.version}`)}
566
- className="inline-flex items-center gap-1.5 px-3 py-2 text-sm font-medium text-white bg-zinc-800 dark:bg-zinc-700 rounded-lg hover:bg-zinc-900 dark:hover:bg-zinc-600 transition-all"
567
- >
568
- Visualizer
569
- <ArrowTopRightOnSquareIcon className="h-4 w-4 text-zinc-400" />
570
- </a>
571
- </div>
572
693
  </div>
573
694
  </div>
574
695
 
@@ -593,39 +714,77 @@ export default function DomainGrid({ domain }: DomainGridProps) {
593
714
  </div>
594
715
  )}
595
716
 
717
+ {/* Subdomains */}
718
+ {subdomains.length > 0 && (
719
+ <div>
720
+ <div className="flex items-center gap-2 mb-4">
721
+ <RectangleGroupIcon className="h-5 w-5 text-orange-600 dark:text-orange-400" />
722
+ <h3 className="text-lg font-semibold text-[rgb(var(--ec-page-text))]">Subdomains</h3>
723
+ <span className="text-sm text-[rgb(var(--ec-page-text-muted))] bg-[rgb(var(--ec-content-hover))] px-2.5 py-0.5 rounded-full font-medium">
724
+ {subdomains.length}
725
+ </span>
726
+ </div>
727
+ <div className="space-y-4">
728
+ {subdomains.map((subdomain: any) => {
729
+ const subdomainId = subdomain?.data?.id || subdomain?.id;
730
+ return subdomainId ? <SubdomainSection key={subdomainId} subdomain={subdomain} defaultOpen /> : null;
731
+ })}
732
+ </div>
733
+ </div>
734
+ )}
735
+
736
+ {/* Systems */}
737
+ {systems.length > 0 && (
738
+ <div>
739
+ <div className="flex items-center gap-2 mb-4">
740
+ <GroupIcon className="h-5 w-5 text-violet-600 dark:text-violet-400" />
741
+ <h3 className="text-lg font-semibold text-[rgb(var(--ec-page-text))]">Systems</h3>
742
+ <span className="text-sm text-[rgb(var(--ec-page-text-muted))] bg-[rgb(var(--ec-content-hover))] px-2.5 py-0.5 rounded-full font-medium">
743
+ {systems.length}
744
+ </span>
745
+ </div>
746
+ <div className="space-y-4">
747
+ {systems.map((system: any) => {
748
+ const systemId = system?.data?.id || system?.id;
749
+ return systemId ? <SystemSection key={systemId} system={system} defaultOpen /> : null;
750
+ })}
751
+ </div>
752
+ </div>
753
+ )}
754
+
596
755
  {/* Top-level Services */}
597
- {topLevelServices.length > 0 && (
756
+ {topLevelInternalServices.length > 0 && (
598
757
  <div>
599
758
  <div className="flex items-center gap-2 mb-4">
600
759
  <ServerIcon className="h-5 w-5 text-pink-600 dark:text-pink-400" />
601
760
  <h3 className="text-lg font-semibold text-[rgb(var(--ec-page-text))]">Services</h3>
602
761
  <span className="text-sm text-[rgb(var(--ec-page-text-muted))] bg-[rgb(var(--ec-content-hover))] px-2.5 py-0.5 rounded-full font-medium">
603
- {topLevelServices.length}
762
+ {topLevelInternalServices.length}
604
763
  </span>
605
764
  </div>
606
765
  <div className="space-y-3">
607
- {topLevelServices.map((service: any) => {
766
+ {topLevelInternalServices.map((service: any) => {
608
767
  const serviceId = service?.data?.id || service?.id;
609
- return serviceId ? <ServiceCard key={serviceId} service={service} /> : null;
768
+ return serviceId ? <ServiceCard key={serviceId} service={service} defaultOpen /> : null;
610
769
  })}
611
770
  </div>
612
771
  </div>
613
772
  )}
614
773
 
615
- {/* Subdomains */}
616
- {subdomains.length > 0 && (
774
+ {/* External Integrations (external systems) */}
775
+ {topLevelExternalServices.length > 0 && (
617
776
  <div>
618
777
  <div className="flex items-center gap-2 mb-4">
619
- <RectangleGroupIcon className="h-5 w-5 text-orange-600 dark:text-orange-400" />
620
- <h3 className="text-lg font-semibold text-[rgb(var(--ec-page-text))]">Subdomains</h3>
778
+ <GlobeAltIcon className="h-5 w-5 text-sky-600 dark:text-sky-400" />
779
+ <h3 className="text-lg font-semibold text-[rgb(var(--ec-page-text))]">External Integrations</h3>
621
780
  <span className="text-sm text-[rgb(var(--ec-page-text-muted))] bg-[rgb(var(--ec-content-hover))] px-2.5 py-0.5 rounded-full font-medium">
622
- {subdomains.length}
781
+ {topLevelExternalServices.length}
623
782
  </span>
624
783
  </div>
625
- <div className="space-y-4">
626
- {subdomains.map((subdomain: any) => {
627
- const subdomainId = subdomain?.data?.id || subdomain?.id;
628
- return subdomainId ? <SubdomainSection key={subdomainId} subdomain={subdomain} /> : null;
784
+ <div className="space-y-3">
785
+ {topLevelExternalServices.map((service: any) => {
786
+ const serviceId = service?.data?.id || service?.id;
787
+ return serviceId ? <ServiceCard key={serviceId} service={service} defaultOpen /> : null;
629
788
  })}
630
789
  </div>
631
790
  </div>
@@ -651,16 +810,20 @@ export default function DomainGrid({ domain }: DomainGridProps) {
651
810
  )}
652
811
 
653
812
  {/* Empty state */}
654
- {entities.length === 0 && services.length === 0 && dataProducts.length === 0 && subdomains.length === 0 && (
655
- <div className="text-center py-12">
656
- <div className="flex items-center justify-center w-16 h-16 mx-auto mb-4 bg-[rgb(var(--ec-content-hover))] rounded-2xl">
657
- <RectangleGroupIcon className="h-8 w-8 text-[rgb(var(--ec-icon-color))]" />
813
+ {entities.length === 0 &&
814
+ services.length === 0 &&
815
+ dataProducts.length === 0 &&
816
+ subdomains.length === 0 &&
817
+ systems.length === 0 && (
818
+ <div className="text-center py-12">
819
+ <div className="flex items-center justify-center w-16 h-16 mx-auto mb-4 bg-[rgb(var(--ec-content-hover))] rounded-2xl">
820
+ <RectangleGroupIcon className="h-8 w-8 text-[rgb(var(--ec-icon-color))]" />
821
+ </div>
822
+ <p className="text-[rgb(var(--ec-page-text-muted))]">
823
+ This domain has no entities, services, data products, subdomains, or systems defined.
824
+ </p>
658
825
  </div>
659
- <p className="text-[rgb(var(--ec-page-text-muted))]">
660
- This domain has no entities, services, data products, or subdomains defined.
661
- </p>
662
- </div>
663
- )}
826
+ )}
664
827
  </div>
665
828
  </div>
666
829
  );
@@ -2,7 +2,6 @@ import { memo, useState } from 'react';
2
2
  import {
3
3
  ServerIcon,
4
4
  CircleStackIcon,
5
- ArrowTopRightOnSquareIcon,
6
5
  ArrowLongRightIcon,
7
6
  BoltIcon,
8
7
  ChatBubbleLeftIcon,
@@ -234,7 +233,6 @@ export default function MessageGridV2({ service, embeded = false, specifications
234
233
  const { sends = [], receives = [], writesTo = [], readsFrom = [] } = service.data;
235
234
  const collection = service.collection || 'services';
236
235
  const resourceLabel = collection === 'agents' ? 'agent' : 'service';
237
- const hasVisualizer = true;
238
236
  const hasContainers = readsFrom.length > 0 || writesTo.length > 0;
239
237
  const hasMessages = receives.length > 0 || sends.length > 0;
240
238
  const hasSpecs = specifications.length > 0;
@@ -250,24 +248,6 @@ export default function MessageGridV2({ service, embeded = false, specifications
250
248
  <p className="text-lg pt-2 text-[rgb(var(--ec-page-text-muted))] font-light">{service.data.summary}</p>
251
249
  )}
252
250
  </div>
253
- <div className="flex items-center gap-2 flex-shrink-0">
254
- <a
255
- href={buildUrl(`/docs/${collection}/${service.data.id}/${service.data.version}`)}
256
- className="inline-flex items-center gap-1.5 px-3 py-2 text-sm font-medium text-[rgb(var(--ec-page-text))] bg-[rgb(var(--ec-card-bg,var(--ec-page-bg)))] border border-[rgb(var(--ec-page-border))] rounded-lg hover:bg-[rgb(var(--ec-content-hover))] hover:border-[rgb(var(--ec-page-text-muted))] transition-all"
257
- >
258
- View docs
259
- <ArrowTopRightOnSquareIcon className="h-4 w-4 text-[rgb(var(--ec-icon-color))]" />
260
- </a>
261
- {hasVisualizer && (
262
- <a
263
- href={buildUrl(`/visualiser/${collection}/${service.data.id}/${service.data.version}`)}
264
- className="inline-flex items-center gap-1.5 px-3 py-2 text-sm font-medium text-white bg-zinc-800 dark:bg-zinc-700 rounded-lg hover:bg-zinc-900 dark:hover:bg-zinc-600 transition-all"
265
- >
266
- Visualizer
267
- <ArrowTopRightOnSquareIcon className="h-4 w-4 text-zinc-400" />
268
- </a>
269
- )}
270
- </div>
271
251
  </div>
272
252
  </div>
273
253