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

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 (65) 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-HGRFXTFC.js → chunk-M7OVJHPV.js} +1 -1
  6. package/dist/{chunk-KIDISU6X.js → chunk-PL3T5TWF.js} +1 -1
  7. package/dist/{chunk-62EV4IJA.js → chunk-Q7FBYUCZ.js} +1 -1
  8. package/dist/{chunk-4J2ZM2HX.js → chunk-TOMV5YJH.js} +1 -1
  9. package/dist/{chunk-XARPIXWF.js → chunk-VA77ENFQ.js} +1 -1
  10. package/dist/constants.cjs +1 -1
  11. package/dist/constants.js +1 -1
  12. package/dist/eventcatalog.cjs +1 -1
  13. package/dist/eventcatalog.js +5 -5
  14. package/dist/generate.cjs +1 -1
  15. package/dist/generate.js +3 -3
  16. package/dist/runtime-dependencies/manifest.json +0 -1
  17. package/dist/utils/cli-logger.cjs +1 -1
  18. package/dist/utils/cli-logger.js +2 -2
  19. package/eventcatalog/src/components/Grids/utils.tsx +0 -23
  20. package/eventcatalog/src/components/MDX/CodeGroup/CodeGroup.tsx +6 -1
  21. package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsPanel.tsx +12 -4
  22. package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +16 -7
  23. package/eventcatalog/src/components/SideNav/NestedSideBar/utils.ts +10 -0
  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/eventcatalog/src/utils/git.ts +11 -5
  56. package/package.json +8 -10
  57. package/dist/runtime-dependencies/shelljs.mjs +0 -3
  58. package/eventcatalog/src/components/Grids/DomainGrid.tsx +0 -838
  59. package/eventcatalog/src/components/Grids/MessageGrid.tsx +0 -344
  60. package/eventcatalog/src/components/Grids/SystemGrid.tsx +0 -185
  61. package/eventcatalog/src/components/Grids/components.tsx +0 -170
  62. package/eventcatalog/src/components/Grids/message-link.ts +0 -38
  63. package/eventcatalog/src/components/Tables/SystemResources/SystemResourcesTable.tsx +0 -294
  64. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_resources.spec.ts +0 -65
  65. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_resources.ts +0 -46
