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

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 (64) hide show
  1. package/dist/analytics/analytics.cjs +1 -1
  2. package/dist/analytics/analytics.js +2 -2
  3. package/dist/analytics/log-build.cjs +1 -1
  4. package/dist/analytics/log-build.js +3 -3
  5. package/dist/{chunk-AYNZXUB5.js → chunk-3PKS2IIP.js} +4 -4
  6. package/dist/{chunk-HGRFXTFC.js → chunk-423S2KHN.js} +1 -1
  7. package/dist/{chunk-KIDISU6X.js → chunk-6OC46WQJ.js} +1 -1
  8. package/dist/{chunk-4J2ZM2HX.js → chunk-BH2F5WPN.js} +1 -1
  9. package/dist/{chunk-62EV4IJA.js → chunk-GYIVJXOJ.js} +1 -1
  10. package/dist/{chunk-XARPIXWF.js → chunk-NDJOAITI.js} +1 -1
  11. package/dist/constants.cjs +1 -1
  12. package/dist/constants.js +1 -1
  13. package/dist/eventcatalog.cjs +1 -1
  14. package/dist/eventcatalog.js +13 -13
  15. package/dist/federation/federate.js +4 -4
  16. package/dist/federation/source-provider.js +2 -2
  17. package/dist/generate.cjs +1 -1
  18. package/dist/generate.js +3 -3
  19. package/dist/utils/cli-logger.cjs +1 -1
  20. package/dist/utils/cli-logger.js +2 -2
  21. package/eventcatalog/src/components/Grids/utils.tsx +0 -23
  22. package/eventcatalog/src/components/MDX/CodeGroup/CodeGroup.tsx +6 -1
  23. package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsPanel.tsx +12 -4
  24. package/eventcatalog/src/components/Tables/Discover/DiscoverTable.tsx +4 -62
  25. package/eventcatalog/src/components/Tables/Discover/columns.tsx +17 -150
  26. package/eventcatalog/src/components/Tables/Resources/ResourcesTable.tsx +425 -0
  27. package/eventcatalog/src/components/Tables/RowActionsMenu.tsx +157 -0
  28. package/eventcatalog/src/components/Tables/TablePagination.tsx +82 -0
  29. package/eventcatalog/src/features/ai/chat-api.ts +3 -3
  30. package/eventcatalog/src/features/mcp/apps/generated/viewer.html +75 -74
  31. package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +0 -2
  32. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.spec.ts +52 -63
  33. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.ts +34 -46
  34. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/index.astro +11 -29
  35. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_ResourcesPage.astro +79 -0
  36. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_index.data.spec.ts +32 -14
  37. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_index.data.ts +63 -58
  38. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/index.astro +2 -62
  39. package/eventcatalog/src/pages/docs/services/[id]/[version]/resources/index.astro +13 -0
  40. package/eventcatalog/src/stores/sidebar-store/builders/domain.ts +24 -42
  41. package/eventcatalog/src/stores/sidebar-store/builders/flow.ts +2 -0
  42. package/eventcatalog/src/stores/sidebar-store/builders/message.ts +2 -0
  43. package/eventcatalog/src/stores/sidebar-store/builders/service.ts +23 -23
  44. package/eventcatalog/src/stores/sidebar-store/builders/shared.ts +13 -0
  45. package/eventcatalog/src/stores/sidebar-store/builders/system.ts +18 -27
  46. package/eventcatalog/src/stores/sidebar-store/state.ts +5 -0
  47. package/eventcatalog/src/styles/tailwind.css +2 -2
  48. package/eventcatalog/src/utils/collections/channels.ts +56 -1
  49. package/eventcatalog/src/utils/collections/flows.ts +50 -1
  50. package/eventcatalog/src/utils/collections/message-connections.ts +60 -0
  51. package/eventcatalog/src/utils/collections/message-triggers.ts +2 -1
  52. package/eventcatalog/src/utils/collections/resource-owners.ts +193 -0
  53. package/eventcatalog/src/utils/collections/resources.ts +310 -0
  54. package/eventcatalog/src/utils/collections/util.ts +31 -0
  55. package/package.json +6 -6
  56. package/eventcatalog/src/components/Grids/DomainGrid.tsx +0 -838
  57. package/eventcatalog/src/components/Grids/MessageGrid.tsx +0 -344
  58. package/eventcatalog/src/components/Grids/SystemGrid.tsx +0 -185
  59. package/eventcatalog/src/components/Grids/components.tsx +0 -170
  60. package/eventcatalog/src/components/Grids/message-link.ts +0 -38
  61. package/eventcatalog/src/components/Tables/SystemResources/SystemResourcesTable.tsx +0 -294
  62. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_resources.spec.ts +0 -65
  63. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_resources.ts +0 -46
  64. package/dist/{chunk-A2RZR3U4.js → chunk-6K7XZAYI.js} +3 -3
