@eventcatalog/core 2.31.5 → 2.32.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 (26) 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-NPIJWM72.js → chunk-EIEK445B.js} +1 -1
  6. package/dist/{chunk-45XUM7UR.js → chunk-NPZQE3LM.js} +1 -1
  7. package/dist/{chunk-VCKSLDEL.js → chunk-YXHR3YSA.js} +1 -1
  8. package/dist/constants.cjs +1 -1
  9. package/dist/constants.js +1 -1
  10. package/dist/eventcatalog.cjs +1 -1
  11. package/dist/eventcatalog.js +3 -3
  12. package/eventcatalog/src/components/Lists/CustomSideBarSectionList.astro +67 -0
  13. package/eventcatalog/src/components/MDX/MessageTable/MessageTable.client.tsx +2 -2
  14. package/eventcatalog/src/components/MDX/ResourceGroupTable/ResourceGroupTable.astro +126 -0
  15. package/eventcatalog/src/components/MDX/ResourceGroupTable/ResourceGroupTable.client.tsx +408 -0
  16. package/eventcatalog/src/components/MDX/components.tsx +2 -0
  17. package/eventcatalog/src/components/SideBars/DomainSideBar.astro +8 -0
  18. package/eventcatalog/src/components/SideBars/FlowSideBar.astro +8 -1
  19. package/eventcatalog/src/components/SideBars/MessageSideBar.astro +8 -0
  20. package/eventcatalog/src/components/SideBars/ServiceSideBar.astro +8 -1
  21. package/eventcatalog/src/components/Tables/columns/ServiceTableColumns.tsx +4 -6
  22. package/eventcatalog/src/components/Tables/columns/TeamsTableColumns.tsx +0 -15
  23. package/eventcatalog/src/content.config.ts +17 -0
  24. package/eventcatalog/src/utils/collections/icons.ts +29 -0
  25. package/eventcatalog/src/utils/collections/services.ts +4 -4
  26. package/package.json +1 -1
@@ -37,7 +37,7 @@ var import_axios = __toESM(require("axios"), 1);
37
37
  var import_os = __toESM(require("os"), 1);
38
38
 
39
39
  // package.json
40
- var version = "2.31.5";
40
+ var version = "2.32.0";
41
41
 
42
42
  // src/constants.ts
43
43
  var VERSION = version;
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  raiseEvent
3
- } from "../chunk-45XUM7UR.js";
4
- import "../chunk-VCKSLDEL.js";
3
+ } from "../chunk-NPZQE3LM.js";
4
+ import "../chunk-YXHR3YSA.js";
5
5
  export {
6
6
  raiseEvent
7
7
  };
@@ -106,7 +106,7 @@ var import_axios = __toESM(require("axios"), 1);
106
106
  var import_os = __toESM(require("os"), 1);
107
107
 
108
108
  // package.json
109
- var version = "2.31.5";
109
+ var version = "2.32.0";
110
110
 
111
111
  // src/constants.ts
112
112
  var VERSION = version;
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  log_build_default
3
- } from "../chunk-NPIJWM72.js";
4
- import "../chunk-45XUM7UR.js";
5
- import "../chunk-VCKSLDEL.js";
3
+ } from "../chunk-EIEK445B.js";
4
+ import "../chunk-NPZQE3LM.js";
5
+ import "../chunk-YXHR3YSA.js";
6
6
  import "../chunk-E7TXTI7G.js";
7
7
  export {
8
8
  log_build_default as default
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  raiseEvent
3
- } from "./chunk-45XUM7UR.js";
3
+ } from "./chunk-NPZQE3LM.js";
4
4
  import {
5
5
  getEventCatalogConfigFile,
6
6
  verifyRequiredFieldsAreInCatalogConfigFile
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  VERSION
3
- } from "./chunk-VCKSLDEL.js";
3
+ } from "./chunk-YXHR3YSA.js";
4
4
 
5
5
  // src/analytics/analytics.js
6
6
  import axios from "axios";
@@ -1,5 +1,5 @@
1
1
  // package.json
2
- var version = "2.31.5";
2
+ var version = "2.32.0";
3
3
 
4
4
  // src/constants.ts
5
5
  var VERSION = version;
@@ -25,7 +25,7 @@ __export(constants_exports, {
25
25
  module.exports = __toCommonJS(constants_exports);
26
26
 
27
27
  // package.json
28
- var version = "2.31.5";
28
+ var version = "2.32.0";
29
29
 
30
30
  // src/constants.ts
31
31
  var VERSION = version;
package/dist/constants.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  VERSION
3
- } from "./chunk-VCKSLDEL.js";
3
+ } from "./chunk-YXHR3YSA.js";
4
4
  export {
5
5
  VERSION
6
6
  };
@@ -157,7 +157,7 @@ var import_axios = __toESM(require("axios"), 1);
157
157
  var import_os = __toESM(require("os"), 1);
158
158
 
159
159
  // package.json
160
- var version = "2.31.5";
160
+ var version = "2.32.0";
161
161
 
162
162
  // src/constants.ts
163
163
  var VERSION = version;
@@ -6,15 +6,15 @@ import {
6
6
  } from "./chunk-UKJ7F5WR.js";
7
7
  import {
8
8
  log_build_default
9
- } from "./chunk-NPIJWM72.js";
10
- import "./chunk-45XUM7UR.js";
9
+ } from "./chunk-EIEK445B.js";
10
+ import "./chunk-NPZQE3LM.js";
11
11
  import {
12
12
  catalogToAstro,
13
13
  checkAndConvertMdToMdx
14
14
  } from "./chunk-7SI5EVOX.js";
