@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
@@ -30,6 +30,7 @@ export type CollectionType =
30
30
  | 'services'
31
31
  | 'external-systems'
32
32
  | 'domains'
33
+ | 'systems'
33
34
  | 'flows'
34
35
  | 'containers'
35
36
  | 'data-products';
@@ -49,6 +50,9 @@ export interface DiscoverTableData {
49
50
  hasDecisionMakers?: boolean;
50
51
  hasInputs?: boolean;
51
52
  hasOutputs?: boolean;
53
+ hasServices?: boolean;
54
+ hasFlows?: boolean;
55
+ isSubdomain?: boolean;
52
56
  data: {
53
57
  id: string;
54
58
  name: string;
@@ -79,6 +83,7 @@ export interface DiscoverTableData {
79
83
  model?: any;
80
84
  agents?: Array<any>;
81
85
  services?: Array<any>;
86
+ flows?: Array<any>;
82
87
  servicesThatWriteToContainer?: Array<any>;
83
88
  servicesThatReadFromContainer?: Array<any>;
84
89
  inputs?: Array<any>;
@@ -91,11 +96,19 @@ export interface PropertyOption {
91
96
  label: string;
92
97
  }
93
98
 
99
+ export interface CollectionKindOption {
100
+ id: CollectionType;
101
+ label: string;
102
+ href: string;
103
+ count: number;
104
+ }
105
+
94
106
  export interface DiscoverTableProps<T extends DiscoverTableData> {
95
107
  data: T[];
96
108
  collectionType: CollectionType;
97
109
  collectionLabel: string;
98
110
  collectionDescription?: string;
111
+ collectionKinds?: CollectionKindOption[];
99
112
  domains?: Array<{ id: string; name: string; version: string }>;
100
113
  owners?: Array<{ id: string; name: string; type?: 'user' | 'team' }>;
101
114
  producers?: Array<{ id: string; name: string }>;
@@ -153,6 +166,7 @@ export function DiscoverTable<T extends DiscoverTableData>({
153
166
  collectionType,
154
167
  collectionLabel,
155
168
  collectionDescription,
169
+ collectionKinds = [],
156
170
  domains = [],
157
171
  owners = [],
158
172
  producers = [],
@@ -197,6 +211,7 @@ export function DiscoverTable<T extends DiscoverTableData>({
197
211
  const [selectedBadges, setSelectedBadges] = useState<string[]>([]);
198
212
  const [selectedProperties, setSelectedProperties] = useState<string[]>([]);
199
213
  const [selectedStatuses, setSelectedStatuses] = useState<string[]>([]);
214
+ const selectedKind = collectionKinds.find((kind) => kind.id === collectionType);
200
215
 
201
216
  // Collect unique badges from all items
202
217
  const allBadges = useMemo(() => {
@@ -341,6 +356,10 @@ export function DiscoverTable<T extends DiscoverTableData>({
341
356
  const services = row.data.services || [];
342
357
  if (services.length === 0) return false;
343
358
  }
359
+ if (prop === 'hasFlows') {
360
+ const flows = row.data.flows || [];
361
+ if (flows.length === 0) return false;
362
+ }
344
363
 
345
364
  // Container-specific checks
346
365
  if (prop === 'hasWriters') {
@@ -554,6 +573,12 @@ export function DiscoverTable<T extends DiscoverTableData>({
554
573
  setSelectedProperties((prev) => (prev.includes(propertyId) ? prev.filter((p) => p !== propertyId) : [...prev, propertyId]));
555
574
  };
556
575
 
576
+ const navigateToKind = (kindId: CollectionType) => {
577
+ const selectedKind = collectionKinds.find((kind) => kind.id === kindId);
578
+ if (!selectedKind || selectedKind.id === collectionType) return;
579
+ window.location.href = selectedKind.href;
580
+ };
581
+
557
582
  const clearAllFilters = () => {
558
583
  setSelectedDomains([]);
559
584
  setSelectedOwners([]);
@@ -629,6 +654,28 @@ export function DiscoverTable<T extends DiscoverTableData>({
629
654
 
630
655
  {/* Filter sections */}
631
656
  <div className="flex-1 overflow-y-auto px-4 pt-4 pb-4 space-y-6">
657
+ {collectionKinds.length > 1 && (
658
+ <div>
659
+ <label className="block text-xs font-medium text-[rgb(var(--ec-page-text)/0.8)] mb-1.5">Kind</label>
660
+ <FilterDropdown
661
+ label="Select kind..."
662
+ selectedItems={selectedKind ? [selectedKind.label] : []}
663
+ onClear={() => {}}
664
+ clearable={false}
665
+ >
666
+ {collectionKinds.map((kind) => (
667
+ <CheckboxItem
668
+ key={kind.id}
669
+ label={kind.label}
670
+ checked={kind.id === collectionType}
671
+ onChange={() => navigateToKind(kind.id)}
672
+ count={kind.count}
673
+ />
674
+ ))}
675
+ </FilterDropdown>
676
+ </div>
677
+ )}
678
+
632
679
  {/* Message Filters Section */}
633
680
  {(showProducersFilter || showConsumersFilter) && (filteredProducers.length > 0 || filteredConsumers.length > 0) && (
634
681
  <div className="space-y-3">
@@ -8,10 +8,18 @@ export interface FilterDropdownProps {
8
8
  selectedItems: string[];
9
9
  onClear: () => void;
10
10
  onRemoveItem?: (item: string) => void;
11
+ clearable?: boolean;
11
12
  children: React.ReactNode;
12
13
  }
13
14
 
14
- export const FilterDropdown = ({ label, selectedItems, onClear, onRemoveItem, children }: FilterDropdownProps) => {
15
+ export const FilterDropdown = ({
16
+ label,
17
+ selectedItems,
18
+ onClear,
19
+ onRemoveItem,
20
+ clearable = true,
21
+ children,
22
+ }: FilterDropdownProps) => {
15
23
  const [isOpen, setIsOpen] = useState(false);
16
24
  const dropdownRef = useRef<HTMLDivElement>(null);
17
25
  const hasSelection = selectedItems.length > 0;
@@ -88,7 +96,7 @@ export const FilterDropdown = ({ label, selectedItems, onClear, onRemoveItem, ch
88
96
  )}
89
97
  </div>
90
98
  <div className="flex items-center gap-1 flex-shrink-0 ml-2">
91
- {hasSelection && (
99
+ {hasSelection && clearable && (
92
100
  <span
93
101
  role="button"
94
102
  tabIndex={0}
@@ -1,5 +1,6 @@
1
1
  import { createColumnHelper } from '@tanstack/react-table';
2
- import { useEffect, useMemo, useRef, useState } from 'react';
2
+ import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
3
+ import { createPortal } from 'react-dom';
3
4
  import { DocumentTextIcon, MapIcon } from '@heroicons/react/24/solid';
4
5
  import {
5
6
  ArrowDownIcon,
@@ -139,9 +140,14 @@ const ResourceNameCell = ({ item }: { item: DiscoverTableData }) => {
139
140
  );
140
141
  };
141
142
 
143
+ const MENU_WIDTH = 280;
144
+ const MENU_GAP = 6;
145
+
142
146
  const RowActionsMenu = ({ item, collectionType }: { item: DiscoverTableData; collectionType: CollectionType }) => {
143
147
  const [isOpen, setIsOpen] = useState(false);
148
+ const buttonRef = useRef<HTMLButtonElement>(null);
144
149
  const menuRef = useRef<HTMLDivElement>(null);
150
+ const [menuPosition, setMenuPosition] = useState<{ top: number; left: number } | null>(null);
145
151
  const favorites = useStore(favoritesStore);
146
152
  const href = buildUrl(`/docs/${item.collection}/${item.data.id}/${item.data.version}`);
147
153
  const visualiserHref = buildUrl(`/visualiser/${item.collection}/${item.data.id}/${item.data.version}`);
@@ -153,9 +159,35 @@ const RowActionsMenu = ({ item, collectionType }: { item: DiscoverTableData; col
153
159
  : collectionType.charAt(0).toUpperCase() + collectionType.slice(1, -1);
154
160
  const isFavorite = favorites.some((fav) => fav.nodeKey === nodeKey);
155
161
 
162
+ // The table lives inside scrollable, overflow-hidden containers, so the menu is
163
+ // rendered in a portal with fixed positioning to avoid being clipped.
164
+ const updatePosition = () => {
165
+ const button = buttonRef.current;
166
+ if (!button) return;
167
+ const rect = button.getBoundingClientRect();
168
+ const menuHeight = menuRef.current?.offsetHeight ?? 0;
169
+ const spaceBelow = window.innerHeight - rect.bottom;
170
+
171
+ // Flip above the trigger when there isn't enough room below it.
172
+ const openUpwards = menuHeight > 0 && spaceBelow < menuHeight + MENU_GAP && rect.top > spaceBelow;
173
+ const top = openUpwards ? rect.top - menuHeight - MENU_GAP : rect.bottom + MENU_GAP;
174
+ const left = Math.max(MENU_GAP, rect.right - MENU_WIDTH);
175
+
176
+ setMenuPosition({ top, left });
177
+ };
178
+
179
+ useLayoutEffect(() => {
180
+ if (!isOpen) return;
181
+ updatePosition();
182
+ // eslint-disable-next-line react-hooks/exhaustive-deps
183
+ }, [isOpen]);
184
+
156
185
  useEffect(() => {
186
+ if (!isOpen) return;
187
+
157
188
  const handlePointerDown = (event: MouseEvent) => {
158
- if (!menuRef.current?.contains(event.target as Node)) {
189
+ const target = event.target as Node;
190
+ if (!menuRef.current?.contains(target) && !buttonRef.current?.contains(target)) {
159
191
  setIsOpen(false);
160
192
  }
161
193
  };
@@ -166,14 +198,20 @@ const RowActionsMenu = ({ item, collectionType }: { item: DiscoverTableData; col
166
198
  }
167
199
  };
168
200
 
201
+ const handleReposition = () => updatePosition();
202
+
169
203
  document.addEventListener('mousedown', handlePointerDown);
170
204
  document.addEventListener('keydown', handleEscape);
205
+ window.addEventListener('resize', handleReposition);
206
+ window.addEventListener('scroll', handleReposition, true);
171
207
 
172
208
  return () => {
173
209
  document.removeEventListener('mousedown', handlePointerDown);
174
210
  document.removeEventListener('keydown', handleEscape);
211
+ window.removeEventListener('resize', handleReposition);
212
+ window.removeEventListener('scroll', handleReposition, true);
175
213
  };
176
- }, []);
214
+ }, [isOpen]);
177
215
 
178
216
  const handleToggleFavorite = () => {
179
217
  const favoriteItem: FavoriteItem = {
@@ -188,8 +226,9 @@ const RowActionsMenu = ({ item, collectionType }: { item: DiscoverTableData; col
188
226
  };
189
227
 
190
228
  return (
191
- <div className="relative flex justify-end" ref={menuRef}>
229
+ <div className="relative flex justify-end">
192
230
  <button
231
+ ref={buttonRef}
193
232
  type="button"
194
233
  aria-haspopup="menu"
195
234
  aria-expanded={isOpen}
@@ -199,34 +238,48 @@ const RowActionsMenu = ({ item, collectionType }: { item: DiscoverTableData; col
199
238
  <EllipsisVerticalIcon className="h-4 w-4" />
200
239
  </button>
201
240
 
202
- {isOpen && (
203
- <div className="absolute right-0 top-[calc(100%+0.35rem)] z-30 min-w-[280px] overflow-hidden rounded-xl border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-dropdown-bg))] shadow-xl">
204
- <a
205
- href={href}
206
- className="flex items-center gap-2.5 px-3 py-2.5 text-xs font-medium text-[rgb(var(--ec-page-text))] transition-colors hover:bg-[rgb(var(--ec-content-hover))] hover:text-[rgb(var(--ec-accent))]"
241
+ {isOpen &&
242
+ typeof document !== 'undefined' &&
243
+ createPortal(
244
+ <div
245
+ ref={menuRef}
246
+ role="menu"
247
+ style={{
248
+ position: 'fixed',
249
+ top: menuPosition?.top ?? -9999,
250
+ left: menuPosition?.left ?? -9999,
251
+ width: MENU_WIDTH,
252
+ visibility: menuPosition ? 'visible' : 'hidden',
253
+ }}
254
+ className="z-50 overflow-hidden rounded-xl border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-dropdown-bg))] shadow-xl"
207
255
  >
208
- <DocumentTextIcon className="h-3.5 w-3.5 text-[rgb(var(--ec-page-text-muted))]" />
209
- View documentation
210
- </a>
211
- {hasVisualiser && (
212
256
  <a
213
- href={visualiserHref}
257
+ href={href}
214
258
  className="flex items-center gap-2.5 px-3 py-2.5 text-xs font-medium text-[rgb(var(--ec-page-text))] transition-colors hover:bg-[rgb(var(--ec-content-hover))] hover:text-[rgb(var(--ec-accent))]"
215
259
  >
216
- <MapIcon className="h-3.5 w-3.5 text-[rgb(var(--ec-page-text-muted))]" />
217
- View in visualiser
260
+ <DocumentTextIcon className="h-3.5 w-3.5 text-[rgb(var(--ec-page-text-muted))]" />
261
+ View documentation
218
262
  </a>
219
- )}
220
- <button
221
- type="button"
222
- onClick={handleToggleFavorite}
223
- className="flex w-full items-center gap-2.5 px-3 py-2.5 text-left text-xs font-medium text-[rgb(var(--ec-page-text))] transition-colors hover:bg-[rgb(var(--ec-content-hover))] hover:text-[rgb(var(--ec-accent))]"
224
- >
225
- <StarIcon className="h-3.5 w-3.5 text-[rgb(var(--ec-page-text-muted))]" />
226
- {isFavorite ? 'Remove from favorites' : 'Add to favorites'}
227
- </button>
228
- </div>
229
- )}
263
+ {hasVisualiser && (
264
+ <a
265
+ href={visualiserHref}
266
+ className="flex items-center gap-2.5 px-3 py-2.5 text-xs font-medium text-[rgb(var(--ec-page-text))] transition-colors hover:bg-[rgb(var(--ec-content-hover))] hover:text-[rgb(var(--ec-accent))]"
267
+ >
268
+ <MapIcon className="h-3.5 w-3.5 text-[rgb(var(--ec-page-text-muted))]" />
269
+ View in visualiser
270
+ </a>
271
+ )}
272
+ <button
273
+ type="button"
274
+ onClick={handleToggleFavorite}
275
+ className="flex w-full items-center gap-2.5 px-3 py-2.5 text-left text-xs font-medium text-[rgb(var(--ec-page-text))] transition-colors hover:bg-[rgb(var(--ec-content-hover))] hover:text-[rgb(var(--ec-accent))]"
276
+ >
277
+ <StarIcon className="h-3.5 w-3.5 text-[rgb(var(--ec-page-text-muted))]" />
278
+ {isFavorite ? 'Remove from favorites' : 'Add to favorites'}
279
+ </button>
280
+ </div>,
281
+ document.body
282
+ )}
230
283
  </div>
231
284
  );
232
285
  };
@@ -617,6 +670,39 @@ export const getDomainColumns = (tableConfiguration: TableConfiguration) => [
617
670
  createActionsColumn('domains', tableConfiguration),
618
671
  ];
619
672
 
673
+ // ============================================================================
674
+ // SYSTEM COLUMNS
675
+ // ============================================================================
676
+ export const getSystemColumns = (tableConfiguration: TableConfiguration) => [
677
+ columnHelper.accessor('data.name', {
678
+ id: 'name',
679
+ header: () => <span>{tableConfiguration?.columns?.name?.label || 'System'}</span>,
680
+ cell: (info) => <ResourceNameCell item={info.row.original} />,
681
+ meta: {
682
+ filterVariant: 'name',
683
+ },
684
+ }),
685
+ createSummaryColumn(tableConfiguration),
686
+ columnHelper.accessor('data.services', {
687
+ id: 'services',
688
+ header: () => <span>Services</span>,
689
+ cell: (info) => <CollectionListCell items={info.getValue()} />,
690
+ meta: {
691
+ showFilter: false,
692
+ },
693
+ }),
694
+ columnHelper.accessor('data.flows', {
695
+ id: 'flows',
696
+ header: () => <span>Flows</span>,
697
+ cell: (info) => <CollectionListCell items={info.getValue()} />,
698
+ meta: {
699
+ showFilter: false,
700
+ },
701
+ }),
702
+ createBadgesColumn(tableConfiguration),
703
+ createActionsColumn('systems', tableConfiguration),
704
+ ];
705
+
620
706
  // ============================================================================
621
707
  // FLOW COLUMNS
622
708
  // ============================================================================
@@ -740,6 +826,8 @@ export const getDiscoverColumns = (collectionType: CollectionType, tableConfigur
740
826
  return getServiceColumns(tableConfiguration);
741
827
  case 'domains':
742
828
  return getDomainColumns(tableConfiguration);
829
+ case 'systems':
830
+ return getSystemColumns(tableConfiguration);
743
831
  case 'flows':
744
832
  return getFlowColumns(tableConfiguration);
745
833
  case 'containers':
@@ -1,4 +1,10 @@
1
1
  export { DiscoverTable } from './DiscoverTable';
2
- export type { DiscoverTableData, DiscoverTableProps, CollectionType, PropertyOption } from './DiscoverTable';
2
+ export type {
3
+ DiscoverTableData,
4
+ DiscoverTableProps,
5
+ CollectionType,
6
+ CollectionKindOption,
7
+ PropertyOption,
8
+ } from './DiscoverTable';
3
9
  export { FilterDropdown, CheckboxItem } from './FilterComponents';
4
10
  export type { FilterDropdownProps, CheckboxItemProps } from './FilterComponents';
@@ -0,0 +1,267 @@
1
+ import { useMemo, useState } from 'react';
2
+ import {
3
+ createColumnHelper,
4
+ flexRender,
5
+ getCoreRowModel,
6
+ getSortedRowModel,
7
+ useReactTable,
8
+ type SortingState,
9
+ } from '@tanstack/react-table';
10
+ import { ArrowDown, ArrowUp, ArrowUpDown, Search, SearchX } from 'lucide-react';
11
+ import { getColorAndIconForCollection } from '@utils/collections/icons';
12
+ import { getCollectionTextColorClass } from '@utils/collection-colors';
13
+ import { isIconPath, resolveIconUrl } from '@utils/icon';
14
+
15
+ export type SystemResourceCollection = 'services' | 'flows' | 'entities' | 'containers' | 'events' | 'commands' | 'queries';
16
+
17
+ export interface SystemResourceItem {
18
+ collection: SystemResourceCollection;
19
+ id: string;
20
+ name: string;
21
+ version: string;
22
+ summary?: string;
23
+ icon?: string;
24
+ href: string;
25
+ }
26
+
27
+ interface SystemResourcesTableProps {
28
+ resources: SystemResourceItem[];
29
+ }
30
+
31
+ const TYPE_LABELS: Record<SystemResourceCollection, string> = {
32
+ services: 'Services',
33
+ flows: 'Flows',
34
+ entities: 'Entities',
35
+ containers: 'Data Stores',
36
+ events: 'Events',
37
+ commands: 'Commands',
38
+ queries: 'Queries',
39
+ };
40
+
41
+ // Order the type filter pills follow on the page.
42
+ const TYPE_ORDER: SystemResourceCollection[] = ['services', 'flows', 'entities', 'containers', 'events', 'commands', 'queries'];
43
+
44
+ // Small icon for a collection type (used in filter pills and the type column).
45
+ const CollectionTypeIcon = ({ collection, className }: { collection: SystemResourceCollection; className?: string }) => {
46
+ const { color, Icon } = getColorAndIconForCollection(collection);
47
+ const colorClass = getCollectionTextColorClass(color, 'text-[rgb(var(--ec-icon-color))]');
48
+ return <Icon className={`${className ?? 'h-3.5 w-3.5'} ${colorClass}`} aria-hidden="true" />;
49
+ };
50
+
51
+ const ResourceIcon = ({ item }: { item: SystemResourceItem }) => {
52
+ if (item.icon && isIconPath(item.icon)) {
53
+ return <img src={resolveIconUrl(item.icon)} alt="" className="h-5 w-5" loading="lazy" />;
54
+ }
55
+ return <CollectionTypeIcon collection={item.collection} className="h-5 w-5" />;
56
+ };
57
+
58
+ const columnHelper = createColumnHelper<SystemResourceItem>();
59
+
60
+ const SortIcon = ({ direction }: { direction: false | 'asc' | 'desc' }) => {
61
+ if (direction === 'asc') return <ArrowUp className="h-3.5 w-3.5" aria-hidden="true" />;
62
+ if (direction === 'desc') return <ArrowDown className="h-3.5 w-3.5" aria-hidden="true" />;
63
+ return <ArrowUpDown className="h-3.5 w-3.5 opacity-40" aria-hidden="true" />;
64
+ };
65
+
66
+ export function SystemResourcesTable({ resources }: SystemResourcesTableProps) {
67
+ const [search, setSearch] = useState('');
68
+ const [selectedTypes, setSelectedTypes] = useState<SystemResourceCollection[]>([]);
69
+ const [sorting, setSorting] = useState<SortingState>([{ id: 'name', desc: false }]);
70
+
71
+ // Only show type filters that actually have resources in this system.
72
+ const availableTypes = useMemo(
73
+ () => TYPE_ORDER.filter((type) => resources.some((resource) => resource.collection === type)),
74
+ [resources]
75
+ );
76
+
77
+ const filtered = useMemo(() => {
78
+ const query = search.trim().toLowerCase();
79
+ return resources.filter((resource) => {
80
+ if (selectedTypes.length > 0 && !selectedTypes.includes(resource.collection)) {
81
+ return false;
82
+ }
83
+ if (query) {
84
+ const haystack = `${resource.name} ${resource.id} ${resource.summary ?? ''}`.toLowerCase();
85
+ if (!haystack.includes(query)) return false;
86
+ }
87
+ return true;
88
+ });
89
+ }, [resources, search, selectedTypes]);
90
+
91
+ const columns = useMemo(
92
+ () => [
93
+ columnHelper.accessor('name', {
94
+ header: 'Name',
95
+ cell: (info) => {
96
+ const resource = info.row.original;
97
+ return (
98
+ <a href={resource.href} className="group flex items-center gap-3">
99
+ <span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-card-bg))]">
100
+ <ResourceIcon item={resource} />
101
+ </span>
102
+ <span className="text-sm font-medium text-[rgb(var(--ec-page-text))] group-hover:text-[rgb(var(--ec-accent))]">
103
+ {resource.name}
104
+ </span>
105
+ </a>
106
+ );
107
+ },
108
+ }),
109
+ columnHelper.accessor((row) => TYPE_LABELS[row.collection], {
110
+ id: 'type',
111
+ header: 'Type',
112
+ cell: (info) => {
113
+ const { collection } = info.row.original;
114
+ return (
115
+ <span className="inline-flex items-center gap-1.5 text-sm text-[rgb(var(--ec-page-text-muted))]">
116
+ <CollectionTypeIcon collection={collection} />
117
+ {TYPE_LABELS[collection]}
118
+ </span>
119
+ );
120
+ },
121
+ }),
122
+ columnHelper.accessor('version', {
123
+ header: 'Version',
124
+ cell: (info) => (
125
+ <span className="inline-flex items-center rounded-md border border-[rgb(var(--ec-page-border))] px-2 py-0.5 text-xs text-[rgb(var(--ec-page-text-muted))]">
126
+ v{info.getValue()}
127
+ </span>
128
+ ),
129
+ }),
130
+ columnHelper.accessor((row) => row.summary ?? '', {
131
+ id: 'summary',
132
+ header: 'Summary',
133
+ enableSorting: false,
134
+ cell: (info) => (
135
+ <span className="line-clamp-2 text-sm text-[rgb(var(--ec-page-text-muted))]">{info.getValue() || '-'}</span>
136
+ ),
137
+ }),
138
+ ],
139
+ []
140
+ );
141
+
142
+ const table = useReactTable({
143
+ data: filtered,
144
+ columns,
145
+ state: { sorting },
146
+ onSortingChange: setSorting,
147
+ getCoreRowModel: getCoreRowModel(),
148
+ getSortedRowModel: getSortedRowModel(),
149
+ });
150
+
151
+ const toggleType = (type: SystemResourceCollection) => {
152
+ setSelectedTypes((prev) => (prev.includes(type) ? prev.filter((t) => t !== type) : [...prev, type]));
153
+ };
154
+
155
+ return (
156
+ <div className="flex flex-col gap-4">
157
+ {/* Controls: search + type filters */}
158
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
159
+ <div className="relative w-full sm:max-w-xs">
160
+ <Search
161
+ className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-[rgb(var(--ec-icon-color))]"
162
+ aria-hidden="true"
163
+ />
164
+ <input
165
+ type="text"
166
+ value={search}
167
+ onChange={(event) => setSearch(event.target.value)}
168
+ placeholder="Search resources..."
169
+ className="w-full rounded-md border border-[rgb(var(--ec-input-border))] bg-[rgb(var(--ec-input-bg))] py-2 pl-9 pr-3 text-sm text-[rgb(var(--ec-input-text))] placeholder:text-[rgb(var(--ec-page-text-muted))] focus:outline-none focus:ring-2 focus:ring-[rgb(var(--ec-accent)/0.4)]"
170
+ />
171
+ </div>
172
+
173
+ {availableTypes.length > 1 && (
174
+ <div className="flex flex-wrap gap-2">
175
+ {availableTypes.map((type) => {
176
+ const active = selectedTypes.includes(type);
177
+ return (
178
+ <button
179
+ key={type}
180
+ type="button"
181
+ onClick={() => toggleType(type)}
182
+ className={[
183
+ 'inline-flex items-center gap-1.5 rounded-full border px-3 py-1 text-xs font-medium transition-colors',
184
+ active
185
+ ? 'border-[rgb(var(--ec-accent))] bg-[rgb(var(--ec-accent-subtle))] text-[rgb(var(--ec-accent))]'
186
+ : 'border-[rgb(var(--ec-page-border))] text-[rgb(var(--ec-page-text-muted))] hover:text-[rgb(var(--ec-page-text))]',
187
+ ].join(' ')}
188
+ >
189
+ <CollectionTypeIcon collection={type} />
190
+ {TYPE_LABELS[type]}
191
+ </button>
192
+ );
193
+ })}
194
+ </div>
195
+ )}
196
+ </div>
197
+
198
+ {/* Table */}
199
+ <div className="overflow-hidden rounded-lg border border-[rgb(var(--ec-page-border))]">
200
+ <table className="min-w-full divide-y divide-[rgb(var(--ec-page-border))]">
201
+ <thead className="bg-[rgb(var(--ec-card-bg))]">
202
+ {table.getHeaderGroups().map((headerGroup) => (
203
+ <tr key={headerGroup.id}>
204
+ {headerGroup.headers.map((header) => {
205
+ const canSort = header.column.getCanSort();
206
+ const isSummary = header.column.id === 'summary';
207
+ return (
208
+ <th
209
+ key={header.id}
210
+ className={[
211
+ 'px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-[rgb(var(--ec-page-text-muted))]',
212
+ isSummary ? 'hidden md:table-cell' : '',
213
+ ].join(' ')}
214
+ >
215
+ {canSort ? (
216
+ <button
217
+ type="button"
218
+ onClick={header.column.getToggleSortingHandler()}
219
+ className="inline-flex items-center gap-1.5 uppercase tracking-wider hover:text-[rgb(var(--ec-page-text))]"
220
+ >
221
+ {flexRender(header.column.columnDef.header, header.getContext())}
222
+ <SortIcon direction={header.column.getIsSorted()} />
223
+ </button>
224
+ ) : (
225
+ flexRender(header.column.columnDef.header, header.getContext())
226
+ )}
227
+ </th>
228
+ );
229
+ })}
230
+ </tr>
231
+ ))}
232
+ </thead>
233
+ <tbody className="divide-y divide-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-page-bg))]">
234
+ {table.getRowModel().rows.length === 0 && (
235
+ <tr>
236
+ <td colSpan={columns.length} className="px-4 py-12">
237
+ <div className="flex flex-col items-center justify-center gap-2 text-center text-[rgb(var(--ec-page-text-muted))]">
238
+ <SearchX className="h-6 w-6" aria-hidden="true" />
239
+ <p className="text-sm">No resources found.</p>
240
+ </div>
241
+ </td>
242
+ </tr>
243
+ )}
244
+ {table.getRowModel().rows.map((row) => (
245
+ <tr key={row.id} className="transition-colors hover:bg-[rgb(var(--ec-card-bg))]">
246
+ {row.getVisibleCells().map((cell) => {
247
+ const isSummary = cell.column.id === 'summary';
248
+ return (
249
+ <td key={cell.id} className={['px-4 py-3', isSummary ? 'hidden max-w-md md:table-cell' : ''].join(' ')}>
250
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
251
+ </td>
252
+ );
253
+ })}
254
+ </tr>
255
+ ))}
256
+ </tbody>
257
+ </table>
258
+ </div>
259
+
260
+ <p className="text-xs text-[rgb(var(--ec-page-text-muted))]">
261
+ Showing {filtered.length} of {resources.length} resources
262
+ </p>
263
+ </div>
264
+ );
265
+ }
266
+
267
+ export default SystemResourcesTable;