@eventcatalog/core 2.65.0 → 3.0.0-beta.0

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 (130) hide show
  1. package/README.md +1 -26
  2. package/dist/analytics/analytics.cjs +1 -1
  3. package/dist/analytics/analytics.js +2 -2
  4. package/dist/analytics/log-build.cjs +1 -1
  5. package/dist/analytics/log-build.js +3 -3
  6. package/dist/{chunk-NK6OYMRD.js → chunk-JB4YT5JY.js} +1 -1
  7. package/dist/{chunk-BMDTX5IN.js → chunk-TQ4HZREX.js} +1 -1
  8. package/dist/{chunk-IJRFYF4B.js → chunk-X4W4YC3U.js} +1 -1
  9. package/dist/constants.cjs +1 -1
  10. package/dist/constants.js +1 -1
  11. package/dist/eventcatalog.cjs +1 -21
  12. package/dist/eventcatalog.config.d.cts +10 -0
  13. package/dist/eventcatalog.config.d.ts +10 -0
  14. package/dist/eventcatalog.js +3 -20
  15. package/eventcatalog/src/components/CopyAsMarkdown.tsx +19 -1
  16. package/eventcatalog/src/components/FavoriteButton.tsx +54 -0
  17. package/eventcatalog/src/components/Grids/DomainGrid.tsx +386 -362
  18. package/eventcatalog/src/components/Grids/MessageGrid.tsx +166 -518
  19. package/eventcatalog/src/components/Header.astro +48 -23
  20. package/eventcatalog/src/components/Lists/VersionList.astro +2 -2
  21. package/eventcatalog/src/components/MDX/Design/Design.astro +4 -1
  22. package/eventcatalog/src/components/MDX/Flow/Flow.astro +2 -1
  23. package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.astro +3 -3
  24. package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsPanel.tsx +8 -2
  25. package/eventcatalog/src/components/SchemaExplorer/SchemaPageViewer.tsx +37 -0
  26. package/eventcatalog/src/components/Search/Search.astro +48 -28
  27. package/eventcatalog/src/components/Search/SearchModal.tsx +393 -702
  28. package/eventcatalog/src/components/SideNav/NestedSideBar/SearchBar.tsx +298 -0
  29. package/eventcatalog/src/components/SideNav/NestedSideBar/builders/container.ts +66 -0
  30. package/eventcatalog/src/components/SideNav/NestedSideBar/builders/domain.ts +101 -0
  31. package/eventcatalog/src/components/SideNav/NestedSideBar/builders/flow.ts +29 -0
  32. package/eventcatalog/src/components/SideNav/NestedSideBar/builders/message.ts +84 -0
  33. package/eventcatalog/src/components/SideNav/NestedSideBar/builders/service.ts +147 -0
  34. package/eventcatalog/src/components/SideNav/NestedSideBar/builders/shared.ts +146 -0
  35. package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +1073 -0
  36. package/eventcatalog/src/components/SideNav/NestedSideBar/sidebar-builder.ts +365 -0
  37. package/eventcatalog/src/components/SideNav/NestedSideBar/storage.ts +90 -0
  38. package/eventcatalog/src/components/SideNav/SideNav.astro +18 -28
  39. package/eventcatalog/src/content.config.ts +2 -0
  40. package/eventcatalog/src/enterprise/custom-documentation/pages/docs/custom/index.astro +10 -4
  41. package/eventcatalog/src/enterprise/eventcatalog-chat/pages/chat/index.astro +3 -3
  42. package/eventcatalog/src/layouts/DirectoryLayout.astro +2 -2
  43. package/eventcatalog/src/layouts/DiscoverLayout.astro +3 -3
  44. package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +85 -63
  45. package/eventcatalog/src/layouts/VisualiserLayout.astro +3 -3
  46. package/eventcatalog/src/pages/_index.astro +530 -110
  47. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.ts +64 -0
  48. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/index.astro +29 -0
  49. package/eventcatalog/src/pages/directory/[type]/_index.data.ts +4 -4
  50. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/_index.data.ts +1 -4
  51. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/changelog/_index.data.ts +3 -3
  52. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/changelog/index.astro +1 -5
  53. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +362 -190
  54. package/eventcatalog/src/pages/docs/[type]/[id]/[version].md.ts +1 -1
  55. package/eventcatalog/src/pages/docs/[type]/[id]/index.astro +4 -4
  56. package/eventcatalog/src/pages/docs/[type]/[id]/language/_index.data.ts +1 -4
  57. package/eventcatalog/src/pages/docs/[type]/[id]/language/index.astro +3 -27
  58. package/eventcatalog/src/pages/docs/teams/[id]/_index.data.ts +2 -2
  59. package/eventcatalog/src/pages/docs/users/[id]/_index.data.ts +2 -2
  60. package/eventcatalog/src/pages/index.astro +14 -5
  61. package/eventcatalog/src/pages/nav-index.json.ts +30 -0
  62. package/eventcatalog/src/pages/schemas/[type]/[id]/[version]/_index.data.ts +77 -0
  63. package/eventcatalog/src/pages/schemas/[type]/[id]/[version]/index.astro +90 -0
  64. package/eventcatalog/src/pages/schemas/{index.astro → explorer/index.astro} +3 -3
  65. package/eventcatalog/src/pages/studio.astro +3 -3
  66. package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/_index.data.ts +4 -3
  67. package/eventcatalog/src/pages/visualiser/[type]/[id]/index.astro +2 -2
  68. package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/entity-map/_index.data.ts +4 -3
  69. package/eventcatalog/src/stores/favorites-store.ts +83 -0
  70. package/eventcatalog/src/stores/sidebar-store.ts +8 -0
  71. package/eventcatalog/src/utils/collections/changelogs.ts +7 -4
  72. package/eventcatalog/src/utils/{channels.ts → collections/channels.ts} +81 -31
  73. package/eventcatalog/src/utils/collections/commands.ts +134 -0
  74. package/eventcatalog/src/utils/collections/containers.ts +44 -33
  75. package/eventcatalog/src/utils/collections/domains.ts +204 -62
  76. package/eventcatalog/src/utils/{entities.ts → collections/entities.ts} +44 -24
  77. package/eventcatalog/src/utils/collections/events.ts +136 -0
  78. package/eventcatalog/src/utils/collections/flows.ts +59 -25
  79. package/eventcatalog/src/utils/{messages.ts → collections/messages.ts} +13 -4
  80. package/eventcatalog/src/utils/{queries.ts → collections/queries.ts} +49 -28
  81. package/eventcatalog/src/utils/collections/services.ts +100 -68
  82. package/eventcatalog/src/utils/collections/teams.ts +94 -0
  83. package/eventcatalog/src/utils/collections/users.ts +122 -0
  84. package/eventcatalog/src/utils/collections/util.ts +57 -1
  85. package/eventcatalog/src/utils/feature.ts +3 -1
  86. package/eventcatalog/src/utils/{collections/file-diffs.ts → file-diffs.ts} +1 -1
  87. package/eventcatalog/src/utils/node-graphs/container-node-graph.ts +2 -0
  88. package/eventcatalog/src/utils/node-graphs/domain-entity-map.ts +16 -6
  89. package/eventcatalog/src/utils/node-graphs/domains-canvas.ts +14 -10
  90. package/eventcatalog/src/utils/node-graphs/domains-node-graph.ts +36 -64
  91. package/eventcatalog/src/utils/node-graphs/flows-node-graph.ts +23 -19
  92. package/eventcatalog/src/utils/node-graphs/message-node-graph.ts +36 -49
  93. package/eventcatalog/src/utils/node-graphs/services-node-graph.ts +22 -18
  94. package/eventcatalog/src/utils/page-loaders/page-data-loader.ts +4 -4
  95. package/eventcatalog/tailwind.config.mjs +14 -0
  96. package/eventcatalog/tsconfig.json +2 -1
  97. package/package.json +7 -4
  98. package/eventcatalog/public/logo_old.png +0 -0
  99. package/eventcatalog/src/components/DiscoverInsight.astro +0 -61
  100. package/eventcatalog/src/components/Grids/ServiceGrid.tsx +0 -534
  101. package/eventcatalog/src/components/Lists/CustomSideBarSectionList.astro +0 -55
  102. package/eventcatalog/src/components/Lists/ProtocolList.tsx +0 -74
  103. package/eventcatalog/src/components/Lists/RepositoryList.astro +0 -37
  104. package/eventcatalog/src/components/Lists/SpecificationsList.astro +0 -67
  105. package/eventcatalog/src/components/SideBars/ChannelSideBar.astro +0 -204
  106. package/eventcatalog/src/components/SideBars/ContainerSideBar.astro +0 -180
  107. package/eventcatalog/src/components/SideBars/DomainSideBar.astro +0 -273
  108. package/eventcatalog/src/components/SideBars/EntitySideBar.astro +0 -139
  109. package/eventcatalog/src/components/SideBars/FlowSideBar.astro +0 -128
  110. package/eventcatalog/src/components/SideBars/MessageSideBar.astro +0 -248
  111. package/eventcatalog/src/components/SideBars/ServiceSideBar.astro +0 -294
  112. package/eventcatalog/src/components/SideNav/ListViewSideBar/components/CollapsibleGroup.tsx +0 -46
  113. package/eventcatalog/src/components/SideNav/ListViewSideBar/components/MessageList.tsx +0 -78
  114. package/eventcatalog/src/components/SideNav/ListViewSideBar/components/SpecificationList.tsx +0 -83
  115. package/eventcatalog/src/components/SideNav/ListViewSideBar/index.tsx +0 -1250
  116. package/eventcatalog/src/components/SideNav/ListViewSideBar/types.ts +0 -91
  117. package/eventcatalog/src/components/SideNav/ListViewSideBar/utils.ts +0 -201
  118. package/eventcatalog/src/components/SideNav/TreeView/getTreeView.ts +0 -190
  119. package/eventcatalog/src/components/SideNav/TreeView/index.tsx +0 -94
  120. package/eventcatalog/src/components/TreeView/index.tsx +0 -328
  121. package/eventcatalog/src/components/TreeView/styles.module.css +0 -264
  122. package/eventcatalog/src/components/TreeView/useSlots.ts +0 -95
  123. package/eventcatalog/src/pages/architecture/[type]/index.astro +0 -14
  124. package/eventcatalog/src/pages/architecture/architecture.astro +0 -101
  125. package/eventcatalog/src/pages/architecture/docs/[type]/index.astro +0 -14
  126. package/eventcatalog/src/utils/commands.ts +0 -112
  127. package/eventcatalog/src/utils/events.ts +0 -108
  128. package/eventcatalog/src/utils/generators/index.ts +0 -10
  129. package/eventcatalog/src/utils/teams.ts +0 -72
  130. package/eventcatalog/src/utils/users.ts +0 -72
