@eventcatalog/core 3.29.1 → 3.30.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 (82) 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-H5UC2A5F.js → chunk-6UG4JMUV.js} +1 -1
  6. package/dist/{chunk-4MSAPCV3.js → chunk-ATRBVTJ6.js} +1 -1
  7. package/dist/{chunk-PLNJC7NZ.js → chunk-K3ZVEX2Y.js} +13 -2
  8. package/dist/{chunk-V4OTI3PF.js → chunk-MVZKHUX2.js} +1 -1
  9. package/dist/{chunk-FVKDNLZK.js → chunk-RRBDF4MM.js} +1 -1
  10. package/dist/{chunk-24NGK43A.js → chunk-Z26P4PCB.js} +1 -1
  11. package/dist/constants.cjs +1 -1
  12. package/dist/constants.js +1 -1
  13. package/dist/eventcatalog.cjs +14 -3
  14. package/dist/eventcatalog.js +6 -6
  15. package/dist/generate.cjs +1 -1
  16. package/dist/generate.js +3 -3
  17. package/dist/utils/cli-logger.cjs +1 -1
  18. package/dist/utils/cli-logger.js +2 -2
  19. package/dist/watcher.cjs +13 -2
  20. package/dist/watcher.js +1 -1
  21. package/eventcatalog/astro.config.mjs +1 -1
  22. package/eventcatalog/public/logo.png +0 -0
  23. package/eventcatalog/src/components/CopyAsMarkdown.tsx +2 -2
  24. package/eventcatalog/src/components/EnvironmentDropdown.tsx +33 -21
  25. package/eventcatalog/src/components/FieldsExplorer/FieldFilters.tsx +3 -53
  26. package/eventcatalog/src/components/FieldsExplorer/FieldsExplorer.tsx +144 -91
  27. package/eventcatalog/src/components/FieldsExplorer/FieldsTable.tsx +112 -109
  28. package/eventcatalog/src/components/Header.astro +9 -19
  29. package/eventcatalog/src/components/MDX/Accordion/Accordion.tsx +12 -14
  30. package/eventcatalog/src/components/MDX/Accordion/AccordionGroup.astro +11 -3
  31. package/eventcatalog/src/components/MDX/ResourceRef/ResourceRef.astro +15 -5
  32. package/eventcatalog/src/components/SchemaExplorer/ApiContentViewer.tsx +164 -53
  33. package/eventcatalog/src/components/SchemaExplorer/DiffViewer.tsx +1 -1
  34. package/eventcatalog/src/components/SchemaExplorer/ExamplesViewer.tsx +4 -4
  35. package/eventcatalog/src/components/SchemaExplorer/Pagination.tsx +12 -10
  36. package/eventcatalog/src/components/SchemaExplorer/SchemaContentViewer.tsx +48 -77
  37. package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsPanel.tsx +238 -169
  38. package/eventcatalog/src/components/SchemaExplorer/SchemaExplorer.tsx +189 -230
  39. package/eventcatalog/src/components/SchemaExplorer/SchemaListItem.tsx +39 -36
  40. package/eventcatalog/src/components/Search/Search.astro +1 -1
  41. package/eventcatalog/src/components/Seo.astro +1 -1
  42. package/eventcatalog/src/components/SideNav/NestedSideBar/SearchBar.tsx +3 -3
  43. package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +229 -256
  44. package/eventcatalog/src/components/Tables/Discover/DiscoverTable.tsx +78 -59
  45. package/eventcatalog/src/components/Tables/Discover/columns.tsx +130 -197
  46. package/eventcatalog/src/components/Tables/Table.tsx +21 -18
  47. package/eventcatalog/src/components/Tables/columns/TeamsTableColumns.tsx +79 -131
  48. package/eventcatalog/src/components/Tables/columns/UserTableColumns.tsx +104 -175
  49. package/eventcatalog/src/enterprise/auth/error.astro +1 -1
  50. package/eventcatalog/src/enterprise/auth/login.astro +1 -1
  51. package/eventcatalog/src/enterprise/custom-documentation/components/CustomDocsNav/index.tsx +95 -93
  52. package/eventcatalog/src/enterprise/custom-documentation/pages/docs/custom/index.astro +174 -136
  53. package/eventcatalog/src/enterprise/fields/pages/fields.astro +10 -8
  54. package/eventcatalog/src/enterprise/integrations/eventcatalog-features.ts +0 -8
  55. package/eventcatalog/src/layouts/DirectoryLayout.astro +17 -88
  56. package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +528 -146
  57. package/eventcatalog/src/layouts/VisualiserLayout.astro +7 -2
  58. package/eventcatalog/src/pages/_index.astro +5 -3
  59. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/index.astro +3 -3
  60. package/eventcatalog/src/pages/diagrams/[id]/[version]/index.astro +223 -73
  61. package/eventcatalog/src/pages/discover/[type]/index.astro +22 -141
  62. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/[docVersion]/index.astro +129 -29
  63. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/index.astro +129 -29
  64. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/asyncapi/[filename].astro +6 -2
  65. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/examples/[...filename].astro +2 -2
  66. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/graphql/[filename].astro +21 -18
  67. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +33 -32
  68. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/spec/[filename].astro +5 -1
  69. package/eventcatalog/src/pages/docs/[type]/[id]/language/[dictionaryId]/index.astro +2 -2
  70. package/eventcatalog/src/pages/docs/[type]/[id]/language/index.astro +4 -6
  71. package/eventcatalog/src/pages/docs/teams/[id]/index.astro +11 -4
  72. package/eventcatalog/src/pages/docs/users/[id]/index.astro +11 -4
  73. package/eventcatalog/src/pages/schemas/explorer/index.astro +10 -8
  74. package/eventcatalog/src/pages/studio.astro +1 -1
  75. package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/entity-map/index.astro +2 -7
  76. package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/index.astro +2 -2
  77. package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/entity-map/index.astro +2 -7
  78. package/eventcatalog/src/styles/theme.css +68 -12
  79. package/eventcatalog/src/types/react-syntax-highlighter.d.ts +13 -0
  80. package/package.json +1 -1
  81. package/eventcatalog/public/logo.svg +0 -14
  82. package/eventcatalog/src/enterprise/plans/index.astro +0 -319