@@ -1,838 +0,0 @@
1
- import { memo, useMemo, useState } from 'react';
2
- import {
3
- ServerIcon,
4
- RectangleGroupIcon,
5
- BoltIcon,
6
- ChatBubbleLeftIcon,
7
- MagnifyingGlassIcon,
8
- CircleStackIcon,
9
- ChevronDownIcon,
10
- ChevronUpIcon,
11
- ArrowTopRightOnSquareIcon,
12
- ArrowLongRightIcon,
13
- ArrowLongLeftIcon,
14
- CubeIcon,
15
- GlobeAltIcon,
16
- } from '@heroicons/react/24/outline';
17
- import { buildUrl } from '@utils/url-builder';
18
- import { BoxIcon, Group as GroupIcon } from 'lucide-react';
19
- import { getSpecUrl, getSpecIcon, getSpecLabel, getServiceSpecifications } from './specification-utils';
20
- import { getMessageLinkProps } from './message-link';
21
-
22
- // ============================================
23
- // Types
24
- // ============================================
25
-
26
- interface DomainGridProps {
27
- domain: any;
28
- }
29
-
30
- // ============================================
31
- // Helper functions
32
- // ============================================
33
-
34
- const getMessageIcon = (collection: string) => {
35
- switch (collection) {
36
- case 'events':
37
- return { Icon: BoltIcon, color: 'orange' };
38
- case 'commands':
39
- return { Icon: ChatBubbleLeftIcon, color: 'blue' };
40
- case 'queries':
41
- return { Icon: MagnifyingGlassIcon, color: 'emerald' };
42
- default:
43
- return { Icon: BoltIcon, color: 'gray' };
44
- }
45
- };
46
-
47
- // ============================================
48
- // Sub-components
49
- // ============================================
50
-
51
- export const EntityBadge = memo(({ entity }: { entity: any }) => {
52
- const id = entity?.data?.id || entity?.id;
53
- const name = entity?.data?.name || entity?.name || id;
54
- const version = entity?.data?.version || entity?.version || 'latest';
55
-
56
- return (
57
- <a
58
- href={buildUrl(`/docs/entities/${id}/${version}`)}
59
- 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"
60
- >
61
- <BoxIcon className="h-4 w-4 text-[rgb(var(--ec-accent))]" />
62
- <span>{name}</span>
63
- </a>
64
- );
65
- });
66
-
67
- const MessageLink = memo(({ message }: { message: any }) => {
68
- const { collection, name, version, href } = getMessageLinkProps(message);
69
- const { Icon, color } = getMessageIcon(collection || '');
70
-
71
- const iconStyles: Record<string, string> = {
72
- orange: 'text-orange-500',
73
- blue: 'text-blue-500',
74
- emerald: 'text-emerald-500',
75
- gray: 'text-gray-500',
76
- };
77
-
78
- const content = (
79
- <>
80
- <Icon className={`h-4 w-4 flex-shrink-0 ${iconStyles[color]}`} />
81
- <span className={href ? 'group-hover:underline' : undefined}>{name}</span>
82
- <span className="text-xs text-[rgb(var(--ec-icon-color))]">v{version}</span>
83
- </>
84
- );
85
-
86
- const className =
87
- 'flex items-center gap-2 py-1.5 text-sm text-[rgb(var(--ec-page-text-muted))] hover:text-[rgb(var(--ec-page-text))] transition-colors group';
88
-
89
- // Missing collection means an unhydrated pointer — do not guess `/docs/events/...`.
90
- if (!href) {
91
- return <span className={className}>{content}</span>;
92
- }
93
-
94
- return (
95
- <a href={buildUrl(href)} className={className}>
96
- {content}
97
- </a>
98
- );
99
- });
100
-
101
- const SpecificationBadge = memo(
102
- ({ spec, serviceId, serviceVersion }: { spec: any; serviceId: string; serviceVersion: string }) => {
103
- return (
104
- <a
105
- href={getSpecUrl(spec, serviceId, serviceVersion)}
106
- className="inline-flex items-center gap-1.5 px-2 py-1.5 bg-[rgb(var(--ec-card-bg,var(--ec-page-bg)))] border border-[rgb(var(--ec-page-border))] rounded-lg text-xs 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"
107
- >
108
- <img src={buildUrl(`/icons/${getSpecIcon(spec.type)}.svg`, true)} alt={`${spec.type} icon`} className="h-3.5 w-3.5" />
109
- <span>{getSpecLabel(spec.type)}</span>
110
- </a>
111
- );
112
- }
113
- );
114
-
115
- const ContainerLink = memo(({ container, type }: { container: any; type: 'reads' | 'writes' }) => {
116
- const data = container?.data || container;
117
- const id = data?.id || container?.id;
118
- const name = data?.name || id;
119
- const version = data?.version || 'latest';
120
-
121
- return (
122
- <a
123
- href={buildUrl(`/docs/containers/${id}/${version}`)}
124
- className="flex items-center gap-2 py-1.5 text-sm text-[rgb(var(--ec-page-text-muted))] hover:text-[rgb(var(--ec-page-text))] transition-colors group"
125
- >
126
- <CircleStackIcon className={`h-4 w-4 ${type === 'reads' ? 'text-amber-500' : 'text-violet-500'}`} />
127
- <span className="group-hover:underline">{name}</span>
128
- </a>
129
- );
130
- });
131
-
132
- // Standalone data store (container) link, used to list a system's data stores
133
- const DataStoreLink = memo(({ container }: { container: any }) => {
134
- const data = container?.data || container;
135
- const id = data?.id || container?.id;
136
- const name = data?.name || id;
137
- const version = data?.version || 'latest';
138
- const technology = data?.technology;
139
-
140
- return (
141
- <a
142
- href={buildUrl(`/docs/containers/${id}/${version}`)}
143
- 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"
144
- >
145
- <CircleStackIcon className="h-4 w-4 text-amber-500" />
146
- <span>{name}</span>
147
- {technology && <span className="text-xs text-[rgb(var(--ec-icon-color))]">{technology}</span>}
148
- </a>
149
- );
150
- });
151
-
152
- // Searchable scrollable box component
153
- const SearchableBox = memo(
154
- ({
155
- title,
156
- icon: Icon,
157
- iconColor,
158
- items,
159
- renderItem,
160
- emptyText = '—',
161
- }: {
162
- title: string;
163
- icon: any;
164
- iconColor: string;
165
- items: any[];
166
- renderItem: (item: any, idx: number) => React.ReactNode;
167
- emptyText?: string;
168
- }) => {
169
- const [search, setSearch] = useState('');
170
-
171
- const filteredItems = items.filter((item) => {
172
- if (!search) return true;
173
- const data = item?.data || item;
174
- const name = data?.name || data?.id || '';
175
- return name.toLowerCase().includes(search.toLowerCase());
176
- });
177
-
178
- return (
179
- <div className="flex flex-col">
180
- <div className="flex items-center gap-2 mb-2">
181
- <div className="flex items-center gap-1.5 flex-shrink-0">
182
- <Icon className={`h-4 w-4 ${iconColor}`} />
183
- <h4 className="text-xs font-semibold text-[rgb(var(--ec-page-text))] uppercase tracking-wide">{title}</h4>
184
- <span className="text-[10px] text-[rgb(var(--ec-icon-color))] font-medium">({items.length})</span>
185
- </div>
186
- {items.length > 0 && (
187
- <input
188
- type="text"
189
- placeholder="Search..."
190
- value={search}
191
- onChange={(e) => setSearch(e.target.value)}
192
- className="flex-1 px-2 py-0.5 text-xs border border-[rgb(var(--ec-input-border))] rounded focus:outline-hidden focus:border-[rgb(var(--ec-accent))] bg-[rgb(var(--ec-input-bg))] text-[rgb(var(--ec-input-text))]"
193
- onClick={(e) => e.stopPropagation()}
194
- />
195
- )}
196
- </div>
197
- {items.length > 0 ? (
198
- <div className="space-y-0.5 max-h-32 overflow-y-auto pr-1">
199
- {filteredItems.length > 0 ? (
200
- filteredItems.map((item, idx) => renderItem(item, idx))
201
- ) : (
202
- <p className="text-xs text-[rgb(var(--ec-icon-color))] italic">No matches</p>
203
- )}
204
- </div>
205
- ) : (
206
- <p className="text-xs text-[rgb(var(--ec-icon-color))]">{emptyText}</p>
207
- )}
208
- </div>
209
- );
210
- }
211
- );
212
-
213
- // Expanded content for service card
214
- const ServiceExpandedContent = memo(
215
- ({ receives, sends, readsFrom, writesTo }: { receives: any[]; sends: any[]; readsFrom: any[]; writesTo: any[] }) => {
216
- const hasMessages = receives.length > 0 || sends.length > 0;
217
- const hasContainers = readsFrom.length > 0 || writesTo.length > 0;
218
-
219
- return (
220
- <div className="border-t border-[rgb(var(--ec-page-border))] px-4 py-3 space-y-4">
221
- {/* Messages Row */}
222
- {hasMessages && (
223
- <div className="grid grid-cols-2 gap-x-6">
224
- <SearchableBox
225
- title="Receives"
226
- icon={ArrowLongRightIcon}
227
- iconColor="text-blue-400"
228
- items={receives}
229
- renderItem={(msg, idx) => {
230
- const msgId = msg?.data?.id || msg?.id;
231
- return msgId ? <MessageLink key={`${msgId}-${idx}`} message={msg} /> : null;
232
- }}
233
- />
234
- <SearchableBox
235
- title="Sends"
236
- icon={ArrowLongLeftIcon}
237
- iconColor="text-emerald-400 rotate-180"
238
- items={sends}
239
- renderItem={(msg, idx) => {
240
- const msgId = msg?.data?.id || msg?.id;
241
- return msgId ? <MessageLink key={`${msgId}-${idx}`} message={msg} /> : null;
242
- }}
243
- />
244
- </div>
245
- )}
246
-
247
- {/* Data Row */}
248
- {hasContainers && (
249
- <div className="grid grid-cols-2 gap-x-6 pt-3 border-t border-[rgb(var(--ec-page-border))]">
250
- <SearchableBox
251
- title="Reads"
252
- icon={CircleStackIcon}
253
- iconColor="text-amber-400"
254
- items={readsFrom}
255
- renderItem={(container, idx) => {
256
- const containerId = container?.data?.id || container?.id;
257
- return containerId ? <ContainerLink key={`${containerId}-${idx}`} container={container} type="reads" /> : null;
258
- }}
259
- />
260
- <SearchableBox
261
- title="Writes"
262
- icon={CircleStackIcon}
263
- iconColor="text-violet-400"
264
- items={writesTo}
265
- renderItem={(container, idx) => {
266
- const containerId = container?.data?.id || container?.id;
267
- return containerId ? <ContainerLink key={`${containerId}-${idx}`} container={container} type="writes" /> : null;
268
- }}
269
- />
270
- </div>
271
- )}
272
- </div>
273
- );
274
- }
275
- );
276
-
277
- export const ServiceCard = memo(({ service, defaultOpen = false }: { service: any; defaultOpen?: boolean }) => {
278
- const data = service?.data || service;
279
- const [isCollapsed, setIsCollapsed] = useState(!defaultOpen);
280
-
281
- if (!data?.id) return null;
282
-
283
- const receives = data.receives || [];
284
- const sends = data.sends || [];
285
- const readsFrom = data.readsFrom || [];
286
- const writesTo = data.writesTo || [];
287
- const specifications = getServiceSpecifications(data);
288
- const hasMessages = receives.length > 0 || sends.length > 0;
289
- const hasContainers = readsFrom.length > 0 || writesTo.length > 0;
290
- const hasSpecs = specifications.length > 0;
291
- const hasContent = hasMessages || hasContainers;
292
-
293
- return (
294
- <div className="bg-[rgb(var(--ec-card-bg,var(--ec-page-bg)))] border border-[rgb(var(--ec-page-border))] rounded-xl shadow-xs hover:shadow-md transition-shadow">
295
- {/* Service Header */}
296
- <div
297
- onClick={() => hasContent && setIsCollapsed(!isCollapsed)}
298
- className={`flex items-center justify-between px-4 py-3 ${hasContent ? 'cursor-pointer hover:bg-[rgb(var(--ec-content-hover))]' : ''} transition-colors`}
299
- >
300
- <div className="flex items-center gap-2.5">
301
- <div className="flex items-center justify-center w-8 h-8 bg-pink-100 dark:bg-pink-500/20 rounded-lg">
302
- <ServerIcon className="h-4 w-4 text-pink-600 dark:text-pink-400" />
303
- </div>
304
- <div>
305
- <div className="flex items-center gap-2">
306
- <span className="font-semibold text-[rgb(var(--ec-page-text))]">{data.name || data.id}</span>
307
- <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">
308
- v{data.version}
309
- </span>
310
- </div>
311
- {data.summary && (
312
- <p className="text-xs text-[rgb(var(--ec-page-text-muted))] line-clamp-1 mt-0.5 max-w-md">{data.summary}</p>
313
- )}
314
- </div>
315
- </div>
316
- <div className="flex items-center gap-2">
317
- {/* Specs in header - always visible */}
318
- {hasSpecs && (
319
- <div className="flex items-center gap-1.5">
320
- {specifications.map((spec: any, idx: number) => (
321
- <a
322
- key={`${spec.type}-${idx}`}
323
- href={getSpecUrl(spec, data.id, data.version)}
324
- onClick={(e) => e.stopPropagation()}
325
- className="flex items-center gap-1 px-2 py-1 bg-[rgb(var(--ec-content-hover))] hover:bg-[rgb(var(--ec-sidebar-hover-bg))] rounded text-xs text-[rgb(var(--ec-page-text-muted))] hover:text-[rgb(var(--ec-page-text))] transition-colors"
326
- title={getSpecLabel(spec.type)}
327
- >
328
- <img src={buildUrl(`/icons/${getSpecIcon(spec.type)}.svg`, true)} alt="" className="h-3.5 w-3.5" />
329
- <span className="hidden sm:inline">{getSpecLabel(spec.type)}</span>
330
- </a>
331
- ))}
332
- </div>
333
- )}
334
- {hasContent && (
335
- <div className="p-1.5 text-[rgb(var(--ec-icon-color))]">
336
- {isCollapsed ? <ChevronDownIcon className="h-4 w-4" /> : <ChevronUpIcon className="h-4 w-4" />}
337
- </div>
338
- )}
339
- <a
340
- href={buildUrl(`/architecture/services/${data.id}/${data.version}`)}
341
- onClick={(e) => e.stopPropagation()}
342
- className="p-1.5 text-[rgb(var(--ec-icon-color))] hover:text-pink-500 hover:bg-pink-500/10 rounded-lg transition-colors"
343
- title="View service architecture"
344
- >
345
- <ArrowTopRightOnSquareIcon className="h-4 w-4" />
346
- </a>
347
- </div>
348
- </div>
349
-
350
- {/* Expanded Content - Compact Flow */}
351
- {!isCollapsed && hasContent && (
352
- <ServiceExpandedContent receives={receives} sends={sends} readsFrom={readsFrom} writesTo={writesTo} />
353
- )}
354
- </div>
355
- );
356
- });
357
-
358
- const DataProductCard = memo(({ dataProduct }: { dataProduct: any }) => {
359
- const data = dataProduct?.data || dataProduct;
360
-
361
- if (!data?.id) return null;
362
-
363
- const href = buildUrl(`/docs/data-products/${data.id}/${data.version}`);
364
-
365
- return (
366
- <a
367
- href={href}
368
- className="block bg-[rgb(var(--ec-card-bg,var(--ec-page-bg)))] border border-[rgb(var(--ec-page-border))] rounded-xl shadow-xs hover:shadow-md hover:border-[rgb(var(--ec-accent)/0.5)] transition-all"
369
- >
370
- <div className="flex items-center justify-between px-4 py-3">
371
- <div className="flex items-center gap-2.5">
372
- <div className="flex items-center justify-center w-8 h-8 bg-purple-100 dark:bg-purple-500/20 rounded-lg">
373
- <CubeIcon className="h-4 w-4 text-purple-600 dark:text-purple-400" />
374
- </div>
375
- <div>
376
- <div className="flex items-center gap-2">
377
- <span className="font-semibold text-[rgb(var(--ec-page-text))]">{data.name || data.id}</span>
378
- <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">
379
- v{data.version}
380
- </span>
381
- </div>
382
- {data.summary && (
383
- <p className="text-xs text-[rgb(var(--ec-page-text-muted))] line-clamp-1 mt-0.5 max-w-md">{data.summary}</p>
384
- )}
385
- </div>
386
- </div>
387
- <div className="p-1.5 text-[rgb(var(--ec-icon-color))]">
388
- <ArrowTopRightOnSquareIcon className="h-4 w-4" />
389
- </div>
390
- </div>
391
- </a>
392
- );
393
- });
394
-
395
- const SubdomainSection = memo(({ subdomain, defaultOpen = false }: { subdomain: any; defaultOpen?: boolean }) => {
396
- const data = subdomain?.data || subdomain;
397
- const [isCollapsed, setIsCollapsed] = useState(!defaultOpen);
398
-
399
- if (!data?.id) return null;
400
-
401
- const services = data.services || [];
402
- const entities = data.entities || [];
403
- const dataProducts = data['data-products'] || [];
404
-
405
- return (
406
- <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">
407
- {/* Subdomain Header - Clickable */}
408
- <div
409
- onClick={() => setIsCollapsed(!isCollapsed)}
410
- 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))]' : ''}`}
411
- >
412
- <div className="flex items-center gap-3">
413
- <div className="flex items-center justify-center w-9 h-9 bg-orange-100 dark:bg-orange-500/20 rounded-lg">
414
- <RectangleGroupIcon className="h-5 w-5 text-orange-600 dark:text-orange-400" />
415
- </div>
416
- <div>
417
- <div className="flex items-center gap-2">
418
- <h3 className="text-base font-semibold text-[rgb(var(--ec-page-text))]">{data.name || data.id}</h3>
419
- <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))]">
420
- v{data.version}
421
- </span>
422
- {/* Show counts when collapsed */}
423
- {isCollapsed && (services.length > 0 || entities.length > 0 || dataProducts.length > 0) && (
424
- <span className="text-[11px] text-[rgb(var(--ec-icon-color))] ml-1">
425
- {services.length > 0 && `${services.length} service${services.length > 1 ? 's' : ''}`}
426
- {services.length > 0 && (entities.length > 0 || dataProducts.length > 0) && ', '}
427
- {entities.length > 0 && `${entities.length} entit${entities.length > 1 ? 'ies' : 'y'}`}
428
- {entities.length > 0 && dataProducts.length > 0 && ', '}
429
- {dataProducts.length > 0 && `${dataProducts.length} data product${dataProducts.length > 1 ? 's' : ''}`}
430
- </span>
431
- )}
432
- </div>
433
- <span className="text-[11px] text-[rgb(var(--ec-page-text-muted))] font-medium">Subdomain</span>
434
- </div>
435
- </div>
436
- <div className="flex items-center gap-1">
437
- <div className="p-2 text-[rgb(var(--ec-icon-color))]">
438
- {isCollapsed ? <ChevronDownIcon className="h-5 w-5" /> : <ChevronUpIcon className="h-5 w-5" />}
439
- </div>
440
- <a
441
- href={buildUrl(`/architecture/domains/${data.id}/${data.version}`)}
442
- onClick={(e) => e.stopPropagation()}
443
- className="p-2 text-[rgb(var(--ec-icon-color))] hover:text-orange-500 hover:bg-orange-500/10 rounded-lg transition-colors"
444
- title="View subdomain architecture"
445
- >
446
- <ArrowTopRightOnSquareIcon className="h-5 w-5" />
447
- </a>
448
- </div>
449
- </div>
450
-
451
- {!isCollapsed && (
452
- <div className="p-5 space-y-5">
453
- {/* Subdomain Entities */}
454
- {entities.length > 0 && (
455
- <div>
456
- <div className="flex items-center gap-2 mb-3">
457
- <BoxIcon className="h-4 w-4 text-[rgb(var(--ec-accent))]" />
458
- <h4 className="text-sm font-semibold text-[rgb(var(--ec-page-text))]">Entities</h4>
459
- <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">
460
- {entities.length}
461
- </span>
462
- </div>
463
- <div className="flex flex-wrap gap-2">
464
- {entities.map((entity: any) => {
465
- const entityId = entity?.data?.id || entity?.id;
466
- return entityId ? <EntityBadge key={entityId} entity={entity} /> : null;
467
- })}
468
- </div>
469
- </div>
470
- )}
471
-
472
- {/* Subdomain Services */}
473
- {services.length > 0 && (
474
- <div>
475
- <div className="flex items-center gap-2 mb-3">
476
- <ServerIcon className="h-4 w-4 text-pink-600 dark:text-pink-400" />
477
- <h4 className="text-sm font-semibold text-[rgb(var(--ec-page-text))]">Services</h4>
478
- <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">
479
- {services.length}
480
- </span>
481
- </div>
482
- <div className="space-y-3">
483
- {services.map((service: any) => {
484
- const serviceId = service?.data?.id || service?.id;
485
- return serviceId ? <ServiceCard key={serviceId} service={service} /> : null;
486
- })}
487
- </div>
488
- </div>
489
- )}
490
-
491
- {/* Subdomain Data Products */}
492
- {dataProducts.length > 0 && (
493
- <div>
494
- <div className="flex items-center gap-2 mb-3">
495
- <CubeIcon className="h-4 w-4 text-purple-600 dark:text-purple-400" />
496
- <h4 className="text-sm font-semibold text-[rgb(var(--ec-page-text))]">Data Products</h4>
497
- <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">
498
- {dataProducts.length}
499
- </span>
500
- </div>
501
- <div className="space-y-3">
502
- {dataProducts.map((dataProduct: any) => {
503
- const dataProductId = dataProduct?.data?.id || dataProduct?.id;
504
- return dataProductId ? <DataProductCard key={dataProductId} dataProduct={dataProduct} /> : null;
505
- })}
506
- </div>
507
- </div>
508
- )}
509
-
510
- {entities.length === 0 && services.length === 0 && dataProducts.length === 0 && (
511
- <p className="text-sm text-[rgb(var(--ec-icon-color))] italic text-center py-4">
512
- No entities, services, or data products in this subdomain
513
- </p>
514
- )}
515
- </div>
516
- )}
517
- </div>
518
- );
519
- });
520
-
521
- const SystemSection = memo(({ system, defaultOpen = false }: { system: any; defaultOpen?: boolean }) => {
522
- const data = system?.data || system;
523
- const [isCollapsed, setIsCollapsed] = useState(!defaultOpen);
524
-
525
- if (!data?.id) return null;
526
-
527
- const services = data.services || [];
528
- const containers = data.containers || [];
529
-
530
- return (
531
- <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">
532
- {/* System Header - Clickable */}
533
- <div
534
- onClick={() => setIsCollapsed(!isCollapsed)}
535
- 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))]' : ''}`}
536
- >
537
- <div className="flex items-center gap-3">
538
- <div className="flex items-center justify-center w-9 h-9 bg-violet-100 dark:bg-violet-500/20 rounded-lg">
539
- <GroupIcon className="h-5 w-5 text-violet-600 dark:text-violet-400" />
540
- </div>
541
- <div>
542
- <div className="flex items-center gap-2">
543
- <h3 className="text-base font-semibold text-[rgb(var(--ec-page-text))]">{data.name || data.id}</h3>
544
- <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))]">
545
- v{data.version}
546
- </span>
547
- {/* Show counts when collapsed */}
548
- {isCollapsed && (services.length > 0 || containers.length > 0) && (
549
- <span className="text-[11px] text-[rgb(var(--ec-icon-color))] ml-1">
550
- {[
551
- services.length > 0 && `${services.length} service${services.length > 1 ? 's' : ''}`,
552
- containers.length > 0 && `${containers.length} data store${containers.length > 1 ? 's' : ''}`,
553
- ]
554
- .filter(Boolean)
555
- .join(' · ')}
556
- </span>
557
- )}
558
- </div>
559
- <span className="text-[11px] text-[rgb(var(--ec-page-text-muted))] font-medium">System</span>
560
- </div>
561
- </div>
562
- <div className="flex items-center gap-1">
563
- <div className="p-2 text-[rgb(var(--ec-icon-color))]">
564
- {isCollapsed ? <ChevronDownIcon className="h-5 w-5" /> : <ChevronUpIcon className="h-5 w-5" />}
565
- </div>
566
- <a
567
- href={buildUrl(`/visualiser/systems/${data.id}/${data.version}`)}
568
- onClick={(e) => e.stopPropagation()}
569
- className="p-2 text-[rgb(var(--ec-icon-color))] hover:text-violet-500 hover:bg-violet-500/10 rounded-lg transition-colors"
570
- title="View system map"
571
- >
572
- <ArrowTopRightOnSquareIcon className="h-5 w-5" />
573
- </a>
574
- </div>
575
- </div>
576
-
577
- {!isCollapsed && (
578
- <div className="p-5 space-y-5">
579
- {services.length > 0 && (
580
- <div>
581
- <div className="flex items-center gap-2 mb-3">
582
- <ServerIcon className="h-4 w-4 text-pink-600 dark:text-pink-400" />
583
- <h4 className="text-sm font-semibold text-[rgb(var(--ec-page-text))]">Services</h4>
584
- <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">
585
- {services.length}
586
- </span>
587
- </div>
588
- <div className="space-y-3">
589
- {services.map((service: any) => {
590
- const serviceId = service?.data?.id || service?.id;
591
- return serviceId ? <ServiceCard key={serviceId} service={service} /> : null;
592
- })}
593
- </div>
594
- </div>
595
- )}
596
-
597
- {containers.length > 0 && (
598
- <div>
599
- <div className="flex items-center gap-2 mb-3">
600
- <CircleStackIcon className="h-4 w-4 text-amber-500" />
601
- <h4 className="text-sm font-semibold text-[rgb(var(--ec-page-text))]">Data Stores</h4>
602
- <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">
603
- {containers.length}
604
- </span>
605
- </div>
606
- <div className="flex flex-wrap gap-2">
607
- {containers.map((container: any) => {
608
- const containerId = container?.data?.id || container?.id;
609
- return containerId ? <DataStoreLink key={containerId} container={container} /> : null;
610
- })}
611
- </div>
612
- </div>
613
- )}
614
-
615
- {services.length === 0 && containers.length === 0 && (
616
- <p className="text-sm text-[rgb(var(--ec-icon-color))] italic text-center py-4">Nothing in this system yet</p>
617
- )}
618
- </div>
619
- )}
620
- </div>
621
- );
622
- });
623
-
624
- // ============================================
625
- // Main Component
626
- // ============================================
627
-
628
- export default function DomainGrid({ domain }: DomainGridProps) {
629
- const data = domain?.data;
630
- if (!data) return <div className="text-[rgb(var(--ec-page-text-muted))]">No domain data</div>;
631
-
632
- const subdomains = data.domains || [];
633
- const systems = data.systems || [];
634
- const entities = data.entities || [];
635
- const services = data.services || [];
636
- const dataProducts = data['data-products'] || [];
637
-
638
- // Get services that are NOT in any subdomain
639
- const subdomainServiceIds = useMemo(
640
- () =>
641
- new Set(
642
- subdomains.flatMap((sd: any) => {
643
- const sdData = sd?.data || sd;
644
- return (sdData?.services || []).map((s: any) => s?.data?.id || s?.id);
645
- })
646
- ),
647
- [subdomains]
648
- );
649
-
650
- // Get data products that are NOT in any subdomain
651
- const subdomainDataProductIds = useMemo(
652
- () =>
653
- new Set(
654
- subdomains.flatMap((sd: any) => {
655
- const sdData = sd?.data || sd;
656
- return (sdData?.['data-products'] || []).map((dp: any) => dp?.data?.id || dp?.id);
657
- })
658
- ),
659
- [subdomains]
660
- );
661
-
662
- const topLevelServices = useMemo(
663
- () =>
664
- services.filter((s: any) => {
665
- const sId = s?.data?.id || s?.id;
666
- return sId && !subdomainServiceIds.has(sId);
667
- }),
668
- [services, subdomainServiceIds]
669
- );
670
-
671
- // Split top-level services into internal services and external integrations
672
- // (mirrors the sidebar, where externalSystem services live under "External Integrations")
673
- const topLevelInternalServices = useMemo(
674
- () => topLevelServices.filter((s: any) => !(s?.data || s)?.externalSystem),
675
- [topLevelServices]
676
- );
677
-
678
- const topLevelExternalServices = useMemo(
679
- () => topLevelServices.filter((s: any) => (s?.data || s)?.externalSystem),
680
- [topLevelServices]
681
- );
682
-
683
- const topLevelDataProducts = useMemo(
684
- () =>
685
- dataProducts.filter((dp: any) => {
686
- const dpId = dp?.data?.id || dp?.id;
687
- return dpId && !subdomainDataProductIds.has(dpId);
688
- }),
689
- [dataProducts, subdomainDataProductIds]
690
- );
691
-
692
- return (
693
- <div className="w-full">
694
- {/* Domain Header - Doc style */}
695
- <div className="border-b border-[rgb(var(--ec-page-border))] md:pb-4">
696
- <div className="flex items-start justify-between">
697
- <div>
698
- <h2 className="text-2xl md:text-4xl font-bold text-[rgb(var(--ec-page-text))]">{data.name || data.id}</h2>
699
- {data.summary && <p className="text-lg pt-2 text-[rgb(var(--ec-page-text-muted))] font-light">{data.summary}</p>}
700
- </div>
701
- </div>
702
- </div>
703
-
704
- {/* Domain Content */}
705
- <div className="py-4 space-y-8">
706
- {/* Domain Entities */}
707
- {entities.length > 0 && (
708
- <div>
709
- <div className="flex items-center gap-2 mb-4">
710
- <BoxIcon className="h-5 w-5 text-[rgb(var(--ec-accent))]" />
711
- <h3 className="text-lg font-semibold text-[rgb(var(--ec-page-text))]">Entities</h3>
712
- <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">
713
- {entities.length}
714
- </span>
715
- </div>
716
- <div className="flex flex-wrap gap-2">
717
- {entities.map((entity: any) => {
718
- const entityId = entity?.data?.id || entity?.id;
719
- return entityId ? <EntityBadge key={entityId} entity={entity} /> : null;
720
- })}
721
- </div>
722
- </div>
723
- )}
724
-
725
- {/* Subdomains */}
726
- {subdomains.length > 0 && (
727
- <div>
728
- <div className="flex items-center gap-2 mb-4">
729
- <RectangleGroupIcon className="h-5 w-5 text-orange-600 dark:text-orange-400" />
730
- <h3 className="text-lg font-semibold text-[rgb(var(--ec-page-text))]">Subdomains</h3>
731
- <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">
732
- {subdomains.length}
733
- </span>
734
- </div>
735
- <div className="space-y-4">
736
- {subdomains.map((subdomain: any) => {
737
- const subdomainId = subdomain?.data?.id || subdomain?.id;
738
- return subdomainId ? <SubdomainSection key={subdomainId} subdomain={subdomain} defaultOpen /> : null;
739
- })}
740
- </div>
741
- </div>
742
- )}
743
-
744
- {/* Systems */}
745
- {systems.length > 0 && (
746
- <div>
747
- <div className="flex items-center gap-2 mb-4">
748
- <GroupIcon className="h-5 w-5 text-violet-600 dark:text-violet-400" />
749
- <h3 className="text-lg font-semibold text-[rgb(var(--ec-page-text))]">Systems</h3>
750
- <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">
751
- {systems.length}
752
- </span>
753
- </div>
754
- <div className="space-y-4">
755
- {systems.map((system: any) => {
756
- const systemId = system?.data?.id || system?.id;
757
- return systemId ? <SystemSection key={systemId} system={system} defaultOpen /> : null;
758
- })}
759
- </div>
760
- </div>
761
- )}
762
-
763
- {/* Top-level Services */}
764
- {topLevelInternalServices.length > 0 && (
765
- <div>
766
- <div className="flex items-center gap-2 mb-4">
767
- <ServerIcon className="h-5 w-5 text-pink-600 dark:text-pink-400" />
768
- <h3 className="text-lg font-semibold text-[rgb(var(--ec-page-text))]">Services</h3>
769
- <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">
770
- {topLevelInternalServices.length}
771
- </span>
772
- </div>
773
- <div className="space-y-3">
774
- {topLevelInternalServices.map((service: any) => {
775
- const serviceId = service?.data?.id || service?.id;
776
- return serviceId ? <ServiceCard key={serviceId} service={service} defaultOpen /> : null;
777
- })}
778
- </div>
779
- </div>
780
- )}
781
-
782
- {/* External Integrations (external systems) */}
783
- {topLevelExternalServices.length > 0 && (
784
- <div>
785
- <div className="flex items-center gap-2 mb-4">
786
- <GlobeAltIcon className="h-5 w-5 text-sky-600 dark:text-sky-400" />
787
- <h3 className="text-lg font-semibold text-[rgb(var(--ec-page-text))]">External Integrations</h3>
788
- <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">
789
- {topLevelExternalServices.length}
790
- </span>
791
- </div>
792
- <div className="space-y-3">
793
- {topLevelExternalServices.map((service: any) => {
794
- const serviceId = service?.data?.id || service?.id;
795
- return serviceId ? <ServiceCard key={serviceId} service={service} defaultOpen /> : null;
796
- })}
797
- </div>
798
- </div>
799
- )}
800
-
801
- {/* Top-level Data Products */}
802
- {topLevelDataProducts.length > 0 && (
803
- <div>
804
- <div className="flex items-center gap-2 mb-4">
805
- <CubeIcon className="h-5 w-5 text-purple-600 dark:text-purple-400" />
806
- <h3 className="text-lg font-semibold text-[rgb(var(--ec-page-text))]">Data Products</h3>
807
- <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">
808
- {topLevelDataProducts.length}
809
- </span>
810
- </div>
811
- <div className="space-y-3">
812
- {topLevelDataProducts.map((dataProduct: any) => {
813
- const dataProductId = dataProduct?.data?.id || dataProduct?.id;
814
- return dataProductId ? <DataProductCard key={dataProductId} dataProduct={dataProduct} /> : null;
815
- })}
816
- </div>
817
- </div>
818
- )}
819
-
820
- {/* Empty state */}
821
- {entities.length === 0 &&
822
- services.length === 0 &&
823
- dataProducts.length === 0 &&
824
- subdomains.length === 0 &&
825
- systems.length === 0 && (
826
- <div className="text-center py-12">
827
- <div className="flex items-center justify-center w-16 h-16 mx-auto mb-4 bg-[rgb(var(--ec-content-hover))] rounded-2xl">
828
- <RectangleGroupIcon className="h-8 w-8 text-[rgb(var(--ec-icon-color))]" />
829
- </div>
830
- <p className="text-[rgb(var(--ec-page-text-muted))]">
831
- This domain has no entities, services, data products, subdomains, or systems defined.
832
- </p>
833
- </div>
834
- )}
835
- </div>
836
- </div>
837
- );
838
- }