@@ -1,390 +1,414 @@
1
- import { useState, useMemo, useEffect } from 'react';
1
+ import { memo, useMemo, useState } from 'react';
2
2
  import {
3
3
  ServerIcon,
4
- EnvelopeIcon,
5
4
  RectangleGroupIcon,
6
- Squares2X2Icon,
7
- QueueListIcon,
5
+ BoltIcon,
6
+ ChatBubbleLeftIcon,
7
+ MagnifyingGlassIcon,
8
8
  CircleStackIcon,
9
+ ChevronDownIcon,
10
+ ChevronUpIcon,
11
+ ArrowsPointingOutIcon,
9
12
  } from '@heroicons/react/24/outline';
10
- import { buildUrlWithParams, buildUrl } from '@utils/url-builder';
11
- import type { CollectionEntry } from 'astro:content';
12
- import { type CollectionMessageTypes } from '@types';
13
- import { getCollectionStyles } from './utils';
14
- import { SearchBar } from './components';
15
- import { BoxIcon } from 'lucide-react';
16
-
17
- export interface ExtendedDomain extends CollectionEntry<'domains'> {
18
- sends: CollectionEntry<CollectionMessageTypes>[];
19
- receives: CollectionEntry<CollectionMessageTypes>[];
20
- services: CollectionEntry<'services'>[];
21
- domains: CollectionEntry<'domains'>[];
22
- }
13
+ import { buildUrl } from '@utils/url-builder';
14
+ import { BoxIcon, ArrowRight, ArrowLeft } from 'lucide-react';
15
+
16
+ // ============================================
17
+ // Types
18
+ // ============================================
23
19
 