@@ -43,6 +43,7 @@ export interface DiscoverTableData {
43
43
  id: string;
44
44
  name: string;
45
45
  summary?: string;
46
+ icon?: string;
46
47
  version: string;
47
48
  latestVersion?: string;
48
49
  draft?: boolean | { title?: string; message: string };
@@ -73,6 +74,7 @@ export interface DiscoverTableProps<T extends DiscoverTableData> {
73
74
  data: T[];
74
75
  collectionType: CollectionType;
75
76
  collectionLabel: string;
77
+ collectionDescription?: string;
76
78
  domains?: Array<{ id: string; name: string; version: string }>;
77
79
  owners?: Array<{ id: string; name: string; type?: 'user' | 'team' }>;
78
80
  producers?: Array<{ id: string; name: string }>;
@@ -89,6 +91,7 @@ export function DiscoverTable<T extends DiscoverTableData>({
89
91
  data: initialData,
90
92
  collectionType,
91
93
  collectionLabel,
94
+ collectionDescription,
92
95
  domains = [],
93
96
  owners = [],
94
97
  producers = [],
@@ -427,9 +430,16 @@ export function DiscoverTable<T extends DiscoverTableData>({
427
430
  const selectedPropertyLabels = selectedProperties.map((id) => propertyOptions.find((p) => p.id === id)?.label || id);
428
431
 
429
432
  return (
430
- <div className="flex h-full">
433
+ <div className="flex h-full min-h-0" style={{ ['--ec-discover-sidebar-width' as any]: '320px' }}>
431
434
  {/* Filter Sidebar */}
432
- <div className="w-[320px] flex-shrink-0 flex flex-col bg-[rgb(var(--ec-page-bg))] bg-gradient-to-bl from-[rgb(var(--ec-page-bg))] via-[rgb(var(--ec-page-bg))] to-[rgb(var(--ec-accent)/0.08)] border-r border-[rgb(var(--ec-page-border))]">
435
+ <div
436
+ className="fixed left-[var(--ec-vertical-nav-width)] top-0 z-20 flex h-screen w-[320px] flex-shrink-0 flex-col border-r border-[rgb(var(--ec-content-border))] bg-[rgb(var(--ec-page-bg))]"
437
+ style={{ width: 'var(--ec-discover-sidebar-width, 320px)' }}
438
+ >
439
+ <div className="flex h-[60px] flex-shrink-0 items-center border-b border-[rgb(var(--ec-page-border))] px-4">
440
+ <h2 className="text-[13px] font-semibold text-[rgb(var(--ec-page-text))]">{collectionLabel} Filters</h2>
441
+ </div>
442
+
433
443
  {/* Filter sections */}
434
444
  <div className="flex-1 overflow-y-auto px-4 pt-4 pb-4 space-y-6">
435
445
  {/* Message Filters Section */}
@@ -495,10 +505,6 @@ export function DiscoverTable<T extends DiscoverTableData>({
495
505
 
496
506
  {/* Catalog Filters Section */}
497
507
  <div className="space-y-3">
498
- <h3 className="text-[11px] font-bold uppercase tracking-widest text-[rgb(var(--ec-page-text))] pb-2">
499
- Catalog Filters
500
- </h3>
501
-
502
508
  {/* Domains Filter */}
503
509
  {showDomainsFilter && domains.length > 0 && (
504
510
  <div>
@@ -666,13 +672,21 @@ export function DiscoverTable<T extends DiscoverTableData>({
666
672
  </div>
667
673
 
668
674
  {/* Main Table */}
669
- <div className="flex-1 min-w-0 flex flex-col overflow-hidden">
675
+ <div
676
+ className="flex-1 min-w-0 flex flex-col overflow-hidden"
677
+ style={{ marginLeft: 'var(--ec-discover-sidebar-width, 320px)' }}
678
+ >
670
679
  {/* Table Header */}
671
- <div className="flex items-center justify-between px-6 py-4">
672
- <h2 className="text-lg font-semibold text-[rgb(var(--ec-page-text))]">
673
- {collectionLabel}{' '}
674
- <span className="text-sm text-[rgb(var(--ec-page-text-muted))] font-normal ml-1">({totalResults})</span>
675
- </h2>
680
+ <div className="flex items-end justify-between gap-6 px-6 py-5">
681
+ <div className="min-w-0">
682
+ <h2 className="text-2xl font-semibold text-[rgb(var(--ec-page-text))] md:text-4xl">
683
+ {collectionLabel}{' '}
684
+ <span className="ml-1 text-lg font-normal text-[rgb(var(--ec-page-text-muted))] md:text-3xl">({totalResults})</span>
685
+ </h2>
686
+ {collectionDescription && (
687
+ <p className="max-w-3xl pt-2 text-base font-light text-[rgb(var(--ec-page-text-muted))]">{collectionDescription}</p>
688
+ )}
689
+ </div>
676
690
  <div className="relative">
677
691
  <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[rgb(var(--ec-icon-color))]" />
678
692
  <input
@@ -680,7 +694,7 @@ export function DiscoverTable<T extends DiscoverTableData>({
680
694
  value={tableFilter}
681
695
  onChange={(e) => setTableFilter(e.target.value)}
682
696
  placeholder="Filter..."
683
- className="pl-9 pr-3 py-1.5 text-sm w-48 bg-[rgb(var(--ec-dropdown-bg))] text-[rgb(var(--ec-input-text))] border border-[rgb(var(--ec-dropdown-border))] rounded-lg placeholder:text-[rgb(var(--ec-icon-color))] focus:outline-hidden focus:ring-1 focus:ring-[rgb(var(--ec-accent)/0.3)] focus:border-[rgb(var(--ec-accent))] transition-colors"
697
+ className="w-64 rounded-lg border border-[rgb(var(--ec-dropdown-border))] bg-[rgb(var(--ec-dropdown-bg))] py-2 pl-9 pr-3 text-sm text-[rgb(var(--ec-input-text))] placeholder:text-[rgb(var(--ec-icon-color))] transition-colors focus:border-[rgb(var(--ec-accent))] focus:outline-hidden focus:ring-1 focus:ring-[rgb(var(--ec-accent)/0.3)]"
684
698
  />
685
699
  {tableFilter && (
686
700
  <button
@@ -694,55 +708,60 @@ export function DiscoverTable<T extends DiscoverTableData>({
694
708
  </div>
695
709
 
696
710
  {/* Table */}
697
- <div className="flex-1 overflow-auto px-6">
698
- <table className="min-w-full divide-y divide-[rgb(var(--ec-page-border))]">
699
- <thead className="sticky top-0 z-10">
700
- {table.getHeaderGroups().map((headerGroup, index) => (
701
- <tr key={`${headerGroup}-${index}`}>
702
- {headerGroup.headers.map((header) => (
703
- <th
704
- key={`${header.id}`}
705
- className="px-4 py-2.5 text-left text-[11px] font-medium text-[rgb(var(--ec-page-text-muted))] uppercase tracking-wider"
706
- >
707
- {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
708
- </th>
709
- ))}
710
- </tr>
711
- ))}
712
- </thead>
713
-
714
- <tbody className="divide-y divide-[rgb(var(--ec-page-border)/0.5)]">
715
- {hasResults ? (
716
- table.getRowModel().rows.map((row, index) => (
717
- <tr key={`${row.id}-${index}`} className="group hover:bg-[rgb(var(--ec-content-hover))] transition-colors">
718
- {row.getVisibleCells().map((cell) => (
719
- <td
720
- key={cell.id}
721
- className={`px-4 py-3 text-sm text-[rgb(var(--ec-page-text))] ${cell.column.columnDef.meta?.className || ''}`}
711
+ <div className="min-h-0 flex-1 overflow-auto px-6 pb-5">
712
+ <div className="overflow-hidden rounded-xl border border-[rgb(var(--ec-page-border)/0.72)] dark:border-white/10 bg-[rgb(var(--ec-dropdown-bg)/0.66)]">
713
+ <table className="min-w-full divide-y divide-[rgb(var(--ec-page-border)/0.62)] dark:divide-white/10">
714
+ <thead className="sticky top-0 z-10 bg-[rgb(var(--ec-content-hover)/0.45)]">
715
+ {table.getHeaderGroups().map((headerGroup, index) => (
716
+ <tr key={`${headerGroup}-${index}`}>
717
+ {headerGroup.headers.map((header) => (
718
+ <th
719
+ key={`${header.id}`}
720
+ className="px-4 py-2.5 text-left text-[11px] font-medium text-[rgb(var(--ec-page-text-muted))] uppercase tracking-wider"
722
721
  >
723
- {flexRender(cell.column.columnDef.cell, cell.getContext())}
724
- </td>
722
+ {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
723
+ </th>
725
724
  ))}
726
725
  </tr>
727
- ))
728
- ) : (
729
- <tr>
730
- <td colSpan={table.getAllColumns().length} className="px-4 py-12 text-center">
731
- <div className="flex flex-col items-center justify-center text-[rgb(var(--ec-page-text-muted))]">
732
- <SearchX className="w-10 h-10 text-[rgb(var(--ec-icon-color))] mb-3 opacity-50" />
733
- <p className="text-sm font-medium text-[rgb(var(--ec-page-text-muted))]">No results found</p>
734
- <p className="text-xs text-[rgb(var(--ec-icon-color))] mt-1">Try adjusting your search or filters</p>
735
- {activeFilterCount > 0 && (
736
- <button onClick={clearAllFilters} className="mt-3 text-sm text-[rgb(var(--ec-accent))] hover:underline">
737
- Clear all filters
738
- </button>
739
- )}
740
- </div>
741
- </td>
742
- </tr>
743
- )}
744
- </tbody>
745
- </table>
726
+ ))}
727
+ </thead>
728
+
729
+ <tbody className="divide-y divide-[rgb(var(--ec-page-border)/0.5)] dark:divide-white/8">
730
+ {hasResults ? (
731
+ table.getRowModel().rows.map((row, index) => (
732
+ <tr
733
+ key={`${row.id}-${index}`}
734
+ className="group bg-transparent transition-colors hover:bg-[rgb(var(--ec-content-hover)/0.38)]"
735
+ >
736
+ {row.getVisibleCells().map((cell) => (
737
+ <td
738
+ key={cell.id}
739
+ className={`px-4 py-3 text-sm text-[rgb(var(--ec-page-text))] ${cell.column.columnDef.meta?.className || ''}`}
740
+ >
741
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
742
+ </td>
743
+ ))}
744
+ </tr>
745
+ ))
746
+ ) : (
747
+ <tr>
748
+ <td colSpan={table.getAllColumns().length} className="px-4 py-12 text-center">
749
+ <div className="flex flex-col items-center justify-center text-[rgb(var(--ec-page-text-muted))]">
750
+ <SearchX className="w-10 h-10 text-[rgb(var(--ec-icon-color))] mb-3 opacity-50" />
751
+ <p className="text-sm font-medium text-[rgb(var(--ec-page-text-muted))]">No results found</p>
752
+ <p className="text-xs text-[rgb(var(--ec-icon-color))] mt-1">Try adjusting your search or filters</p>
753
+ {activeFilterCount > 0 && (
754
+ <button onClick={clearAllFilters} className="mt-3 text-sm text-[rgb(var(--ec-accent))] hover:underline">
755
+ Clear all filters
756
+ </button>
757
+ )}
758
+ </div>
759
+ </td>
760
+ </tr>
761
+ )}
762
+ </tbody>
763
+ </table>
764
+ </div>
746
765
  </div>
747
766
 
748
767
  {/* Pagination */}
@@ -1,22 +1,12 @@
1
1
  import { createColumnHelper } from '@tanstack/react-table';
2
- import { useMemo, useState } from 'react';
3
- import {
4
- ServerIcon,
5
- BoltIcon,
6
- ChatBubbleLeftIcon,
7
- MagnifyingGlassIcon,
8
- RectangleGroupIcon,
9
- QueueListIcon,
10
- DocumentTextIcon,
11
- MapIcon,
12
- CubeIcon,
13
- } from '@heroicons/react/24/solid';
14
- import { ArrowDownIcon, ArrowUpIcon } from '@heroicons/react/24/outline';
15
- import { DatabaseIcon } from 'lucide-react';
16
- import * as Tooltip from '@radix-ui/react-tooltip';
2
+ import { useEffect, useMemo, useRef, useState } from 'react';
3
+ import { DocumentTextIcon, MapIcon } from '@heroicons/react/24/solid';
4
+ import { ArrowDownIcon, ArrowUpIcon, EllipsisVerticalIcon, StarIcon } from '@heroicons/react/24/outline';
17
5
  import { buildUrl } from '@utils/url-builder';
18
6
  import { getColorAndIconForCollection } from '@utils/collections/icons';
19
- import FavoriteButton from '@components/FavoriteButton';
7
+ import { isIconPath, resolveIconUrl } from '@utils/icon';
8
+ import { useStore } from '@nanostores/react';
9
+ import { favoritesStore, toggleFavorite, type FavoriteItem } from '../../../stores/favorites-store';
20
10
  import type { DiscoverTableData, CollectionType } from './DiscoverTable';
21
11
  import type { TableConfiguration } from '@types';
22
12
 
@@ -34,25 +24,6 @@ const colorClasses: Record<string, string> = {
34
24
  cyan: 'text-cyan-500',
35
25
  };
36
26
 
37
- // Reusable tooltip wrapper component
38
- const ActionTooltip = ({ children, label }: { children: React.ReactNode; label: string }) => (
39
- <Tooltip.Provider delayDuration={200}>
40
- <Tooltip.Root>
41
- <Tooltip.Trigger asChild>{children}</Tooltip.Trigger>
42
- <Tooltip.Portal>
43
- <Tooltip.Content
44
- className="bg-[rgb(var(--ec-page-text))] text-[rgb(var(--ec-page-bg))] rounded px-2 py-1 text-xs shadow-md z-50"
45
- side="top"
46
- sideOffset={5}
47
- >
48
- {label}
49
- <Tooltip.Arrow className="fill-[rgb(var(--ec-page-text))]" />
50
- </Tooltip.Content>
51
- </Tooltip.Portal>
52
- </Tooltip.Root>
53
- </Tooltip.Provider>
54
- );
55
-
56
27
  const columnHelper = createColumnHelper<DiscoverTableData>();
57
28
 
58
29
  // Badge cell component (proper React component to use hooks)
@@ -95,6 +66,119 @@ const createBadgesColumn = (tableConfiguration: TableConfiguration) =>
95
66
  },
96
67
  });
97
68
 
69
+ const ResourceNameCell = ({ item }: { item: DiscoverTableData }) => {
70
+ const isLatestVersion = item.data.version === item.data.latestVersion;
71
+ const { color, Icon } = getColorAndIconForCollection(item.collection);
72
+ const resourceIcon = item.data.icon;
73
+ const resourceIconUrl = isIconPath(resourceIcon) ? resolveIconUrl(resourceIcon) : null;
74
+
75
+ return (
76
+ <a
77
+ href={buildUrl(`/docs/${item.collection}/${item.data.id}/${item.data.version}`)}
78
+ className="group inline-flex items-center gap-2.5 hover:text-[rgb(var(--ec-accent))] transition-colors"
79
+ >
80
+ {resourceIconUrl ? (
81
+ <img src={resourceIconUrl} alt="" className="h-5 w-5 flex-shrink-0 rounded-sm object-contain" />
82
+ ) : (
83
+ <Icon className={`h-4 w-4 flex-shrink-0 ${colorClasses[color] || 'text-[rgb(var(--ec-icon-color))]'}`} />
84
+ )}
85
+ <span className="text-sm font-semibold text-[rgb(var(--ec-page-text))] group-hover:text-[rgb(var(--ec-accent))]">
86
+ {item.data.name}
87
+ </span>
88
+ {!isLatestVersion && <span className="text-xs text-[rgb(var(--ec-icon-color))]">v{item.data.version}</span>}
89
+ </a>
90
+ );
91
+ };
92
+
93
+ const RowActionsMenu = ({ item, collectionType }: { item: DiscoverTableData; collectionType: CollectionType }) => {
94
+ const [isOpen, setIsOpen] = useState(false);
95
+ const menuRef = useRef<HTMLDivElement>(null);
96
+ const favorites = useStore(favoritesStore);
97
+ const href = buildUrl(`/docs/${item.collection}/${item.data.id}/${item.data.version}`);
98
+ const visualiserHref = buildUrl(`/visualiser/${item.collection}/${item.data.id}/${item.data.version}`);
99
+ const nodeKey = `${item.collection}-${item.data.id}-${item.data.version}`;
100
+ const badgeLabel =
101
+ collectionType === 'external-systems'
102
+ ? 'External System'
103
+ : collectionType.charAt(0).toUpperCase() + collectionType.slice(1, -1);
104
+ const isFavorite = favorites.some((fav) => fav.nodeKey === nodeKey);
105
+
106
+ useEffect(() => {
107
+ const handlePointerDown = (event: MouseEvent) => {
108
+ if (!menuRef.current?.contains(event.target as Node)) {
109
+ setIsOpen(false);
110
+ }
111
+ };
112
+
113
+ const handleEscape = (event: KeyboardEvent) => {
114
+ if (event.key === 'Escape') {
115
+ setIsOpen(false);
116
+ }
117
+ };
118
+
119
+ document.addEventListener('mousedown', handlePointerDown);
120
+ document.addEventListener('keydown', handleEscape);
121
+
122
+ return () => {
123
+ document.removeEventListener('mousedown', handlePointerDown);
124
+ document.removeEventListener('keydown', handleEscape);
125
+ };
126
+ }, []);
127
+
128
+ const handleToggleFavorite = () => {
129
+ const favoriteItem: FavoriteItem = {
130
+ nodeKey,
131
+ path: [],
132
+ title: item.data.name,
133
+ badge: badgeLabel,
134
+ href,
135
+ };
136
+ toggleFavorite(favoriteItem);
137
+ setIsOpen(false);
138
+ };
139
+
140
+ return (
141
+ <div className="relative flex justify-end" ref={menuRef}>
142
+ <button
143
+ type="button"
144
+ aria-haspopup="menu"
145
+ aria-expanded={isOpen}
146
+ onClick={() => setIsOpen((prev) => !prev)}
147
+ className="rounded-md p-1.5 text-[rgb(var(--ec-icon-color))] transition-colors hover:bg-[rgb(var(--ec-content-hover)/0.5)] hover:text-[rgb(var(--ec-page-text))]"
148
+ >
149
+ <EllipsisVerticalIcon className="h-4 w-4" />
150
+ </button>
151
+
152
+ {isOpen && (
153
+ <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">
154
+ <a
155
+ href={href}
156
+ 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))]"
157
+ >
158
+ <DocumentTextIcon className="h-3.5 w-3.5 text-[rgb(var(--ec-page-text-muted))]" />
159
+ View documentation
160
+ </a>
161
+ <a
162
+ href={visualiserHref}
163
+ 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))]"
164
+ >
165
+ <MapIcon className="h-3.5 w-3.5 text-[rgb(var(--ec-page-text-muted))]" />
166
+ View in visualiser
167
+ </a>
168
+ <button
169
+ type="button"
170
+ onClick={handleToggleFavorite}
171
+ 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))]"
172
+ >
173
+ <StarIcon className="h-3.5 w-3.5 text-[rgb(var(--ec-page-text-muted))]" />
174
+ {isFavorite ? 'Remove from favorites' : 'Add to favorites'}
175
+ </button>
176
+ </div>
177
+ )}
178
+ </div>
179
+ );
180
+ };
181
+
98
182
  // Shared actions column
99
183
  const createActionsColumn = (collectionType: CollectionType, tableConfiguration: TableConfiguration) =>
100
184
  columnHelper.accessor('data.name', {
@@ -102,38 +186,7 @@ const createActionsColumn = (collectionType: CollectionType, tableConfiguration:
102
186
  header: () => <span></span>,
103
187
  cell: (info) => {
104
188
  const item = info.row.original;
105
- const href = buildUrl(`/docs/${item.collection}/${item.data.id}/${item.data.version}`);
106
- const nodeKey = `${item.collection}-${item.data.id}-${item.data.version}`;
107
- const badgeLabel =
108
- collectionType === 'external-systems'
109
- ? 'External System'
110
- : collectionType.charAt(0).toUpperCase() + collectionType.slice(1, -1);
111
-
112
- return (
113
- <div className="flex items-center gap-0.5">
114
- <ActionTooltip label="View documentation">
115
- <a
116
- className="p-1.5 text-[rgb(var(--ec-icon-color))] hover:text-[rgb(var(--ec-accent))] hover:bg-[rgb(var(--ec-accent)/0.1)] rounded-md transition-colors"
117
- href={href}
118
- >
119
- <DocumentTextIcon className="w-4 h-4" />
120
- </a>
121
- </ActionTooltip>
122
- <ActionTooltip label="View in visualiser">
123
- <a
124
- className="p-1.5 text-[rgb(var(--ec-icon-color))] hover:text-[rgb(var(--ec-accent))] hover:bg-[rgb(var(--ec-accent)/0.1)] rounded-md transition-colors"
125
- href={buildUrl(`/visualiser/${item.collection}/${item.data.id}/${item.data.version}`)}
126
- >
127
- <MapIcon className="w-4 h-4" />
128
- </a>
129
- </ActionTooltip>
130
- <ActionTooltip label="Add to favorites">
131
- <span>
132
- <FavoriteButton nodeKey={nodeKey} title={item.data.name} badge={badgeLabel} href={href} size="sm" />
133
- </span>
134
- </ActionTooltip>
135
- </div>
136
- );
189
+ return <RowActionsMenu item={item} collectionType={collectionType} />;
137
190
  },
138
191
  meta: {
139
192
  showFilter: false,
@@ -150,7 +203,7 @@ const createSummaryColumn = (tableConfiguration: TableConfiguration) =>
150
203
  const isDraft = info.row.original.data.draft;
151
204
  const displayText = `${summary || ''}${isDraft ? ' (Draft)' : ''}`;
152
205
  return (
153
- <span className="text-sm text-[rgb(var(--ec-icon-color))] line-clamp-2" title={displayText}>
206
+ <span className="text-[0.8rem] text-[rgb(var(--ec-icon-color))] line-clamp-2" title={displayText}>
154
207
  {displayText}
155
208
  </span>
156
209
  );
@@ -193,7 +246,7 @@ const CollectionListCell = ({
193
246
  <a
194
247
  key={`${item.data.id}-${index}`}
195
248
  href={buildUrl(`/docs/${item.collection}/${item.data.id}/${item.data.version}`)}
196
- className="group inline-flex items-center gap-1.5 text-xs hover:text-[rgb(var(--ec-accent))] transition-colors"
249
+ className="group inline-flex items-center gap-1.5 text-[0.8rem] text-[rgb(var(--ec-icon-color))] hover:text-[rgb(var(--ec-accent))] transition-colors"
197
250
  >
198
251
  <item.Icon className={`h-3.5 w-3.5 ${colorClasses[item.color] || 'text-gray-500'} flex-shrink-0`} />
199
252
  <span className="truncate max-w-[120px]" title={item.data.name}>
@@ -220,22 +273,7 @@ export const getEventColumns = (tableConfiguration: TableConfiguration) => [
220
273
  columnHelper.accessor('data.name', {
221
274
  id: 'name',
222
275
  header: () => <span>{tableConfiguration?.columns?.name?.label || 'Event'}</span>,
223
- cell: (info) => {
224
- const item = info.row.original;
225
- const isLatestVersion = item.data.version === item.data.latestVersion;
226
- return (
227
- <a
228
- href={buildUrl(`/docs/${item.collection}/${item.data.id}/${item.data.version}`)}
229
- className="group inline-flex items-center gap-2 hover:text-[rgb(var(--ec-accent))] transition-colors"
230
- >
231
- <BoltIcon className="h-4 w-4 text-orange-500 flex-shrink-0" />
232
- <span className="text-sm font-semibold text-[rgb(var(--ec-page-text))] group-hover:text-[rgb(var(--ec-accent))]">
233
- {item.data.name}
234
- </span>
235
- {!isLatestVersion && <span className="text-xs text-[rgb(var(--ec-icon-color))]">v{item.data.version}</span>}
236
- </a>
237
- );
238
- },
276
+ cell: (info) => <ResourceNameCell item={info.row.original} />,
239
277
  meta: {
240
278
  filterVariant: 'name',
241
279
  },
@@ -268,22 +306,7 @@ export const getCommandColumns = (tableConfiguration: TableConfiguration) => [
268
306
  columnHelper.accessor('data.name', {
269
307
  id: 'name',
270
308
  header: () => <span>{tableConfiguration?.columns?.name?.label || 'Command'}</span>,
271
- cell: (info) => {
272
- const item = info.row.original;
273
- const isLatestVersion = item.data.version === item.data.latestVersion;
274
- return (
275
- <a
276
- href={buildUrl(`/docs/${item.collection}/${item.data.id}/${item.data.version}`)}
277
- className="group inline-flex items-center gap-2 hover:text-[rgb(var(--ec-accent))] transition-colors"
278
- >
279
- <ChatBubbleLeftIcon className="h-4 w-4 text-blue-500 flex-shrink-0" />
280
- <span className="text-sm font-semibold text-[rgb(var(--ec-page-text))] group-hover:text-[rgb(var(--ec-accent))]">
281
- {item.data.name}
282
- </span>
283
- {!isLatestVersion && <span className="text-xs text-[rgb(var(--ec-icon-color))]">v{item.data.version}</span>}
284
- </a>
285
- );
286
- },
309
+ cell: (info) => <ResourceNameCell item={info.row.original} />,
287
310
  meta: {
288
311
  filterVariant: 'name',
289
312
  },
@@ -316,22 +339,7 @@ export const getQueryColumns = (tableConfiguration: TableConfiguration) => [
316
339
  columnHelper.accessor('data.name', {
317
340
  id: 'name',
318
341
  header: () => <span>{tableConfiguration?.columns?.name?.label || 'Query'}</span>,
319
- cell: (info) => {
320
- const item = info.row.original;
321
- const isLatestVersion = item.data.version === item.data.latestVersion;
322
- return (
323
- <a
324
- href={buildUrl(`/docs/${item.collection}/${item.data.id}/${item.data.version}`)}
325
- className="group inline-flex items-center gap-2 hover:text-[rgb(var(--ec-accent))] transition-colors"
326
- >
327
- <MagnifyingGlassIcon className="h-4 w-4 text-green-500 flex-shrink-0" />
328
- <span className="text-sm font-semibold text-[rgb(var(--ec-page-text))] group-hover:text-[rgb(var(--ec-accent))]">
329
- {item.data.name}
330
- </span>
331
- {!isLatestVersion && <span className="text-xs text-[rgb(var(--ec-icon-color))]">v{item.data.version}</span>}
332
- </a>
333
- );
334
- },
342
+ cell: (info) => <ResourceNameCell item={info.row.original} />,
335
343
  meta: {
336
344
  filterVariant: 'name',
337
345
  },
@@ -364,22 +372,7 @@ export const getServiceColumns = (tableConfiguration: TableConfiguration) => [
364
372
  columnHelper.accessor('data.name', {
365
373
  id: 'name',
366
374
  header: () => <span>{tableConfiguration?.columns?.name?.label || 'Service'}</span>,
367
- cell: (info) => {
368
- const item = info.row.original;
369
- const isLatestVersion = item.data.version === item.data.latestVersion;
370
- return (
371
- <a
372
- href={buildUrl(`/docs/${item.collection}/${item.data.id}/${item.data.version}`)}
373
- className="group inline-flex items-center gap-2 hover:text-[rgb(var(--ec-accent))] transition-colors"
374
- >
375
- <ServerIcon className="h-4 w-4 text-pink-500 flex-shrink-0" />
376
- <span className="text-sm font-semibold text-[rgb(var(--ec-page-text))] group-hover:text-[rgb(var(--ec-accent))]">
377
- {item.data.name}
378
- </span>
379
- {!isLatestVersion && <span className="text-xs text-[rgb(var(--ec-icon-color))]">v{item.data.version}</span>}
380
- </a>
381
- );
382
- },
375
+ cell: (info) => <ResourceNameCell item={info.row.original} />,
383
376
  meta: {
384
377
  filterVariant: 'name',
385
378
  },
@@ -422,22 +415,7 @@ export const getDomainColumns = (tableConfiguration: TableConfiguration) => [
422
415
  columnHelper.accessor('data.name', {
423
416
  id: 'name',
424
417
  header: () => <span>{tableConfiguration?.columns?.name?.label || 'Domain'}</span>,
425
- cell: (info) => {
426
- const item = info.row.original;
427
- const isLatestVersion = item.data.version === item.data.latestVersion;
428
- return (
429
- <a
430
- href={buildUrl(`/docs/${item.collection}/${item.data.id}/${item.data.version}`)}
431
- className="group inline-flex items-center gap-2 hover:text-[rgb(var(--ec-accent))] transition-colors"
432
- >
433
- <RectangleGroupIcon className="h-4 w-4 text-yellow-500 flex-shrink-0" />
434
- <span className="text-sm font-semibold text-[rgb(var(--ec-page-text))] group-hover:text-[rgb(var(--ec-accent))]">
435
- {item.data.name}
436
- </span>
437
- {!isLatestVersion && <span className="text-xs text-[rgb(var(--ec-icon-color))]">v{item.data.version}</span>}
438
- </a>
439
- );
440
- },
418
+ cell: (info) => <ResourceNameCell item={info.row.original} />,
441
419
  meta: {
442
420
  filterVariant: 'name',
443
421
  },
@@ -462,22 +440,7 @@ export const getFlowColumns = (tableConfiguration: TableConfiguration) => [
462
440
  columnHelper.accessor('data.name', {
463
441
  id: 'name',
464
442
  header: () => <span>{tableConfiguration?.columns?.name?.label || 'Flow'}</span>,
465
- cell: (info) => {
466
- const item = info.row.original;
467
- const isLatestVersion = item.data.version === item.data.latestVersion;
468
- return (
469
- <a
470
- href={buildUrl(`/docs/${item.collection}/${item.data.id}/${item.data.version}`)}
471
- className="group inline-flex items-center gap-2 hover:text-[rgb(var(--ec-accent))] transition-colors"
472
- >
473
- <QueueListIcon className="h-4 w-4 text-teal-500 flex-shrink-0" />
474
- <span className="text-sm font-semibold text-[rgb(var(--ec-page-text))] group-hover:text-[rgb(var(--ec-accent))]">
475
- {item.data.name}
476
- </span>
477
- {!isLatestVersion && <span className="text-xs text-[rgb(var(--ec-icon-color))]">v{item.data.version}</span>}
478
- </a>
479
- );
480
- },
443
+ cell: (info) => <ResourceNameCell item={info.row.original} />,
481
444
  meta: {
482
445
  filterVariant: 'name',
483
446
  },
@@ -494,22 +457,7 @@ export const getContainerColumns = (tableConfiguration: TableConfiguration) => [
494
457
  columnHelper.accessor('data.name', {
495
458
  id: 'name',
496
459
  header: () => <span>{tableConfiguration?.columns?.name?.label || 'Data'}</span>,
497
- cell: (info) => {
498
- const item = info.row.original;
499
- const isLatestVersion = item.data.version === item.data.latestVersion;
500
- return (
501
- <a
502
- href={buildUrl(`/docs/${item.collection}/${item.data.id}/${item.data.version}`)}
503
- className="group inline-flex items-center gap-2 hover:text-[rgb(var(--ec-accent))] transition-colors"
504
- >
505
- <DatabaseIcon className="h-4 w-4 text-blue-500 flex-shrink-0" />
506
- <span className="text-sm font-semibold text-[rgb(var(--ec-page-text))] group-hover:text-[rgb(var(--ec-accent))]">
507
- {item.data.name}
508
- </span>
509
- {!isLatestVersion && <span className="text-xs text-[rgb(var(--ec-icon-color))]">v{item.data.version}</span>}
510
- </a>
511
- );
512
- },
460
+ cell: (info) => <ResourceNameCell item={info.row.original} />,
513
461
  meta: {
514
462
  filterVariant: 'name',
515
463
  },
@@ -552,22 +500,7 @@ export const getDataProductColumns = (tableConfiguration: TableConfiguration) =>
552
500
  columnHelper.accessor('data.name', {
553
501
  id: 'name',
554
502
  header: () => <span>{tableConfiguration?.columns?.name?.label || 'Data Product'}</span>,
555
- cell: (info) => {
556
- const item = info.row.original;
557
- const isLatestVersion = item.data.version === item.data.latestVersion;
558
- return (
559
- <a
560
- href={buildUrl(`/docs/${item.collection}/${item.data.id}/${item.data.version}`)}
561
- className="group inline-flex items-center gap-2 hover:text-[rgb(var(--ec-accent))] transition-colors"
562
- >
563
- <CubeIcon className="h-4 w-4 text-cyan-500 flex-shrink-0" />
564
- <span className="text-sm font-semibold text-[rgb(var(--ec-page-text))] group-hover:text-[rgb(var(--ec-accent))]">
565
- {item.data.name}
566
- </span>
567
- {!isLatestVersion && <span className="text-xs text-[rgb(var(--ec-icon-color))]">v{item.data.version}</span>}
568
- </a>
569
- );
570
- },
503
+ cell: (info) => <ResourceNameCell item={info.row.original} />,
571
504
  meta: {
572
505
  filterVariant: 'name',
573
506
  },