@@ -1,170 +0,0 @@
1
- import React from 'react';
2
- import {
3
- MagnifyingGlassIcon,
4
- ChevronLeftIcon,
5
- ChevronRightIcon,
6
- ChevronDoubleLeftIcon,
7
- ChevronDoubleRightIcon,
8
- } from '@heroicons/react/24/outline';
9
- import type { CollectionMessageTypes } from '@types';
10
- import { getCollectionStyles, type PaginationProps, type SearchBarProps, type TypeFilterProps } from './utils';
11
-
12
- export function SearchBar({ searchQuery, onSearchChange, placeholder, totalResults, totalItems }: SearchBarProps) {
13
- return (
14
- <div className="w-full md:w-96">
15
- <div className="relative">
16
- <div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
17
- <MagnifyingGlassIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
18
- </div>
19
- <input
20
- type="text"
21
- placeholder={placeholder || 'Search...'}
22
- value={searchQuery}
23
- onChange={(e) => onSearchChange(e.target.value)}
24
- className="block w-full rounded-lg border-0 py-2.5 pl-10 pr-4 text-gray-900 shadow-xs ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-primary sm:text-sm sm:leading-6"
25
- />
26
- {searchQuery && (
27
- <div className="absolute inset-y-0 right-0 flex items-center pr-3">
28
- <button onClick={() => onSearchChange('')} className="text-gray-400 hover:text-gray-500 focus:outline-hidden">
29
- <span className="sr-only">Clear search</span>
30
- <svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
31
- <path
32
- fillRule="evenodd"
33
- d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
34
- clipRule="evenodd"
35
- />
36
- </svg>
37
- </button>
38
- </div>
39
- )}
40
- </div>
41
- {searchQuery && totalResults !== undefined && totalItems !== undefined && (
42
- <div className="mt-2 text-sm text-gray-500 flex items-center justify-between">
43
- <span>
44
- Found <span className="font-medium text-gray-900">{totalResults}</span> of{' '}
45
- <span className="font-medium text-gray-900">{totalItems}</span>
46
- </span>
47
- <span className="text-gray-400 text-xs">ESC to clear</span>
48
- </div>
49
- )}
50
- </div>
51
- );
52
- }
53
-
54
- export function TypeFilters({ selectedTypes, onTypeChange, filteredCount, totalCount }: TypeFilterProps) {
55
- const types: CollectionMessageTypes[] = ['events', 'commands', 'queries'];
56
-
57
- return (
58
- <div className="flex items-center gap-2">
59
- {types.map((type) => {
60
- const { color, Icon } = getCollectionStyles(type);
61
- const isSelected = selectedTypes.includes(type);
62
- return (
63
- <button
64
- key={type}
65
- onClick={() => {
66
- onTypeChange(selectedTypes.includes(type) ? selectedTypes.filter((t) => t !== type) : [...selectedTypes, type]);
67
- }}
68
- className={`
69
- inline-flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium
70
- transition-colors duration-200
71
- ${
72
- isSelected
73
- ? `bg-${color}-100 text-${color}-700 ring-2 ring-${color}-500`
74
- : 'bg-gray-50 text-gray-600 hover:bg-gray-100'
75
- }
76
- `}
77
- >
78
- <Icon className={`h-4 w-4 ${isSelected ? `text-${color}-500` : 'text-gray-400'}`} />
79
- <span className="capitalize">{type}</span>
80
- {isSelected && filteredCount !== undefined && (
81
- <span
82
- className={`inline-flex items-center justify-center px-2 py-0.5 text-xs font-medium bg-${color}-50 text-${color}-700 rounded-full`}
83
- >
84
- {filteredCount}
85
- </span>
86
- )}
87
- </button>
88
- );
89
- })}
90
- {selectedTypes.length > 0 && (
91
- <button onClick={() => onTypeChange([])} className="text-xs text-gray-500 hover:text-gray-700 hover:underline">
92
- Clear filters
93
- </button>
94
- )}
95
- </div>
96
- );
97
- }
98
-
99
- export function Pagination({ currentPage, totalPages, totalItems, itemsPerPage, onPageChange }: PaginationProps) {
100
- if (totalPages <= 1) return null;
101
-
102
- return (
103
- <div className="flex items-center justify-between border-gray-200 bg-white px-4 py-3 sm:px-6">
104
- <div className="flex flex-1 justify-between sm:hidden">
105
- <button
106
- onClick={() => onPageChange(Math.max(1, currentPage - 1))}
107
- disabled={currentPage === 1}
108
- className="relative inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50"
109
- >
110
- Previous
111
- </button>
112
- <button
113
- onClick={() => onPageChange(Math.min(totalPages, currentPage + 1))}
114
- disabled={currentPage === totalPages}
115
- className="relative ml-3 inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50"
116
- >
117
- Next
118
- </button>
119
- </div>
120
- <div className="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
121
- <div className="pr-4">
122
- <p className="text-sm text-gray-700">
123
- Showing <span className="font-medium">{(currentPage - 1) * itemsPerPage + 1}</span> to{' '}
124
- <span className="font-medium">{Math.min(currentPage * itemsPerPage, totalItems)}</span> of{' '}
125
- <span className="font-medium">{totalItems}</span> results
126
- </p>
127
- </div>
128
- <div>
129
- <nav className="isolate inline-flex -space-x-px rounded-md shadow-xs" aria-label="Pagination">
130
- <button
131
- onClick={() => onPageChange(1)}
132
- disabled={currentPage === 1}
133
- className="relative inline-flex items-center rounded-l-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0 disabled:opacity-50"
134
- >
135
- <span className="sr-only">First</span>
136
- <ChevronDoubleLeftIcon className="h-5 w-5" aria-hidden="true" />
137
- </button>
138
- <button
139
- onClick={() => onPageChange(Math.max(1, currentPage - 1))}
140
- disabled={currentPage === 1}
141
- className="relative inline-flex items-center px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0 disabled:opacity-50"
142
- >
143
- <span className="sr-only">Previous</span>
144
- <ChevronLeftIcon className="h-5 w-5" aria-hidden="true" />
145
- </button>
146
- <span className="relative inline-flex items-center px-4 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 focus:outline-offset-0">
147
- Page {currentPage} of {totalPages}
148
- </span>
149
- <button
150
- onClick={() => onPageChange(Math.min(totalPages, currentPage + 1))}
151
- disabled={currentPage === totalPages}
152
- className="relative inline-flex items-center px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0 disabled:opacity-50"
153
- >
154
- <span className="sr-only">Next</span>
155
- <ChevronRightIcon className="h-5 w-5" aria-hidden="true" />
156
- </button>
157
- <button
158
- onClick={() => onPageChange(totalPages)}
159
- disabled={currentPage === totalPages}
160
- className="relative inline-flex items-center rounded-r-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0 disabled:opacity-50"
161
- >
162
- <span className="sr-only">Last</span>
163
- <ChevronDoubleRightIcon className="h-5 w-5" aria-hidden="true" />
164
- </button>
165
- </nav>
166
- </div>
167
- </div>
168
- </div>
169
- );
170
- }
@@ -1,38 +0,0 @@
1
- import type { CollectionMessageTypes } from '@types';
2
-
3
- export const MESSAGE_COLLECTIONS: readonly CollectionMessageTypes[] = ['events', 'commands', 'queries'];
4
-
5
- export function isMessageCollection(value: unknown): value is CollectionMessageTypes {
6
- return value === 'events' || value === 'commands' || value === 'queries';
7
- }
8
-
9
- export interface MessageLinkProps {
10
- collection?: CollectionMessageTypes;
11
- id?: string;
12
- name?: string;
13
- version: string;
14
- href?: string;
15
- }
16
-
17
- /**
18
- * Resolve a DomainGrid/SystemGrid receives/sends item into a docs link.
19
- *
20
- * Hydrated collection entries carry `collection` + `data.name`. Bare `{id, version}`
21
- * pointers do not — never guess `events` for those, or commands/queries get the wrong URL.
22
- */
23
- export function getMessageLinkProps(message: any): MessageLinkProps {
24
- const data = message?.data ?? message;
25
- const collectionCandidate = message?.collection ?? data?.collection;
26
- const collection = isMessageCollection(collectionCandidate) ? collectionCandidate : undefined;
27
- const id = data?.id ?? message?.id;
28
- const name = data?.name || message?.name || id;
29
- const version = data?.version ?? message?.version ?? 'latest';
30
-
31
- return {
32
- collection,
33
- id,
34
- name,
35
- version,
36
- href: collection && id ? `/docs/${collection}/${id}/${version}` : undefined,
37
- };
38
- }
@@ -1,294 +0,0 @@
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 =
16
- | 'domains'
17
- | 'systems'
18
- | 'agents'
19
- | 'services'
20
- | 'flows'
21
- | 'entities'
22
- | 'data-products'
23
- | 'containers'
24
- | 'events'
25
- | 'commands'
26
- | 'queries';
27
-
28
- export interface SystemResourceItem {
29
- collection: SystemResourceCollection;
30
- id: string;
31
- name: string;
32
- version: string;
33
- summary?: string;
34
- icon?: string;
35
- href: string;
36
- }
37
-
38
- interface SystemResourcesTableProps {
39
- resources: SystemResourceItem[];
40
- }
41
-
42
- const TYPE_LABELS: Record<SystemResourceCollection, string> = {
43
- domains: 'Subdomains',
44
- systems: 'Systems',
45
- agents: 'Agents',
46
- services: 'Services',
47
- flows: 'Flows',
48
- entities: 'Entities',
49
- 'data-products': 'Data Products',
50
- containers: 'Data Stores',
51
- events: 'Events',
52
- commands: 'Commands',
53
- queries: 'Queries',
54
- };
55
-
56
- // Order the type filter pills follow on the page.
57
- const TYPE_ORDER: SystemResourceCollection[] = [
58
- 'domains',
59
- 'systems',
60
- 'agents',
61
- 'services',
62
- 'flows',
63
- 'entities',
64
- 'data-products',
65
- 'containers',
66
- 'events',
67
- 'commands',
68
- 'queries',
69
- ];
70
-
71
- // Small icon for a collection type (used in filter pills and the type column).
72
- const CollectionTypeIcon = ({ collection, className }: { collection: SystemResourceCollection; className?: string }) => {
73
- const { color, Icon } = getColorAndIconForCollection(collection);
74
- const colorClass = getCollectionTextColorClass(color, 'text-[rgb(var(--ec-icon-color))]');
75
- return <Icon className={`${className ?? 'h-3.5 w-3.5'} ${colorClass}`} aria-hidden="true" />;
76
- };
77
-
78
- const ResourceIcon = ({ item }: { item: SystemResourceItem }) => {
79
- if (item.icon && isIconPath(item.icon)) {
80
- return <img src={resolveIconUrl(item.icon)} alt="" className="h-5 w-5" loading="lazy" />;
81
- }
82
- return <CollectionTypeIcon collection={item.collection} className="h-5 w-5" />;
83
- };
84
-
85
- const columnHelper = createColumnHelper<SystemResourceItem>();
86
-
87
- const SortIcon = ({ direction }: { direction: false | 'asc' | 'desc' }) => {
88
- if (direction === 'asc') return <ArrowUp className="h-3.5 w-3.5" aria-hidden="true" />;
89
- if (direction === 'desc') return <ArrowDown className="h-3.5 w-3.5" aria-hidden="true" />;
90
- return <ArrowUpDown className="h-3.5 w-3.5 opacity-40" aria-hidden="true" />;
91
- };
92
-
93
- export function SystemResourcesTable({ resources }: SystemResourcesTableProps) {
94
- const [search, setSearch] = useState('');
95
- const [selectedTypes, setSelectedTypes] = useState<SystemResourceCollection[]>([]);
96
- const [sorting, setSorting] = useState<SortingState>([{ id: 'name', desc: false }]);
97
-
98
- // Only show type filters that actually have resources in this system.
99
- const availableTypes = useMemo(
100
- () => TYPE_ORDER.filter((type) => resources.some((resource) => resource.collection === type)),
101
- [resources]
102
- );
103
-
104
- const filtered = useMemo(() => {
105
- const query = search.trim().toLowerCase();
106
- return resources.filter((resource) => {
107
- if (selectedTypes.length > 0 && !selectedTypes.includes(resource.collection)) {
108
- return false;
109
- }
110
- if (query) {
111
- const haystack = `${resource.name} ${resource.id} ${resource.summary ?? ''}`.toLowerCase();
112
- if (!haystack.includes(query)) return false;
113
- }
114
- return true;
115
- });
116
- }, [resources, search, selectedTypes]);
117
-
118
- const columns = useMemo(
119
- () => [
120
- columnHelper.accessor('name', {
121
- header: 'Name',
122
- cell: (info) => {
123
- const resource = info.row.original;
124
- return (
125
- <a href={resource.href} className="group flex items-center gap-3">
126
- <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))]">
127
- <ResourceIcon item={resource} />
128
- </span>
129
- <span className="text-sm font-medium text-[rgb(var(--ec-page-text))] group-hover:text-[rgb(var(--ec-accent))]">
130
- {resource.name}
131
- </span>
132
- </a>
133
- );
134
- },
135
- }),
136
- columnHelper.accessor((row) => TYPE_LABELS[row.collection], {
137
- id: 'type',
138
- header: 'Type',
139
- cell: (info) => {
140
- const { collection } = info.row.original;
141
- return (
142
- <span className="inline-flex items-center gap-1.5 text-sm text-[rgb(var(--ec-page-text-muted))]">
143
- <CollectionTypeIcon collection={collection} />
144
- {TYPE_LABELS[collection]}
145
- </span>
146
- );
147
- },
148
- }),
149
- columnHelper.accessor('version', {
150
- header: 'Version',
151
- cell: (info) => (
152
- <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))]">
153
- v{info.getValue()}
154
- </span>
155
- ),
156
- }),
157
- columnHelper.accessor((row) => row.summary ?? '', {
158
- id: 'summary',
159
- header: 'Summary',
160
- enableSorting: false,
161
- cell: (info) => (
162
- <span className="line-clamp-2 text-sm text-[rgb(var(--ec-page-text-muted))]">{info.getValue() || '-'}</span>
163
- ),
164
- }),
165
- ],
166
- []
167
- );
168
-
169
- const table = useReactTable({
170
- data: filtered,
171
- columns,
172
- state: { sorting },
173
- onSortingChange: setSorting,
174
- getCoreRowModel: getCoreRowModel(),
175
- getSortedRowModel: getSortedRowModel(),
176
- });
177
-
178
- const toggleType = (type: SystemResourceCollection) => {
179
- setSelectedTypes((prev) => (prev.includes(type) ? prev.filter((t) => t !== type) : [...prev, type]));
180
- };
181
-
182
- return (
183
- <div className="flex flex-col gap-4">
184
- {/* Controls: search + type filters */}
185
- <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
186
- <div className="relative w-full sm:max-w-xs">
187
- <Search
188
- className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-[rgb(var(--ec-icon-color))]"
189
- aria-hidden="true"
190
- />
191
- <input
192
- type="text"
193
- value={search}
194
- onChange={(event) => setSearch(event.target.value)}
195
- placeholder="Search resources..."
196
- 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)]"
197
- />
198
- </div>
199
-
200
- {availableTypes.length > 1 && (
201
- <div className="flex flex-wrap gap-2">
202
- {availableTypes.map((type) => {
203
- const active = selectedTypes.includes(type);
204
- return (
205
- <button
206
- key={type}
207
- type="button"
208
- onClick={() => toggleType(type)}
209
- className={[
210
- 'inline-flex items-center gap-1.5 rounded-full border px-3 py-1 text-xs font-medium transition-colors',
211
- active
212
- ? 'border-[rgb(var(--ec-accent))] bg-[rgb(var(--ec-accent-subtle))] text-[rgb(var(--ec-accent))]'
213
- : 'border-[rgb(var(--ec-page-border))] text-[rgb(var(--ec-page-text-muted))] hover:text-[rgb(var(--ec-page-text))]',
214
- ].join(' ')}
215
- >
216
- <CollectionTypeIcon collection={type} />
217
- {TYPE_LABELS[type]}
218
- </button>
219
- );
220
- })}
221
- </div>
222
- )}
223
- </div>
224
-
225
- {/* Table */}
226
- <div className="overflow-hidden rounded-lg border border-[rgb(var(--ec-page-border))]">
227
- <table className="min-w-full divide-y divide-[rgb(var(--ec-page-border))]">
228
- <thead className="bg-[rgb(var(--ec-card-bg))]">
229
- {table.getHeaderGroups().map((headerGroup) => (
230
- <tr key={headerGroup.id}>
231
- {headerGroup.headers.map((header) => {
232
- const canSort = header.column.getCanSort();
233
- const isSummary = header.column.id === 'summary';
234
- return (
235
- <th
236
- key={header.id}
237
- className={[
238
- 'px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-[rgb(var(--ec-page-text-muted))]',
239
- isSummary ? 'hidden md:table-cell' : '',
240
- ].join(' ')}
241
- >
242
- {canSort ? (
243
- <button
244
- type="button"
245
- onClick={header.column.getToggleSortingHandler()}
246
- className="inline-flex items-center gap-1.5 uppercase tracking-wider hover:text-[rgb(var(--ec-page-text))]"
247
- >
248
- {flexRender(header.column.columnDef.header, header.getContext())}
249
- <SortIcon direction={header.column.getIsSorted()} />
250
- </button>
251
- ) : (
252
- flexRender(header.column.columnDef.header, header.getContext())
253
- )}
254
- </th>
255
- );
256
- })}
257
- </tr>
258
- ))}
259
- </thead>
260
- <tbody className="divide-y divide-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-page-bg))]">
261
- {table.getRowModel().rows.length === 0 && (
262
- <tr>
263
- <td colSpan={columns.length} className="px-4 py-12">
264
- <div className="flex flex-col items-center justify-center gap-2 text-center text-[rgb(var(--ec-page-text-muted))]">
265
- <SearchX className="h-6 w-6" aria-hidden="true" />
266
- <p className="text-sm">No resources found.</p>
267
- </div>
268
- </td>
269
- </tr>
270
- )}
271
- {table.getRowModel().rows.map((row) => (
272
- <tr key={row.id} className="transition-colors hover:bg-[rgb(var(--ec-card-bg))]">
273
- {row.getVisibleCells().map((cell) => {
274
- const isSummary = cell.column.id === 'summary';
275
- return (
276
- <td key={cell.id} className={['px-4 py-3', isSummary ? 'hidden max-w-md md:table-cell' : ''].join(' ')}>
277
- {flexRender(cell.column.columnDef.cell, cell.getContext())}
278
- </td>
279
- );
280
- })}
281
- </tr>
282
- ))}
283
- </tbody>
284
- </table>
285
- </div>
286
-
287
- <p className="text-xs text-[rgb(var(--ec-page-text-muted))]">
288
- Showing {filtered.length} of {resources.length} resources
289
- </p>
290
- </div>
291
- );
292
- }
293
-
294
- export default SystemResourcesTable;
@@ -1,65 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { getResourceGroups, hasResources } from './_resources';
3
-
4
- const resource = (id: string, version = '1.0.0') => ({ data: { id, name: id, version } });
5
- const message = (collection: 'events' | 'commands' | 'queries', id: string, version = '1.0.0') => ({
6
- collection,
7
- data: { id, name: id, version },
8
- });
9
-
10
- describe('resource page groups', () => {
11
- it('includes every resource directly attached to a domain', () => {
12
- const sharedEvent = message('events', 'OrderPlaced');
13
- const groups = getResourceGroups(
14
- {
15
- domains: [resource('Fulfilment')],
16
- systems: [resource('OrderingSystem')],
17
- agents: [resource('OrderAgent')],
18
- 'data-products': [resource('OrderAnalytics')],
19
- services: [resource('OrdersService')],
20
- flows: [resource('PlaceOrder')],
21
- entities: [resource('Order')],
22
- sends: [sharedEvent, message('commands', 'PlaceOrder')],
23
- receives: [sharedEvent, message('queries', 'GetOrder')],
24
- },
25
- 'domains'
26
- );
27
-
28
- expect(Object.fromEntries(groups.map(({ collection, items }) => [collection, items.map((item) => item.data.id)]))).toEqual({
29
- domains: ['Fulfilment'],
30
- systems: ['OrderingSystem'],
31
- agents: ['OrderAgent'],
32
- 'data-products': ['OrderAnalytics'],
33
- services: ['OrdersService'],
34
- flows: ['PlaceOrder'],
35
- entities: ['Order'],
36
- containers: [],
37
- events: ['OrderPlaced'],
38
- commands: ['PlaceOrder'],
39
- queries: ['GetOrder'],
40
- });
41
- });
42
-
43
- it('keeps domain-only resources off system resource pages', () => {
44
- const collections = getResourceGroups(
45
- {
46
- domains: [resource('Fulfilment')],
47
- systems: [resource('OrderingSystem')],
48
- agents: [resource('OrderAgent')],
49
- 'data-products': [resource('OrderAnalytics')],
50
- services: [resource('OrdersService')],
51
- containers: [resource('OrdersDatabase')],
52
- },
53
- 'systems'
54
- ).map(({ collection }) => collection);
55
-
56
- expect(collections).toEqual(['services', 'flows', 'entities', 'containers', 'events', 'commands', 'queries']);
57
- });
58
-
59
- it.each(['domains', 'systems', 'agents', 'data-products'] as const)(
60
- 'makes a domain with only %s eligible for a Resources page',
61
- (collection) => {
62
- expect(hasResources({ data: { [collection]: [resource('direct-resource')] } }, 'domains')).toBe(true);
63
- }
64
- );
65
- });
@@ -1,46 +0,0 @@
1
- import type { SystemResourceCollection } from '@components/Tables/SystemResources/SystemResourcesTable';
2
-
3
- export type ResourceOwnerCollection = 'systems' | 'domains';
4
-
5
- export interface ResourceGroup {
6
- collection: SystemResourceCollection;
7
- items: any[];
8
- }
9
-
10
- const getDomainMessages = (data: any) => {
11
- const seen = new Map<string, any>();
12
-
13
- for (const message of [...(data.sends || []), ...(data.receives || [])]) {
14
- if (!message?.data?.id || !message?.collection) continue;
15
- seen.set(`${message.collection}:${message.data.id}:${message.data.version}`, message);
16
- }
17
-
18
- return Array.from(seen.values());
19
- };
20
-
21
- export const getResourceGroups = (data: any, ownerCollection: ResourceOwnerCollection): ResourceGroup[] => {
22
- const domainMessages = ownerCollection === 'domains' ? getDomainMessages(data) : [];
23
- const domainOnlyGroups: ResourceGroup[] =
24
- ownerCollection === 'domains'
25
- ? [
26
- { collection: 'domains', items: data.domains || [] },
27
- { collection: 'systems', items: data.systems || [] },
28
- { collection: 'agents', items: data.agents || [] },
29
- { collection: 'data-products', items: data['data-products'] || [] },
30
- ]
31
- : [];
32
-
33
- return [
34
- ...domainOnlyGroups,
35
- { collection: 'services', items: data.services || [] },
36
- { collection: 'flows', items: data.flows || [] },
37
- { collection: 'entities', items: data.entities || [] },
38
- { collection: 'containers', items: data.containers || [] },
39
- { collection: 'events', items: domainMessages.filter((message) => message.collection === 'events') },
40
- { collection: 'commands', items: domainMessages.filter((message) => message.collection === 'commands') },
41
- { collection: 'queries', items: domainMessages.filter((message) => message.collection === 'queries') },
42
- ];
43
- };
44
-
45
- export const hasResources = (owner: any, ownerCollection: ResourceOwnerCollection): boolean =>
46
- getResourceGroups(owner?.data ?? {}, ownerCollection).some(({ items }) => items.length > 0);