24
20
  interface DomainGridProps {
25
- domains: ExtendedDomain[];
26
- embeded: boolean;
21
+ domain: any;
27
22
  }
28
23
 
29
- export default function DomainGrid({ domains, embeded }: DomainGridProps) {
30
- const [searchQuery, setSearchQuery] = useState('');
31
- const [isMultiColumn, setIsMultiColumn] = useState(false);
32
-
33
- useEffect(() => {
34
- if (typeof window !== 'undefined') {
35
- const saved = localStorage.getItem('EventCatalog:ArchitectureColumnLayout');
36
- if (saved !== null) {
37
- setIsMultiColumn(saved === 'multi');
38
- }
39
- }
40
- }, []);
41
-
42
- const toggleColumnLayout = () => {
43
- const newValue = !isMultiColumn;
44
- setIsMultiColumn(newValue);
45
- if (typeof window !== 'undefined') {
46
- localStorage.setItem('EventCatalog:ArchitectureColumnLayout', newValue ? 'multi' : 'single');
47
- }
48
- };
49
-
50
- const filteredDomains = useMemo(() => {
51
- let result = [...domains];
52
-
53
- // Filter by search query
54
- if (searchQuery) {
55
- const query = searchQuery.toLowerCase();
56
- result = result.filter(
57
- (domain) =>
58
- domain.data.name?.toLowerCase().includes(query) ||
59
- domain.data.summary?.toLowerCase().includes(query) ||
60
- domain.data.services?.some((service: any) => service.data.name.toLowerCase().includes(query)) ||
61
- domain.sends?.some((message: any) => message.data.name.toLowerCase().includes(query)) ||
62
- domain.receives?.some((message: any) => message.data.name.toLowerCase().includes(query))
63
- );
64
- }
65
-
66
- // Sort by name by default
67
- result.sort((a, b) => (a.data.name || a.data.id).localeCompare(b.data.name || b.data.id));
68
-
69
- return result;
70
- }, [domains, searchQuery]);
24
+ // ============================================
25
+ // Helper functions
26
+ // ============================================
27
+
28
+ const getMessageIcon = (collection: string) => {
29
+ switch (collection) {
30
+ case 'events':
31
+ return { Icon: BoltIcon, color: 'orange' };
32
+ case 'commands':
33
+ return { Icon: ChatBubbleLeftIcon, color: 'blue' };
34
+ case 'queries':
35
+ return { Icon: MagnifyingGlassIcon, color: 'green' };
36
+ default:
37
+ return { Icon: BoltIcon, color: 'gray' };
38
+ }
39
+ };
40
+
41
+ // ============================================
42
+ // Simple Sub-components
43
+ // ============================================
44
+
45
+ const EntityBadge = memo(({ entity }: { entity: any }) => {
46
+ const id = entity?.data?.id || entity?.id;
47
+ const name = entity?.data?.name || entity?.name || id;
71
48
 
72
49
  return (
73
- <div>
74
- {/* Breadcrumb */}
75
- <nav className="mb-4 flex items-center space-x-2 text-sm text-gray-500">
76
- <div className="flex items-center gap-2">
77
- <RectangleGroupIcon className="h-4 w-4" />
78
- <span className="text-gray-900">Domains</span>
79
- </div>
80
- </nav>
81
-
82
- <div className="relative border-b border-gray-200 mb-4 pb-4">
83
- <div className="md:flex md:items-start md:justify-between">
84
- <div className="min-w-0 flex-1 max-w-lg">
85
- <h1 className="text-2xl font-bold leading-7 text-gray-900 sm:text-3xl sm:truncate">
86
- Domains ({filteredDomains.length})
87
- </h1>
88
- <p className="mt-2 text-sm text-gray-500">Browse and manage domains in your event-driven architecture</p>
50
+ <a
51
+ href={buildUrl(`/docs/entities/${id}`)}
52
+ className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-purple-100 border border-purple-300 rounded-md text-xs font-medium hover:bg-purple-200 transition-colors"
53
+ >
54
+ <BoxIcon className="h-3.5 w-3.5 text-purple-600" />
55
+ <span className="text-purple-800">{name}</span>
56
+ </a>
57
+ );
58
+ });
59
+
60
+ const MessageBadge = memo(({ message }: { message: any }) => {
61
+ const data = message?.data || message;
62
+ const collection = message?.collection || 'events';
63
+ const { Icon, color } = getMessageIcon(collection);
64
+ const id = data?.id || message?.id;
65
+ const name = data?.name || data?.id || id;
66
+ const version = data?.version;
67
+
68
+ return (
69
+ <a
70
+ href={buildUrl(`/docs/${collection}/${id}/${version}`)}
71
+ className="flex items-center gap-1.5 px-2 py-1 bg-white border border-gray-200 rounded text-[11px] font-medium hover:bg-gray-50 transition-colors"
72
+ >
73
+ <Icon className={`h-3 w-3 text-${color}-500`} />
74
+ <span className="text-gray-700 truncate max-w-[120px]">{name}</span>
75
+ </a>
76
+ );
77
+ });
78
+
79
+ const ContainerBadge = memo(({ container, type }: { container: any; type: 'reads' | 'writes' }) => {
80
+ const data = container?.data || container;
81
+ const id = data?.id || container?.id;
82
+ const name = data?.name || id;
83
+ const version = data?.version;
84
+ const colorClass = type === 'reads' ? 'orange' : 'purple';
85
+
86
+ return (
87
+ <a
88
+ href={buildUrl(`/docs/containers/${id}/${version}`)}
89
+ className={`inline-flex items-center gap-1.5 px-2 py-1 bg-${colorClass}-100 border border-${colorClass}-300 rounded text-[11px] font-medium hover:bg-${colorClass}-200 transition-colors`}
90
+ >
91
+ <CircleStackIcon className={`h-3 w-3 text-${colorClass}-600`} />
92
+ <span className={`text-${colorClass}-800`}>{name}</span>
93
+ </a>
94
+ );
95
+ });
96
+
97
+ const ServiceCard = memo(({ service }: { service: any }) => {
98
+ const data = service?.data || service;
99
+ if (!data?.id) return null;
100
+
101
+ const receives = data.receives || [];
102
+ const sends = data.sends || [];
103
+ const readsFrom = data.readsFrom || [];
104
+ const writesTo = data.writesTo || [];
105
+ const hasMessages = receives.length > 0 || sends.length > 0;
106
+ const hasContainers = readsFrom.length > 0 || writesTo.length > 0;
107
+
108
+ return (
109
+ <div className="bg-white border-2 border-dashed border-pink-400 rounded-lg p-4">
110
+ {/* Service Header */}
111
+ <div className="flex items-center justify-between">
112
+ <a
113
+ href={buildUrl(`/architecture/services/${data.id}/${data.version}`)}
114
+ className="flex items-center gap-2 hover:underline"
115
+ >
116
+ <ServerIcon className="h-5 w-5 text-pink-500" />
117
+ <span className="font-semibold text-gray-900">{data.name || data.id}</span>
118
+ <span className="text-xs text-gray-500">v{data.version}</span>
119
+ </a>
120
+ <a
121
+ href={buildUrl(`/architecture/services/${data.id}/${data.version}`)}
122
+ className="p-1 hover:bg-pink-100 rounded-md transition-colors duration-200"
123
+ title="Expand service architecture"
124
+ onClick={(e) => e.stopPropagation()}
125
+ >
126
+ <ArrowsPointingOutIcon className="h-4 w-4 text-gray-500 hover:text-pink-600" />
127
+ </a>
128
+ </div>
129
+
130
+ {data.summary && <p className="mt-2 text-sm text-gray-600 line-clamp-2">{data.summary}</p>}
131
+
132
+ {/* Message Flow Diagram */}
133
+ {hasMessages && (
134
+ <div className="mt-4 flex items-stretch gap-3">
135
+ {/* Receives (Inbound) */}
136
+ <div className="flex-1 bg-blue-50 border border-blue-200 rounded-lg p-3">
137
+ <div className="flex items-center gap-1.5 mb-2">
138
+ <ArrowRight className="h-3.5 w-3.5 text-blue-500" />
139
+ <span className="text-xs font-semibold text-blue-700 uppercase">Inbound Messages</span>
140
+ <span className="text-xs text-blue-500">({receives.length})</span>
141
+ </div>
142
+ {receives.length > 0 ? (
143
+ <div className="space-y-1.5">
144
+ {receives.slice(0, 4).map((msg: any, idx: number) => {
145
+ const msgId = msg?.data?.id || msg?.id;
146
+ return msgId ? <MessageBadge key={`${msgId}-${idx}`} message={msg} /> : null;
147
+ })}
148
+ {receives.length > 4 && <p className="text-[10px] text-gray-500 text-center">+{receives.length - 4} more</p>}
149
+ </div>
150
+ ) : (
151
+ <p className="text-[10px] text-gray-400 italic">No incoming messages</p>
152
+ )}
89
153
  </div>
90
154
 
91
- <div className="mt-6 md:mt-0 md:ml-4 flex-shrink-0 flex items-center gap-3">
92
- <SearchBar
93
- searchQuery={searchQuery}
94
- onSearchChange={setSearchQuery}
95
- placeholder="Search domains..."
96
- totalResults={filteredDomains.length}
97
- totalItems={domains.length}
98
- />
99
- <button
100
- onClick={toggleColumnLayout}
101
- className="flex items-center justify-center p-2 bg-white border border-gray-300 rounded-md hover:bg-gray-50 transition-colors duration-200"
102
- title={isMultiColumn ? 'Switch to single column' : 'Switch to multi column'}
103
- >
104
- {isMultiColumn ? (
105
- <QueueListIcon className="h-5 w-5 text-gray-600" />
106
- ) : (
107
- <Squares2X2Icon className="h-5 w-5 text-gray-600" />
108
- )}
109
- </button>
155
+ {/* Service Icon (Center) */}
156
+ <div className="flex items-center">
157
+ <div className="bg-pink-100 border-2 border-pink-300 rounded-lg p-3">
158
+ <ServerIcon className="h-6 w-6 text-pink-500" />
159
+ </div>
110
160
  </div>
111
- </div>
112
- </div>
113
161
 
114
- <div className={`grid gap-6 ${isMultiColumn ? 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-2 xl:grid-cols-2' : 'grid-cols-1'}`}>
115
- {filteredDomains.map((domain) => (
116
- <a
117
- key={domain.data.id}
118
- href={buildUrlWithParams('/architecture/services', {
119
- serviceIds: domain.data.services?.map((s: any) => s.data.id).join(','),
120
- domainId: domain.data.id,
121
- domainName: domain.data.name,
122
- })}
123
- className="group hover:bg-orange-100 border-2 border-orange-400/50 bg-yellow-50 rounded-lg shadow-sm hover:shadow-lg transition-all duration-200 overflow-hidden"
124
- >
125
- <div className="p-6">
126
- <div className="flex items-center justify-between mb-3">
127
- <div className="flex items-center gap-2">
128
- <RectangleGroupIcon className="h-5 w-5 text-orange-500" />
129
- <h3 className="text-lg font-semibold text-gray-900 truncate group-hover:underline transition-colors duration-200">
130
- {domain.data.name || domain.data.id}
131
- </h3>
132
- </div>
133
- <span className="ml-2 shrink-0 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-orange-50 text-orange-700">
134
- v{domain.data.version}
135
- </span>
162
+ {/* Sends (Outbound) */}
163
+ <div className="flex-1 bg-green-50 border border-green-200 rounded-lg p-3">
164
+ <div className="flex items-center gap-1.5 mb-2">
165
+ <ArrowLeft className="h-3.5 w-3.5 text-green-500" />
166
+ <span className="text-xs font-semibold text-green-700 uppercase">Outbound Messages</span>
167
+ <span className="text-xs text-green-500">({sends.length})</span>
168
+ </div>
169
+ {sends.length > 0 ? (
170
+ <div className="space-y-1.5">
171
+ {sends.slice(0, 4).map((msg: any, idx: number) => {
172
+ const msgId = msg?.data?.id || msg?.id;
173
+ return msgId ? <MessageBadge key={`${msgId}-${idx}`} message={msg} /> : null;
174
+ })}
175
+ {sends.length > 4 && <p className="text-[10px] text-gray-500 text-center">+{sends.length - 4} more</p>}
136
176
  </div>
177
+ ) : (
178
+ <p className="text-[10px] text-gray-400 italic">No outgoing messages</p>
179
+ )}
180
+ </div>
181
+ </div>
182
+ )}
137
183
 
138
- <p className="text-gray-600 text-sm line-clamp-2 min-h-[2.5rem]">
139
- {domain.data.summary || <span className="italic">No summary available</span>}
140
- </p>
141
-
142
- <div className="flex gap-4 mb-4">
143
- <div className="flex items-center gap-2 bg-white rounded-lg px-3 py-2 border border-orange-200">
144
- <RectangleGroupIcon className="h-4 w-4 text-yellow-500" />
145
- <div className="flex">
146
- <p className="text-sm font-medium text-gray-900">{domain.data.domains?.length || 0} Subdomains</p>
147
- </div>
148
- </div>
149
- <div className="flex items-center gap-2 bg-white rounded-lg px-3 py-2 border border-pink-200">
150
- <ServerIcon className="h-4 w-4 text-pink-500" />
151
- <div className="flex">
152
- <p className="text-sm font-medium text-gray-900">{domain.data.services?.length || 0} Services</p>
153
- </div>
154
- </div>
155
- <div className="flex items-center gap-2 bg-white rounded-lg px-3 py-2 border border-gray-200 ">
156
- <EnvelopeIcon className="h-4 w-4 text-blue-500" />
157
- <div>
158
- <p className="text-sm font-medium text-gray-900">
159
- {(domain.sends?.length || 0) + (domain.receives?.length || 0)} Messages
160
- </p>
161
- </div>
162
- </div>
163
- {domain.data.entities && domain.data.entities.length > 0 && (
164
- <div className="flex items-center gap-2 bg-white rounded-lg px-3 py-2 border border-gray-200 ">
165
- <BoxIcon className="h-4 w-4 text-purple-500" />
166
- <div>
167
- <p className="text-sm font-medium text-gray-900">{domain.data.entities?.length} Entities</p>
168
- </div>
169
- </div>
170
- )}
184
+ {/* Container Relationships */}
185
+ {hasContainers && (
186
+ <div className="mt-4 pt-4 border-t border-gray-200 grid grid-cols-2 gap-4">
187
+ {/* Reads From */}
188
+ {readsFrom.length > 0 && (
189
+ <div>
190
+ <div className="flex items-center gap-1.5 mb-2">
191
+ <CircleStackIcon className="h-3.5 w-3.5 text-orange-500" />
192
+ <span className="text-xs font-semibold text-gray-700">Reads from</span>
193
+ </div>
194
+ <div className="flex flex-wrap gap-1.5">
195
+ {readsFrom.slice(0, 3).map((container: any, idx: number) => {
196
+ const containerId = container?.data?.id || container?.id;
197
+ return containerId ? <ContainerBadge key={`${containerId}-${idx}`} container={container} type="reads" /> : null;
198
+ })}
199
+ {readsFrom.length > 3 && <span className="text-[10px] text-gray-500">+{readsFrom.length - 3} more</span>}
171
200
  </div>
201
+ </div>
202
+ )}
172
203
 
173
- <div className="space-y-6">
174
- {/* Subdomains and there services */}
175
- {domain.data.domains?.slice(0, 2).map((subdomain: any) => (
176
- <div
177
- key={subdomain.data.id}
178
- className="block space-y-2 bg-white border-2 border-dashed border-orange-400 p-4 rounded-lg transition-colors duration-200"
179
- >
180
- <div className="flex items-center justify-between">
181
- <div className="flex items-center gap-2">
182
- <RectangleGroupIcon className="h-4 w-4 text-orange-500" />
183
- <h4 className="text-sm font-medium text-gray-900">
184
- {subdomain.data.name || subdomain.data.id} (Subdomain)
185
- </h4>
186
- </div>
187
- <span className="text-xs text-gray-500">v{subdomain.data.version}</span>
188
- </div>
189
-
190
- <div className="flex gap-4">
191
- <div className="flex items-center gap-2 bg-white rounded-lg px-3 py-2 border border-pink-200">
192
- <ServerIcon className="h-4 w-4 text-pink-500" />
193
- <div className="flex">
194
- <p className="text-sm font-medium text-gray-900">{subdomain.data.services?.length || 0} Services</p>
195
- </div>
196
- </div>
197
- <div className="flex items-center gap-2 bg-white rounded-lg px-3 py-2 border border-gray-200">
198
- <EnvelopeIcon className="h-4 w-4 text-blue-500" />
199
- <div>
200
- <p className="text-sm font-medium text-gray-900">
201
- {(subdomain.sends?.length || 0) + (subdomain.receives?.length || 0)} Messages
202
- </p>
203
- </div>
204
- </div>
205
- </div>
206
- </div>
207
- ))}
208
-
209
- {/* Services and their messages */}
210
- {domain.data.services?.slice(0, 2).map((service: any) => (
211
- <div
212
- key={service.data.id}
213
- className="block space-y-2 bg-white border-2 border-dashed border-pink-400 p-4 rounded-lg transition-colors duration-200"
214
- >
215
- <div className="flex items-center justify-between">
216
- <div className="flex items-center gap-2">
217
- <ServerIcon className="h-4 w-4 text-pink-500" />
218
- <h4 className="text-sm font-medium text-gray-900">{service.data.name || service.data.id}</h4>
219
- </div>
220
- <span className="text-xs text-gray-500">v{service.data.version}</span>
221
- </div>
222
-
223
- <div className="flex items-center gap-4">
224
- <div className="flex-1 h-full flex flex-col bg-blue-50 border border-blue-300 rounded-lg p-3">
225
- <div className="space-y-1.5 flex-1">
226
- {service.data.receives?.slice(0, 3).map((message: any) => {
227
- const { Icon, color } = getCollectionStyles(message.collection);
228
- return (
229
- <div
230
- key={`${message.id}-${message.version}`}
231
- className="group flex border border-gray-200 items-center gap-1 rounded-md text-[11px] font-medium bg-white"
232
- >
233
- <div className="bg-white border-r border-gray-200 px-2 py-1.5 rounded-l-md">
234
- <Icon className={`h-3 w-3 text-${color}-500`} />
235
- </div>
236
- <span className="px-1 py-1 truncate max-w-[140px]">{message.id}</span>
237
- </div>
238
- );
239
- })}
240
- {service.data.receives && service.data.receives.length > 3 && (
241
- <div className="text-center py-1">
242
- <p className="text-gray-500 text-[10px]">+ {service.data.receives.length - 3} more</p>
243
- </div>
244
- )}
245
- {!service.data.receives?.length && (
246
- <div className="text-center py-6">
247
- <p className="text-gray-500 text-xs">No messages received</p>
248
- </div>
249
- )}
250
- </div>
251
- </div>
252
-
253
- <div className="flex items-center gap-2">
254
- <div className="w-4 h-[2px] bg-blue-200"></div>
255
- <div className="bg-white border-2 border-gray-300 rounded-lg p-2 shadow-sm">
256
- <div className="flex flex-col items-center gap-2">
257
- <ServerIcon className="h-6 w-6 text-pink-500" />
258
- <div className="text-center">
259
- <p className="text-xs font-medium text-gray-900">{service.data.name || service.data.id}</p>
260
- <p className="text-[10px] text-gray-500">v{service.data.version}</p>
261
- </div>
262
- </div>
263
- </div>
264
- <div className="w-4 h-[2px] bg-emerald-200"></div>
265
- </div>
266
-
267
- <div className="flex-1 h-full flex flex-col bg-green-100 border border-green-300 rounded-lg p-3">
268
- <div className="space-y-1.5 flex-1">
269
- {service.data.sends?.slice(0, 3).map((message: any) => {
270
- const { Icon, color } = getCollectionStyles(message.collection);
271
- return (
272
- <div
273
- key={`${message.id}-${message.version}`}
274
- className="group flex border border-gray-200 items-center gap-1 rounded-md text-[11px] font-medium bg-white"
275
- >
276
- <div className="bg-white border-r border-gray-200 px-2 py-1.5 rounded-l-md">
277
- <Icon className={`h-3 w-3 text-${color}-500`} />
278
- </div>
279
-
280
- <span className="px-1 py-1 truncate max-w-[140px]">{message.id}</span>
281
- </div>
282
- );
283
- })}
284
- {service.data.sends && service.data.sends.length > 3 && (
285
- <div className="text-center py-1">
286
- <p className="text-gray-500 text-xs">+ {service.data.sends.length - 3} more</p>
287
- </div>
288
- )}
289
- {!service.data.sends?.length && (
290
- <div className="text-center py-6">
291
- <p className="text-gray-500 text-xs">No messages sent</p>
292
- </div>
293
- )}
294
- </div>
295
- </div>
296
- </div>
297
-
298
- {/* Container lists at the bottom */}
299
- {((service.data.readsFrom && service.data.readsFrom.length > 0) ||
300
- (service.data.writesTo && service.data.writesTo.length > 0)) && (
301
- <div className="mt-3 pt-3 border-t border-gray-200 grid grid-cols-2 gap-4">
302
- {/* Reads From */}
303
- {service.data.readsFrom && service.data.readsFrom.length > 0 && (
304
- <div className="space-y-2">
305
- <div className="flex items-center gap-2">
306
- <CircleStackIcon className="h-4 w-4 text-orange-500" />
307
- <h4 className="text-xs font-semibold text-gray-700">Reads from</h4>
308
- </div>
309
- <div className="flex flex-wrap gap-1">
310
- {service.data.readsFrom.slice(0, 3).map((container: any) => (
311
- <span
312
- key={container.id}
313
- className="group inline-flex items-center gap-1 px-2 py-1 bg-orange-100 border border-orange-300 rounded-md text-[11px] font-medium hover:bg-orange-200 transition-colors duration-200"
314
- >
315
- <CircleStackIcon className="h-3 w-3 text-orange-600" />
316
- <span className="text-orange-800">{container.id}</span>
317
- </span>
318
- ))}
319
- {service.data.readsFrom.length > 3 && (
320
- <span className="inline-flex items-center px-2 py-1 text-xs text-gray-500">
321
- + {service.data.readsFrom.length - 3} more
322
- </span>
323
- )}
324
- </div>
325
- </div>
326
- )}
327
-
328
- {/* Writes To */}
329
- {service.data.writesTo && service.data.writesTo.length > 0 && (
330
- <div className="space-y-2">
331
- <div className="flex items-center gap-2">
332
- <CircleStackIcon className="h-4 w-4 text-purple-500" />
333
- <h4 className="text-xs font-semibold text-gray-700">Writes to</h4>
334
- </div>
335
- <div className="flex flex-wrap gap-1">
336
- {service.data.writesTo.slice(0, 3).map((container: any) => (
337
- <span
338
- key={container.id}
339
- className="group inline-flex items-center gap-1 px-2 py-1 bg-purple-100 border border-purple-300 rounded-md text-[11px] font-medium hover:bg-purple-200 transition-colors duration-200"
340
- >
341
- <CircleStackIcon className="h-3 w-3 text-purple-600" />
342
- <span className="text-purple-800">{container.id}</span>
343
- </span>
344
- ))}
345
- {service.data.writesTo.length > 3 && (
346
- <span className="inline-flex items-center px-2 py-1 text-xs text-gray-500">
347
- + {service.data.writesTo.length - 3} more
348
- </span>
349
- )}
350
- </div>
351
- </div>
352
- )}
353
- </div>
354
- )}
355
- </div>
356
- ))}
357
- {domain.data.domains && domain.data.domains.length > 2 && (
358
- <div className="block space-y-2 bg-white border-2 border-dashed border-orange-400 p-4 rounded-lg transition-colors duration-200">
359
- <div className="flex items-center justify-between">
360
- <div className="flex items-center gap-2">
361
- <RectangleGroupIcon className="h-4 w-4 text-orange-500/70" />
362
- <h4 className="text-sm font-medium text-gray-600">+{domain.data.domains.length - 2} more subdomains</h4>
363
- </div>
364
- </div>
365
- </div>
366
- )}
367
- {domain.data.services && domain.data.services.length > 2 && (
368
- <div className="block space-y-2 bg-white border-2 border-dashed border-pink-400 p-4 rounded-lg transition-colors duration-200">
369
- <div className="flex items-center justify-between">
370
- <div className="flex items-center gap-2">
371
- <ServerIcon className="h-4 w-4 text-pink-500/70" />
372
- <h4 className="text-sm font-medium text-gray-600">+{domain.data.services.length - 2} more services</h4>
373
- </div>
374
- </div>
375
- </div>
376
- )}
204
+ {/* Writes To */}
205
+ {writesTo.length > 0 && (
206
+ <div>
207
+ <div className="flex items-center gap-1.5 mb-2">
208
+ <CircleStackIcon className="h-3.5 w-3.5 text-purple-500" />
209
+ <span className="text-xs font-semibold text-gray-700">Writes to</span>
210
+ </div>
211
+ <div className="flex flex-wrap gap-1.5">
212
+ {writesTo.slice(0, 3).map((container: any, idx: number) => {
213
+ const containerId = container?.data?.id || container?.id;
214
+ return containerId ? (
215
+ <ContainerBadge key={`${containerId}-${idx}`} container={container} type="writes" />
216
+ ) : null;
217
+ })}
218
+ {writesTo.length > 3 && <span className="text-[10px] text-gray-500">+{writesTo.length - 3} more</span>}
377
219
  </div>
378
220
  </div>
221
+ )}
222
+ </div>
223
+ )}
224
+ </div>
225
+ );
226
+ });
227
+
228
+ const SubdomainSection = memo(({ subdomain }: { subdomain: any }) => {
229
+ const data = subdomain?.data || subdomain;
230
+ const [isCollapsed, setIsCollapsed] = useState(false);
231
+
232
+ if (!data?.id) return null;
233
+
234
+ const services = data.services || [];
235
+ const entities = data.entities || [];
236
+
237
+ return (
238
+ <div className="bg-orange-50 border-2 border-orange-400 rounded-lg p-6">
239
+ {/* Subdomain Header */}
240
+ <div className="flex items-center justify-between mb-4">
241
+ <div className="flex items-center gap-2">
242
+ <RectangleGroupIcon className="h-5 w-5 text-orange-500" />
243
+ <h3 className="text-lg font-semibold text-gray-900">{data.name || data.id}</h3>
244
+ <span className="text-xs text-gray-500">v{data.version}</span>
245
+ <span className="px-2 py-0.5 bg-orange-200 text-orange-800 text-xs rounded">Subdomain</span>
246
+ </div>
247
+ <div className="flex gap-2">
248
+ <button
249
+ onClick={() => setIsCollapsed(!isCollapsed)}
250
+ className="p-1 hover:bg-orange-200 rounded-md transition-colors cursor-pointer text-gray-500 hover:text-gray-700"
251
+ >
252
+ {isCollapsed ? <ChevronDownIcon className="h-5 w-5" /> : <ChevronUpIcon className="h-5 w-5" />}
253
+ </button>
254
+ <a
255
+ href={buildUrl(`/architecture/domains/${data.id}/${data.version}`)}
256
+ className="p-1 hover:bg-orange-200 rounded-md transition-colors cursor-pointer text-gray-500 hover:text-gray-700"
257
+ title="Expand domain architecture"
258
+ onClick={(e) => e.stopPropagation()}
259
+ >
260
+ <ArrowsPointingOutIcon className="h-5 w-5" />
379
261
  </a>
380
- ))}
262
+ </div>
381
263
  </div>
382
264
 
383
- {filteredDomains.length === 0 && (
384
- <div className="text-center py-12">
385
- <p className="text-gray-500 text-lg">No domains found matching your criteria</p>
386
- </div>
265
+ {!isCollapsed && (
266
+ <>
267
+ {/* Subdomain Entities */}
268
+ {entities.length > 0 && (
269
+ <div className="mb-4">
270
+ <h4 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-2">Entities</h4>
271
+ <div className="flex flex-wrap gap-2">
272
+ {entities.map((entity: any) => {
273
+ const entityId = entity?.data?.id || entity?.id;
274
+ return entityId ? <EntityBadge key={entityId} entity={entity} /> : null;
275
+ })}
276
+ </div>
277
+ </div>
278
+ )}
279
+
280
+ {/* Subdomain Services */}
281
+ {services.length > 0 && (
282
+ <div>
283
+ <h4 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-2">Services</h4>
284
+ <div className="grid gap-4 xl:grid-cols-2">
285
+ {services.map((service: any) => {
286
+ const serviceId = service?.data?.id || service?.id;
287
+ // Ensure we pass the service down with its messages populated
288
+ return serviceId ? <ServiceCard key={serviceId} service={service} /> : null;
289
+ })}
290
+ </div>
291
+ </div>
292
+ )}
293
+
294
+ {entities.length === 0 && services.length === 0 && (
295
+ <p className="text-sm text-gray-500 italic">No entities or services in this subdomain</p>
296
+ )}
297
+ </>
387
298
  )}
388
299
  </div>
389
300
  );
301
+ });
302
+
303
+ // ============================================
304
+ // Main Component
305
+ // ============================================
306
+
307
+ export default function DomainGrid({ domain }: DomainGridProps) {
308
+ const data = domain?.data;
309
+ if (!data) return <div>No domain data</div>;
310
+
311
+ const subdomains = data.domains || [];
312
+ const entities = data.entities || [];
313
+ const services = data.services || [];
314
+
315
+ // Get services that are NOT in any subdomain
316
+ const subdomainServiceIds = useMemo(
317
+ () =>
318
+ new Set(
319
+ subdomains.flatMap((sd: any) => {
320
+ const sdData = sd?.data || sd;
321
+ return (sdData?.services || []).map((s: any) => s?.data?.id || s?.id);
322
+ })
323
+ ),
324
+ [subdomains]
325
+ );
326
+
327
+ const topLevelServices = useMemo(
328
+ () =>
329
+ services.filter((s: any) => {
330
+ const sId = s?.data?.id || s?.id;
331
+ return sId && !subdomainServiceIds.has(sId);
332
+ }),
333
+ [services, subdomainServiceIds]
334
+ );
335
+
336
+ return (
337
+ <div className="space-y-6">
338
+ {/* Domain Container - Yellow */}
339
+ <div className="bg-yellow-50 border-2 border-yellow-400 rounded-xl p-6">
340
+ {/* Domain Header */}
341
+ <div className="flex items-center justify-between mb-4">
342
+ <div className="flex items-center gap-3">
343
+ <RectangleGroupIcon className="h-7 w-7 text-yellow-600" />
344
+ <h1 className="text-2xl font-bold text-gray-900">{data.name || data.id}</h1>
345
+ <span className="px-2 py-0.5 bg-yellow-200 text-yellow-800 text-xs font-medium rounded">v{data.version}</span>
346
+ <span className="px-2 py-0.5 bg-yellow-300 text-yellow-900 text-xs font-medium rounded">Domain</span>
347
+ </div>
348
+ <div className="flex gap-3">
349
+ <a
350
+ href={buildUrl(`/docs/domains/${data.id}/${data.version}`)}
351
+ className="text-sm bg-white px-3 py-1.5 rounded border border-gray-300 hover:bg-gray-50 transition-colors"
352
+ >
353
+ View docs
354
+ </a>
355
+ <a
356
+ href={buildUrl(`/visualiser/domains/${data.id}/${data.version}`)}
357
+ className="text-sm bg-white px-3 py-1.5 rounded border border-gray-300 hover:bg-gray-50 transition-colors"
358
+ >
359
+ Visualizer
360
+ </a>
361
+ </div>
362
+ </div>
363
+
364
+ {data.summary && <p className="text-gray-600 mb-4">{data.summary}</p>}
365
+
366
+ {/* Domain Entities */}
367
+ {entities.length > 0 && (
368
+ <div className="mb-6">
369
+ <h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-2">Entities</h3>
370
+ <div className="flex flex-wrap gap-2">
371
+ {entities.map((entity: any) => {
372
+ const entityId = entity?.data?.id || entity?.id;
373
+ return entityId ? <EntityBadge key={entityId} entity={entity} /> : null;
374
+ })}
375
+ </div>
376
+ </div>
377
+ )}
378
+
379
+ {/* Top-level Services (not in subdomains) */}
380
+ {topLevelServices.length > 0 && (
381
+ <div className="mb-6">
382
+ <h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-3">Services</h3>
383
+ <div className="grid gap-4 xl:grid-cols-2">
384
+ {topLevelServices.map((service: any) => {
385
+ const serviceId = service?.data?.id || service?.id;
386
+ return serviceId ? <ServiceCard key={serviceId} service={service} /> : null;
387
+ })}
388
+ </div>
389
+ </div>
390
+ )}
391
+
392
+ {/* Subdomains - nested inside domain */}
393
+ {subdomains.length > 0 && (
394
+ <div>
395
+ <h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-3">Subdomains</h3>
396
+ <div className="space-y-4">
397
+ {subdomains.map((subdomain: any) => {
398
+ const subdomainId = subdomain?.data?.id || subdomain?.id;
399
+ return subdomainId ? <SubdomainSection key={subdomainId} subdomain={subdomain} /> : null;
400
+ })}
401
+ </div>
402
+ </div>
403
+ )}
404
+
405
+ {/* Empty state */}
406
+ {entities.length === 0 && services.length === 0 && subdomains.length === 0 && (
407
+ <div className="text-center py-8">
408
+ <p className="text-gray-500">This domain has no entities, services, or subdomains defined.</p>
409
+ </div>
410
+ )}
411
+ </div>
412
+ </div>
413
+ );
390
414
  }