15
15
  import {
16
16
  VERSION
17
- } from "./chunk-VCKSLDEL.js";
17
+ } from "./chunk-YXHR3YSA.js";
18
18
  import {
19
19
  isBackstagePluginEnabled,
20
20
  isEventCatalogProEnabled
@@ -0,0 +1,67 @@
1
+ ---
2
+ import { buildUrl } from '@utils/url-builder';
3
+ import { getCollection } from 'astro:content';
4
+ import { getItemsFromCollectionByIdAndSemverOrLatest } from '@utils/collections/util';
5
+ import PillListFlat from './PillListFlat';
6
+ interface Props {
7
+ section?: {
8
+ title?: string;
9
+ limit?: number;
10
+ items: {
11
+ id: string;
12
+ type: string;
13
+ version?: string;
14
+ }[];
15
+ };
16
+ }
17
+
18
+ const { section } = Astro.props;
19
+ const title = section?.title || 'Custom Section';
20
+ const limit = section?.limit || 10;
21
+ const sectionItems = section?.items || [];
22
+
23
+ // Type-safe mapping of resource types to collection names
24
+ const resourceToCollectionMap = {
25
+ service: 'services',
26
+ event: 'events',
27
+ command: 'commands',
28
+ query: 'queries',
29
+ domain: 'domains',
30
+ flow: 'flows',
31
+ channel: 'channels',
32
+ user: 'users',
33
+ team: 'teams',
34
+ } as const; // Make this a const assertion
35
+
36
+ // Array to store resolved related resources
37
+ const resolvedResources = [];
38
+
39
+ // Fetch related resources
40
+ for (const resource of sectionItems) {
41
+ try {
42
+ // Use type assertion to ensure TypeScript understands this is a valid collection name
43
+ const collectionName = resourceToCollectionMap[resource.type as keyof typeof resourceToCollectionMap];
44
+
45
+ // Use getEntry instead of getCollection for single item lookup by ID
46
+ const allItemsInCollection = await getCollection(collectionName);
47
+ // @ts-ignore
48
+ const entryToMatchVersion = getItemsFromCollectionByIdAndSemverOrLatest(allItemsInCollection, resource.id, resource.version);
49
+ const entry = entryToMatchVersion[0];
50
+
51
+ if (entry) {
52
+ resolvedResources.push(entry);
53
+ }
54
+ } catch (error) {
55
+ console.error(`Failed to fetch related resource: ${resource.id} of type ${resource.type}`, error);
56
+ }
57
+ }
58
+
59
+ const sectionList = resolvedResources.map((p: any) => ({
60
+ label: `${p.data.name}`,
61
+ tag: `v${p.data.version}`,
62
+ collection: p.collection,
63
+ href: buildUrl(`/docs/${p.collection}/${p.data.id}/${p.data.version}`),
64
+ }));
65
+ ---
66
+
67
+ <PillListFlat title={`${title} (${sectionList.length})`} pills={sectionList} color="orange" limit={limit} client:load />
@@ -1,4 +1,4 @@
1
- import { getColorAndIconForMessageType } from '@components/Tables/columns/MessageTableColumns';
1
+ import { getColorAndIconForCollection } from '@utils/collections/icons';
2
2
  import { buildUrl } from '@utils/url-builder';
3
3
  import { useState, useMemo, useCallback, memo } from 'react';
4
4
 
@@ -25,7 +25,7 @@ type MessageType = 'event' | 'query' | 'command' | null;
25
25
 
26
26
  const MessageRow = memo(
27
27
  ({ message, showChannels, collection }: { message: MessageTableMessage; showChannels?: boolean; collection: string }) => {
28
- const { color, Icon } = getColorAndIconForMessageType(message.type);
28
+ const { color, Icon } = getColorAndIconForCollection(message.collection);
29
29
  const url = buildUrl(`/docs/${collection}/${message.id}/${message.version}`);
30
30
  let type = collection.slice(0, -1);
31
31
  type = type === 'querie' ? 'query' : type;
@@ -0,0 +1,126 @@
1
+ ---
2
+ import type { CollectionEntry } from 'astro:content';
3
+ import { getCollection } from 'astro:content';
4
+ import { getItemsFromCollectionByIdAndSemverOrLatest } from '@utils/collections/util';
5
+ import ResourceGroupTableClient from './ResourceGroupTable.client';
6
+ import Admonition from '../Admonition';
7
+
8
+ export interface Props extends CollectionEntry<'services'> {
9
+ limit?: number;
10
+ id: string;
11
+ showTags?: boolean;
12
+ showOwners?: boolean;
13
+ title?: string;
14
+ subtitle?: string;
15
+ description?: string;
16
+ }
17
+
18
+ const {
19
+ id,
20
+ limit,
21
+ showTags = false,
22
+ showOwners = false,
23
+ title = 'Resources',
24
+ subtitle,
25
+ description = 'Browse resources in this group or search by name, type, or description.',
26
+ ...resource
27
+ } = Astro.props;
28
+
29
+ // Find the requested resource group section by ID
30
+ const section = resource.data.resourceGroups?.find((section: any) => section.id === id);
31
+
32
+ const collection = Astro.props.collection as 'services' | 'domains';
33
+
34
+ // Type-safe mapping of resource types to collection names
35
+ const resourceToCollectionMap = {
36
+ service: 'services',
37
+ event: 'events',
38
+ command: 'commands',
39
+ query: 'queries',
40
+ domain: 'domains',
41
+ flow: 'flows',
42
+ channel: 'channels',
43
+ user: 'users',
44
+ team: 'teams',
45
+ } as const;
46
+
47
+ // This will hold our processed resources for the client component
48
+ const resolvedResources: any[] = [];
49
+
50
+ // Process each resource item in the section
51
+ if (section?.items) {
52
+ for (const item of section.items) {
53
+ // Get the collection name from the resource type
54
+ const collectionName = resourceToCollectionMap[item.type as keyof typeof resourceToCollectionMap];
55
+
56
+ if (!collectionName) continue;
57
+
58
+ try {
59
+ // Get all items in the collection
60
+ const allItemsInCollection = await getCollection(collectionName);
61
+
62
+ // Find the specific version of the resource
63
+ const matchedItems = getItemsFromCollectionByIdAndSemverOrLatest(allItemsInCollection as any, item.id, item.version);
64
+
65
+ if (matchedItems.length > 0) {
66
+ const entry = matchedItems[0] as any;
67
+
68
+ // Create a simplified resource object for the client component
69
+ resolvedResources.push({
70
+ id: entry.data?.id || entry.id || item.id,
71
+ name: entry.data?.name || item.id,
72
+ version: entry.data?.version || item.version || '1.0.0',
73
+ collection: entry.collection || collectionName,
74
+ type: item.type,
75
+ summary: entry.data?.summary || '',
76
+ description: entry.data?.description || '',
77
+ owners: entry.data?.owners || [],
78
+ tags: entry.data?.tags || [],
79
+ });
80
+ }
81
+ } catch (error) {
82
+ console.error(`Error processing resource ${item.id} of type ${item.type}:`, error);
83
+ }
84
+ }
85
+ }
86
+ ---
87
+
88
+ {
89
+ resolvedResources.length > 0 && (
90
+ <ResourceGroupTableClient
91
+ client:load
92
+ resources={resolvedResources}
93
+ limit={limit}
94
+ title={title}
95
+ subtitle={subtitle}
96
+ description={description}
97
+ showTags={showTags}
98
+ showOwners={showOwners}
99
+ />
100
+ )
101
+ }
102
+
103
+ {
104
+ !resolvedResources.length && (
105
+ <Admonition type="warning">
106
+ <div>
107
+ {`<ResourceGroupTable/>`} cannot find any resources in your catalog for the resource group with id {id}.
108
+ </div>
109
+ </Admonition>
110
+ )
111
+ }
112
+
113
+ {
114
+ !section && (
115
+ <Admonition type="warning">
116
+ <div>
117
+ <span class="font-bold">
118
+ {`<ResourceGroupTable/>`} cannot find the resource group with id {id}.
119
+ </span>
120
+ <span class="block">
121
+ Please make sure the id is correct and the resource group is defined in this {collection.slice(0, -1)}.
122
+ </span>
123
+ </div>
124
+ </Admonition>
125
+ )
126
+ }
@@ -0,0 +1,408 @@
1
+ import { getColorAndIconForCollection } from '@utils/collections/icons';
2
+ import { buildUrl } from '@utils/url-builder';
3
+ import { useState, useMemo, useCallback, memo } from 'react';
4
+
5
+ type Resource = {
6
+ id: string;
7
+ name: string;
8
+ version: string;
9
+ collection: string;
10
+ type: string;
11
+ summary?: string;
12
+ description?: string;
13
+ owners?: any[];
14
+ tags?: string[];
15
+ };
16
+
17
+ type ResourceGroupTableProps = {
18
+ resources: Resource[];
19
+ limit?: number;
20
+ showTags?: boolean;
21
+ showOwners?: boolean;
22
+ title: string;
23
+ subtitle: string;
24
+ description: string;
25
+ };
26
+
27
+ type ResourceType = 'service' | 'event' | 'query' | 'command' | 'domain' | 'flow' | 'channel' | 'user' | 'team' | null;
28
+
29
+ const ResourceRow = memo(
30
+ ({ resource, showTags, showOwners }: { resource: Resource; showTags?: boolean; showOwners?: boolean }) => {
31
+ const { color, Icon } = getColorAndIconForCollection(resource.collection);
32
+ const url = buildUrl(`/docs/${resource.collection}/${resource.id}/${resource.version}`);
33
+ let type = resource.collection.slice(0, -1);
34
+ type = type === 'querie' ? 'query' : type;
35
+
36
+ const tags = resource.tags || [];
37
+ const owners = resource.owners || [];
38
+
39
+ return (
40
+ <tr className="group hover:bg-gray-100">
41
+ <td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6 relative">
42
+ <a href={url} className="absolute inset-0 z-10" aria-label={`View details for ${resource.name}`} />
43
+ <div className="flex items-center gap-2 relative">
44
+ <Icon className={`h-5 w-5 text-${color}-500`} />
45
+ <span className="group-hover:text-blue-600 break-all">{resource.name}</span>
46
+ </div>
47
+ </td>
48
+ <td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500 relative">
49
+ <a href={url} className="absolute inset-0 z-10" aria-hidden="true" />
50
+ <span>v{resource.version}</span>
51
+ </td>
52
+ <td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500 relative">
53
+ <a href={url} className="absolute inset-0 z-10" aria-hidden="true" />
54
+ <span>{type}</span>
55
+ </td>
56
+ <td className="px-3 py-4 text-sm text-gray-500 relative">
57
+ <a href={url} className="absolute inset-0 z-10" aria-hidden="true" />
58
+ <span className="line-clamp-2 break-words">{resource.summary || resource.description || '-'}</span>
59
+ </td>
60
+ {showTags && (
61
+ <td className="px-3 py-4 text-sm text-gray-500 relative">
62
+ <a href={url} className="absolute inset-0 z-10" aria-hidden="true" />
63
+ <div className="flex flex-wrap gap-1">
64
+ {tags.length > 0
65
+ ? tags.map((tag, index) => (
66
+ <span
67
+ key={`${tag}-${index}`}
68
+ className="inline-flex items-center rounded-md bg-gray-50 px-2 py-1 text-xs font-medium text-gray-600 ring-1 ring-inset ring-gray-500/10"
69
+ >
70
+ {tag}
71
+ </span>
72
+ ))
73
+ : '-'}
74
+ </div>
75
+ </td>
76
+ )}
77
+ {showOwners && (
78
+ <td className="px-3 py-4 text-sm text-gray-500 relative">
79
+ <a href={url} className="absolute inset-0 z-10" aria-hidden="true" />
80
+ <div className="flex flex-wrap gap-1">
81
+ {owners.length > 0
82
+ ? owners.map((owner, index) => (
83
+ <span
84
+ key={`${owner.id || owner.name}-${index}`}
85
+ className="inline-flex items-center rounded-md bg-gray-50 px-2 py-1 text-xs font-medium text-gray-600 ring-1 ring-inset ring-gray-500/10"
86
+ >
87
+ {owner.name || owner.id}
88
+ </span>
89
+ ))
90
+ : '-'}
91
+ </div>
92
+ </td>
93
+ )}
94
+ </tr>
95
+ );
96
+ }
97
+ );
98
+
99
+ const FilterButton = memo(
100
+ ({
101
+ type,
102
+ label,
103
+ typeFilter,
104
+ setTypeFilter,
105
+ setCurrentPage,
106
+ count,
107
+ }: {
108
+ type: ResourceType;
109
+ label: string;
110
+ typeFilter: ResourceType;
111
+ setTypeFilter: (type: ResourceType) => void;
112
+ setCurrentPage: (page: number) => void;
113
+ count: number;
114
+ }) => (
115
+ <button
116
+ onClick={() => {
117
+ setTypeFilter(typeFilter === type ? null : type);
118
+ setCurrentPage(1);
119
+ }}
120
+ className={`px-3 py-1 rounded-md text-sm font-medium ${
121
+ typeFilter === type
122
+ ? 'bg-black text-white border border-gray-200 hover:bg-gray-900'
123
+ : 'bg-white text-black border border-gray-200 hover:bg-gray-100'
124
+ }`}
125
+ >
126
+ {label} ({count})
127
+ </button>
128
+ )
129
+ );
130
+
131
+ const ResourceGroupTable = (props: ResourceGroupTableProps) => {
132
+ const { resources = [], limit, showTags = false, showOwners = false, title, subtitle = 'Resources', description } = props;
133
+ const [searchTerm, setSearchTerm] = useState('');
134
+ const [currentPage, setCurrentPage] = useState(1);
135
+ const [typeFilter, setTypeFilter] = useState<ResourceType>(null);
136
+ const itemsPerPage = limit || 10;
137
+
138
+ const filterResources = useCallback((resources: Resource[], searchTerm: string, typeFilter: ResourceType) => {
139
+ let filtered = resources;
140
+
141
+ if (typeFilter) {
142
+ filtered = filtered.filter((resource) => {
143
+ const collectionType = resource.collection.slice(0, -1);
144
+ const normalizedType = collectionType === 'querie' ? 'query' : collectionType;
145
+ return normalizedType === typeFilter;
146
+ });
147
+ }
148
+
149
+ if (searchTerm) {
150
+ const lowerSearchTerm = searchTerm.toLowerCase();
151
+ filtered = filtered.filter((resource) => {
152
+ const collectionType = resource.collection.slice(0, -1);
153
+ const normalizedType = collectionType === 'querie' ? 'query' : collectionType;
154
+
155
+ return (
156
+ resource.name.toLowerCase().includes(lowerSearchTerm) ||
157
+ resource.summary?.toLowerCase().includes(lowerSearchTerm) ||
158
+ resource.description?.toLowerCase().includes(lowerSearchTerm) ||
159
+ normalizedType.toLowerCase().includes(lowerSearchTerm) ||
160
+ resource.tags?.some((tag) => tag.toLowerCase().includes(lowerSearchTerm)) ||
161
+ false
162
+ );
163
+ });
164
+ }
165
+
166
+ return filtered;
167
+ }, []);
168
+
169
+ const filteredResources = useMemo(
170
+ () => filterResources(resources, searchTerm, typeFilter),
171
+ [resources, searchTerm, typeFilter, filterResources]
172
+ );
173
+
174
+ const totalPages = Math.ceil(filteredResources.length / itemsPerPage);
175
+ const startIndex = (currentPage - 1) * itemsPerPage;
176
+ const paginatedResources = useMemo(
177
+ () => filteredResources.slice(startIndex, startIndex + itemsPerPage),
178
+ [filteredResources, startIndex, itemsPerPage]
179
+ );
180
+
181
+ // Get unique resource types and their counts
182
+ const resourceTypeCounts = useMemo(() => {
183
+ const counts = new Map<ResourceType, number>();
184
+ resources.forEach((resource) => {
185
+ const collectionType = resource.collection.slice(0, -1);
186
+ const normalizedType = (collectionType === 'querie' ? 'query' : collectionType) as ResourceType;
187
+ counts.set(normalizedType, (counts.get(normalizedType) || 0) + 1);
188
+ });
189
+ return counts;
190
+ }, [resources]);
191
+
192
+ const availableTypes = useMemo(
193
+ () =>
194
+ Array.from(
195
+ new Set(
196
+ resources.map((resource) => {
197
+ const collectionType = resource.collection.slice(0, -1);
198
+ return collectionType === 'querie' ? 'query' : collectionType;
199
+ })
200
+ )
201
+ ) as ResourceType[],
202
+ [resources]
203
+ );
204
+
205
+ const filterButtons = useMemo(
206
+ () =>
207
+ availableTypes
208
+ .filter((type): type is NonNullable<ResourceType> => type !== null)
209
+ .map((type) => ({
210
+ type,
211
+ // Format the label to be capitalized and pluralized if needed
212
+ label: type.charAt(0).toUpperCase() + type.slice(1) + (type.endsWith('s') ? '' : 's'),
213
+ count: resourceTypeCounts.get(type) || 0,
214
+ })),
215
+ [availableTypes, resourceTypeCounts]
216
+ );
217
+
218
+ return (
219
+ <div className="mx-auto not-prose py-4 space-y-4 my-4">
220
+ {title && <h2 className="text-2xl font-semibold">{title}</h2>}
221
+ <div className="flow-root bg-white border-gray-200 border p-4 pb-2 rounded-lg text-gray-900">
222
+ <div className="space-y-4">
223
+ <h2 className="text-xl font-semibold">
224
+ {subtitle} ({searchTerm || typeFilter ? `${filteredResources.length}/${resources.length}` : resources.length})
225
+ </h2>
226
+ <span className="text-sm text-gray-700">{description}</span>
227
+
228
+ {/* Type filter buttons - only shown if there are filter options */}
229
+ {filterButtons.length > 0 && (
230
+ <div className="flex gap-2 pb-2 flex-wrap">
231
+ {filterButtons.map((button) => (
232
+ <FilterButton
233
+ key={button.type}
234
+ type={button.type}
235
+ label={button.label}
236
+ count={button.count}
237
+ typeFilter={typeFilter}
238
+ setTypeFilter={setTypeFilter}
239
+ setCurrentPage={setCurrentPage}
240
+ />
241
+ ))}
242
+ </div>
243
+ )}
244
+
245
+ <div className="relative">
246
+ <div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
247
+ <svg className="h-5 w-5 text-gray-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
248
+ <path
249
+ fillRule="evenodd"
250
+ d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z"
251
+ clipRule="evenodd"
252
+ />
253
+ </svg>
254
+ </div>
255
+ <input
256
+ type="text"
257
+ value={searchTerm}
258
+ onChange={(e) => {
259
+ setSearchTerm(e.target.value);
260
+ setCurrentPage(1); // Reset to first page when searching
261
+ }}
262
+ placeholder="Search by name, type, description, or tags..."
263
+ className="block w-full rounded-md border-0 py-1.5 pl-10 pr-10 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
264
+ />
265
+ {searchTerm && (
266
+ <button
267
+ onClick={() => {
268
+ setSearchTerm('');
269
+ setCurrentPage(1); // Reset to first page when clearing search
270
+ }}
271
+ className="absolute inset-y-0 right-0 flex items-center pr-3"
272
+ aria-label="Clear search"
273
+ >
274
+ <svg className="h-5 w-5 text-gray-400 hover:text-gray-500" viewBox="0 0 20 20" fill="currentColor">
275
+ <path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" />
276
+ </svg>
277
+ </button>
278
+ )}
279
+ </div>
280
+ </div>
281
+ <div className="overflow-x-auto">
282
+ <div className="inline-block min-w-full py-2 align-middle">
283
+ <div className="max-w-full overflow-hidden">
284
+ <table className="min-w-full table-fixed divide-y divide-gray-300 rounded-sm bg-white">
285
+ <thead>
286
+ <tr>
287
+ <th
288
+ scope="col"
289
+ className={`${showTags || showOwners ? 'w-1/5' : 'w-1/4'} py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6`}
290
+ >
291
+ Name
292
+ </th>
293
+ <th scope="col" className="w-[100px] px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
294
+ Version
295
+ </th>
296
+ <th scope="col" className="w-[100px] py-3.5 pl-3.5 pr-3 text-left text-sm font-semibold text-gray-900">
297
+ Type
298
+ </th>
299
+ <th
300
+ scope="col"
301
+ className={`${showTags && showOwners ? 'w-1/4' : showTags || showOwners ? 'w-1/3' : 'w-1/2'} px-3 py-3.5 text-left text-sm font-semibold text-gray-900`}
302
+ >
303
+ Description
304
+ </th>
305
+ {showTags && (
306
+ <th scope="col" className="w-1/6 px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
307
+ Tags
308
+ </th>
309
+ )}
310
+ {showOwners && (
311
+ <th scope="col" className="w-1/6 px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
312
+ Owners
313
+ </th>
314
+ )}
315
+ </tr>
316
+ </thead>
317
+ <tbody className="divide-y divide-gray-200">
318
+ {paginatedResources.length > 0 ? (
319
+ paginatedResources.map((resource) => (
320
+ <ResourceRow
321
+ key={`${resource.collection}-${resource.id}-${resource.version}`}
322
+ resource={resource}
323
+ showTags={showTags}
324
+ showOwners={showOwners}
325
+ />
326
+ ))
327
+ ) : (
328
+ <tr>
329
+ <td
330
+ colSpan={showTags && showOwners ? 6 : showTags || showOwners ? 5 : 4}
331
+ className="text-center py-4 text-sm text-gray-500"
332
+ >
333
+ No resources found
334
+ </td>
335
+ </tr>
336
+ )}
337
+ </tbody>
338
+ </table>
339
+ </div>
340
+ </div>
341
+ </div>
342
+ {totalPages > 1 && (
343
+ <div className="flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6 -mt-2">
344
+ <div className="flex flex-1 justify-between sm:hidden">
345
+ <button
346
+ onClick={() => setCurrentPage(currentPage - 1)}
347
+ disabled={currentPage === 1}
348
+ className={`relative inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium ${currentPage === 1 ? 'text-gray-300' : 'text-gray-700 hover:bg-gray-50'}`}
349
+ >
350
+ Previous
351
+ </button>
352
+ <button
353
+ onClick={() => setCurrentPage(currentPage + 1)}
354
+ disabled={currentPage === totalPages}
355
+ className={`relative ml-3 inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium ${currentPage === totalPages ? 'text-gray-300' : 'text-gray-700 hover:bg-gray-50'}`}
356
+ >
357
+ Next
358
+ </button>
359
+ </div>
360
+ <div className="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
361
+ <div>
362
+ <p className="text-sm text-gray-700">
363
+ Showing <span className="font-medium">{startIndex + 1}</span> to{' '}
364
+ <span className="font-medium">{Math.min(startIndex + itemsPerPage, filteredResources.length)}</span> of{' '}
365
+ <span className="font-medium">{filteredResources.length}</span> results
366
+ </p>
367
+ </div>
368
+ <div>
369
+ <nav className="isolate inline-flex -space-x-px rounded-md shadow-sm" aria-label="Pagination">
370
+ <button
371
+ onClick={() => setCurrentPage(currentPage - 1)}
372
+ disabled={currentPage === 1}
373
+ className={`relative inline-flex items-center rounded-l-md px-2 py-2 ${currentPage === 1 ? 'text-gray-300' : 'text-gray-400 hover:bg-gray-50'} ring-1 ring-inset ring-gray-300`}
374
+ >
375
+ <span className="sr-only">Previous</span>
376
+ <svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
377
+ <path
378
+ fillRule="evenodd"
379
+ d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z"
380
+ clipRule="evenodd"
381
+ />
382
+ </svg>
383
+ </button>
384
+ <button
385
+ onClick={() => setCurrentPage(currentPage + 1)}
386
+ disabled={currentPage === totalPages}
387
+ className={`relative inline-flex items-center rounded-r-md px-2 py-2 ${currentPage === totalPages ? 'text-gray-300' : 'text-gray-400 hover:bg-gray-50'} ring-1 ring-inset ring-gray-300`}
388
+ >
389
+ <span className="sr-only">Next</span>
390
+ <svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
391
+ <path
392
+ fillRule="evenodd"
393
+ d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z"
394
+ clipRule="evenodd"
395
+ />
396
+ </svg>
397
+ </button>
398
+ </nav>
399
+ </div>
400
+ </div>
401
+ </div>
402
+ )}
403
+ </div>
404
+ </div>
405
+ );
406
+ };
407
+
408
+ export default ResourceGroupTable;
@@ -13,6 +13,7 @@ import OpenAPI from '@components/MDX/OpenAPI/OpenAPI.astro';
13
13
  import AsyncAPI from '@components/MDX/AsyncAPI/AsyncAPI.astro';
14
14
  import ChannelInformation from '@components/MDX/ChannelInformation/ChannelInformation';
15
15
  import MessageTable from '@components/MDX/MessageTable/MessageTable.astro';
16
+ import ResourceGroupTable from '@components/MDX/ResourceGroupTable/ResourceGroupTable.astro';
16
17
  import Tabs from '@components/MDX/Tabs/Tabs.astro';
17
18
  import TabItem from '@components/MDX/Tabs/TabItem.astro';
18
19
 
@@ -41,6 +42,7 @@ const components = (props: any) => {
41
42
  SchemaViewer: (mdxProp: any) => SchemaViewerPortal({ ...props.data, ...mdxProp }),
42
43
  Schema: (mdxProp: any) => jsx(Schema, { ...props, ...mdxProp }),
43
44
  MessageTable: (mdxProp: any) => jsx(MessageTable, { ...props, ...mdxProp }),
45
+ ResourceGroupTable: (mdxProp: any) => jsx(ResourceGroupTable, { ...props, ...mdxProp }),
44
46
  };
45
47
  };
46
48
 
@@ -4,6 +4,7 @@ import PillListFlat from '@components/Lists/PillListFlat';
4
4
  import RepositoryList from '@components/Lists/RepositoryList.astro';
5
5
  import VersionList from '@components/Lists/VersionList.astro';
6
6
  import { getUbiquitousLanguage, getMessagesForDomain } from '@utils/collections/domains';
7
+ import CustomSideBarSectionList from '@components/Lists/CustomSideBarSectionList.astro';
7
8
  import { getOwner } from '@utils/collections/owners';
8
9
  import { buildUrl } from '@utils/url-builder';
9
10
  import type { CollectionEntry } from 'astro:content';
@@ -27,6 +28,8 @@ const filteredOwners = owners.filter((o) => o !== undefined);
27
28
 
28
29
  const messagesForDomain = await getMessagesForDomain(domain);
29
30
 
31
+ const resourceGroups = domain.data?.resourceGroups || [];
32
+
30
33
  const serviceList = services.map((p) => ({
31
34
  label: p.data.name,
32
35
  badge: p.collection,
@@ -73,6 +76,11 @@ const ownersList = filteredOwners.map((o) => ({
73
76
 
74
77
  <aside class="sticky top-28 left-0 space-y-8 h-full overflow-y-auto py-4">
75
78
  <div>
79
+ {
80
+ resourceGroups
81
+ .filter((section) => section.items.length > 0 && section.sidebar)
82
+ .map((section) => <CustomSideBarSectionList section={section} />)
83
+ }
76
84
  <PillListFlat
77
85
  title={`Services (${services.length})`}
78
86
  pills={serviceList}
@@ -6,7 +6,7 @@ import { getOwner } from '@utils/collections/owners';
6
6
  import type { CollectionEntry } from 'astro:content';
7
7
  import { ScrollText, Workflow, RssIcon } from 'lucide-react';
8
8
  import config from '@config';
9
-
9
+ import CustomSideBarSectionList from '@components/Lists/CustomSideBarSectionList.astro';
10
10
  interface Props {
11
11
  flow: CollectionEntry<'flows'>;
12
12
  }
@@ -17,6 +17,8 @@ const ownersRaw = flow.data?.owners || [];
17
17
  const owners = await Promise.all<ReturnType<typeof getOwner>>(ownersRaw.map(getOwner));
18
18
  const filteredOwners = owners.filter((o) => o !== undefined);
19
19
 
20
+ const resourceGroups = flow.data?.resourceGroups || [];
21
+
20
22
  const ownersList = filteredOwners.map((o) => ({
21
23
  label: o.data.name,
22
24
  type: o.collection,
@@ -30,6 +32,11 @@ const isRSSEnabled = config.rss?.enabled;
30
32
 
31
33
  <aside class="sticky top-28 left-0 h-full overflow-y-auto pr-6 py-4">
32
34
  <div id="sidebar-cta-portal" class="">
35
+ {
36
+ resourceGroups
37
+ .filter((section) => section.items.length > 0 && section.sidebar)
38
+ .map((section) => <CustomSideBarSectionList section={section} />)
39
+ }
33
40
  {flow.data.versions && <VersionList versions={flow.data.versions} collectionItem={flow} />}
34
41
 
35
42
  <OwnersList
@@ -9,6 +9,7 @@ import { buildUrl } from '@utils/url-builder';
9
9
  import { FileDownIcon, ScrollText, Workflow, RssIcon } from 'lucide-react';
10
10
  import RepositoryList from '@components/Lists/RepositoryList.astro';
11
11
  import { getOwner } from '@utils/collections/owners';
12
+ import CustomSideBarSectionList from '@components/Lists/CustomSideBarSectionList.astro';
12
13
  import config from '@config';
13
14
 
14
15
  interface Props {
@@ -25,6 +26,8 @@ const ownersRaw = message.data?.owners || [];
25
26
  const owners = await Promise.all<ReturnType<typeof getOwner>>(ownersRaw.map(getOwner));
26
27
  const filteredOwners = owners.filter((o) => o !== undefined);
27
28
 
29
+ const resourceGroups = message.data?.resourceGroups || [];
30
+
28
31
  const producerList = producers.map((p) => ({
29
32
  label: `${p.data.name}`,
30
33
  tag: `v${p.data.version}`,
@@ -98,6 +101,11 @@ const schemaURL = path.join(publicPath, schemaFilePath || '');
98
101
 
99
102
  <aside class="sticky top-28 left-0 space-y-8 h-full overflow-y-auto py-4">
100
103
  <div class="">
104
+ {
105
+ resourceGroups
106
+ .filter((section) => section.items.length > 0 && section.sidebar)
107
+ .map((section) => <CustomSideBarSectionList section={section} />)
108
+ }
101
109
  {
102
110
  producerList.length > 0 && (
103
111
  <PillListFlat
@@ -3,6 +3,7 @@ import OwnersList from '@components/Lists/OwnersList';
3
3
  import PillListFlat from '@components/Lists/PillListFlat';
4
4
  import RepositoryList from '@components/Lists/RepositoryList.astro';
5
5
  import SpecificationsList from '@components/Lists/SpecificationsList.astro';
6
+ import CustomSideBarSectionList from '@components/Lists/CustomSideBarSectionList.astro';
6
7
  import VersionList from '@components/Lists/VersionList.astro';
7
8
  import { buildUrl } from '@utils/url-builder';
8
9
  import { getOwner } from '@utils/collections/owners';
@@ -10,7 +11,6 @@ import type { CollectionEntry } from 'astro:content';
10
11
  import { ScrollText, Workflow, FileDownIcon, Code, Link, RssIcon } from 'lucide-react';
11
12
  import { join } from 'node:path';
12
13
  import config from '@config';
13
-
14
14
  interface Props {
15
15
  service: CollectionEntry<'services'>;
16
16
  }
@@ -26,6 +26,8 @@ const ownersRaw = service.data?.owners || [];
26
26
  const owners = await Promise.all<ReturnType<typeof getOwner>>(ownersRaw.map(getOwner));
27
27
  const filteredOwners = owners.filter((o) => o !== undefined);
28
28
 
29
+ const resourceGroups = service.data?.resourceGroups || [];
30
+
29
31
  const sendsList = sends
30
32
  .sort((a, b) => a.collection.localeCompare(b.collection))
31
33
  .map((p) => ({
@@ -66,6 +68,11 @@ const schemaURL = join(publicPath, schemaFilePath || '');
66
68
 
67
69
  <aside class="sticky top-28 left-0 h-full overflow-y-auto pr-6 py-4">
68
70
  <div id="sidebar-cta-portal" class="">
71
+ {
72
+ resourceGroups
73
+ .filter((section) => section.items.length > 0 && section.sidebar)
74
+ .map((section) => <CustomSideBarSectionList section={section} />)
75
+ }
69
76
  <PillListFlat
70
77
  title={`Receives Messages (${receivesList.length})`}
71
78
  pills={receivesList}
@@ -1,9 +1,9 @@
1
- import { ServerIcon, BoltIcon, ChatBubbleLeftIcon } from '@heroicons/react/24/solid';
1
+ import { ServerIcon } from '@heroicons/react/24/solid';
2
2
  import { createColumnHelper } from '@tanstack/react-table';
3
3
  import { useMemo, useState } from 'react';
4
4
  import { filterByName, filterCollectionByName } from '../filters/custom-filters';
5
5
  import { buildUrl } from '@utils/url-builder';
6
- import { getColorAndIconForMessageType } from './MessageTableColumns';
6
+ import { getColorAndIconForCollection } from '@utils/collections/icons';
7
7
  import { createBadgesColumn } from './SharedColumns';
8
8
  import type { TData } from '../Table';
9
9
 
@@ -66,10 +66,9 @@ export const columns = () => [
66
66
  const receiversWithIcons = useMemo(
67
67
  () =>
68
68
  receives?.map((consumer) => {
69
- const type = consumer.collection.slice(0, -1);
70
69
  return {
71
70
  ...consumer,
72
- ...getColorAndIconForMessageType(type),
71
+ ...getColorAndIconForCollection(consumer.collection),
73
72
  };
74
73
  }) || [],
75
74
  [receives]
@@ -131,10 +130,9 @@ export const columns = () => [
131
130
  const sendersWithIcons = useMemo(
132
131
  () =>
133
132
  sends?.map((sender) => {
134
- const type = sender.collection.slice(0, -1);
135
133
  return {
136
134
  ...sender,
137
- ...getColorAndIconForMessageType(type),
135
+ ...getColorAndIconForCollection(sender.collection),
138
136
  };
139
137
  }) || [],
140
138
  [sends]
@@ -5,26 +5,11 @@ import { filterByName, filterCollectionByName } from '../filters/custom-filters'
5
5
  import { buildUrl } from '@utils/url-builder';
6
6
  import type { TData } from '../Table';
7
7
  import type { CollectionUserTypes } from '@types';
8
- import { User, Users } from 'lucide-react';
9
8
  import type { CollectionEntry } from 'astro:content';
10
9
  import { ServerIcon, BoltIcon, ChatBubbleLeftIcon } from '@heroicons/react/24/solid';
11
10
 
12
11
  const columnHelper = createColumnHelper<TData<CollectionUserTypes>>();
13
12
 
14
- export const getColorAndIconForMessageType = (type: string) => {
15
- switch (type) {
16
- case 'event':
17
- return { color: 'orange', Icon: BoltIcon };
18
- case 'command':
19
- return { color: 'blue', Icon: ChatBubbleLeftIcon };
20
- case 'querie':
21
- case 'query':
22
- return { color: 'green', Icon: MagnifyingGlassIcon };
23
- default:
24
- return { color: 'gray', Icon: ChatBubbleLeftIcon };
25
- }
26
- };
27
-
28
13
  export const columns = () => [
29
14
  columnHelper.accessor('data.name', {
30
15
  id: 'name',
@@ -35,6 +35,12 @@ const channelPointer = z
35
35
  })
36
36
  .merge(pointer);
37
37
 
38
+ const resourcePointer = z.object({
39
+ id: z.string(),
40
+ version: z.string().optional().default('latest'),
41
+ type: z.enum(['service', 'event', 'command', 'query', 'flow', 'channel', 'domain', 'user', 'team']),
42
+ });
43
+
38
44
  const changelogs = defineCollection({
39
45
  loader: glob({
40
46
  pattern: ['**/changelog.(md|mdx)'],
@@ -87,6 +93,17 @@ const baseSchema = z.object({
87
93
  })
88
94
  .optional(),
89
95
  hidden: z.boolean().optional(),
96
+ resourceGroups: z
97
+ .array(
98
+ z.object({
99
+ id: z.string().optional(),
100
+ title: z.string().optional(),
101
+ items: z.array(resourcePointer),
102
+ limit: z.number().optional().default(10),
103
+ sidebar: z.boolean().optional().default(true),
104
+ })
105
+ )
106
+ .optional(),
90
107
  // Used by eventcatalog
91
108
  versions: z.array(z.string()).optional(),
92
109
  latestVersion: z.string().optional(),
@@ -43,3 +43,32 @@ export const getIconForCollection = (collection: string) => {
43
43
  return ServerIcon;
44
44
  }
45
45
  };
46
+
47
+ export const getColorAndIconForCollection = (collection: string) => {
48
+ const icon = getIconForCollection(collection);
49
+
50
+ switch (collection) {
51
+ case 'events':
52
+ return { color: 'orange', Icon: icon };
53
+ case 'commands':
54
+ return { color: 'blue', Icon: icon };
55
+ case 'queries':
56
+ return { color: 'green', Icon: icon };
57
+ case 'flows':
58
+ return { color: 'teal', Icon: icon };
59
+ case 'teams':
60
+ return { color: 'red', Icon: icon };
61
+ case 'users':
62
+ return { color: 'gray', Icon: icon };
63
+ case 'channels':
64
+ return { color: 'purple', Icon: icon };
65
+ case 'ubiquitousLanguages':
66
+ return { color: 'green', Icon: icon };
67
+ case 'domains':
68
+ return { color: 'yellow', Icon: icon };
69
+ case 'services':
70
+ return { color: 'pink', Icon: icon };
71
+ default:
72
+ return { color: 'gray', Icon: icon };
73
+ }
74
+ };
@@ -21,10 +21,10 @@ let cachedServices: Record<string, Service[]> = {
21
21
  export const getServices = async ({ getAllVersions = true }: Props = {}): Promise<Service[]> => {
22
22
  const cacheKey = getAllVersions ? 'allVersions' : 'currentVersions';
23
23
 
24
- // // Check if we have cached domains for this specific getAllVersions value
25
- // if (cachedServices[cacheKey].length > 0) {
26
- // return cachedServices[cacheKey];
27
- // }
24
+ // Check if we have cached domains for this specific getAllVersions value
25
+ if (cachedServices[cacheKey].length > 0) {
26
+ return cachedServices[cacheKey];
27
+ }
28
28
 
29
29
  // Get services that are not versioned
30
30
  const services = await getCollection('services', (service) => {
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "url": "https://github.com/event-catalog/eventcatalog.git"
7
7
  },
8
8
  "type": "module",
9
- "version": "2.31.5",
9
+ "version": "2.32.0",
10
10
  "publishConfig": {
11
11
  "access": "public"
12
12
  },