@eventcatalog/core 2.26.0 → 2.27.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 (40) hide show
  1. package/README.md +7 -3
  2. package/dist/analytics/analytics.cjs +1 -1
  3. package/dist/analytics/analytics.js +2 -2
  4. package/dist/analytics/log-build.cjs +1 -1
  5. package/dist/analytics/log-build.js +3 -3
  6. package/dist/catalog-to-astro-content-directory.cjs +1 -7
  7. package/dist/catalog-to-astro-content-directory.js +2 -2
  8. package/dist/{chunk-3EOBEGSB.js → chunk-2VGR4HMJ.js} +1 -1
  9. package/dist/{chunk-TFBAK5C5.js → chunk-CTL6CH3C.js} +1 -1
  10. package/dist/{chunk-7JDTB3U5.js → chunk-FIY5JLSQ.js} +0 -2
  11. package/dist/{chunk-IY5HYH2G.js → chunk-LMNJPHFP.js} +1 -1
  12. package/dist/{chunk-VCR3LHZR.js → chunk-R2NILSWL.js} +2 -6
  13. package/dist/{chunk-OW2FQPYP.js → chunk-WUCY3QHK.js} +1 -1
  14. package/dist/constants.cjs +1 -1
  15. package/dist/constants.js +1 -1
  16. package/dist/eventcatalog.cjs +2 -8
  17. package/dist/eventcatalog.js +6 -6
  18. package/dist/map-catalog-to-astro.cjs +0 -2
  19. package/dist/map-catalog-to-astro.js +1 -1
  20. package/dist/watcher.cjs +0 -2
  21. package/dist/watcher.js +2 -2
  22. package/eventcatalog/astro.config.mjs +0 -3
  23. package/eventcatalog/src/components/Grids/DomainGrid.tsx +233 -0
  24. package/eventcatalog/src/components/Grids/MessageGrid.tsx +457 -0
  25. package/eventcatalog/src/components/Grids/ServiceGrid.tsx +364 -0
  26. package/eventcatalog/src/components/Grids/components.tsx +170 -0
  27. package/eventcatalog/src/components/Grids/utils.tsx +38 -0
  28. package/eventcatalog/src/{content/config.ts → content.config.ts} +12 -2
  29. package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +29 -17
  30. package/eventcatalog/src/pages/architecture/[type]/index.astro +88 -0
  31. package/eventcatalog/src/pages/chat/index.astro +1 -1
  32. package/eventcatalog/src/pages/docs/teams/[id]/index.astro +6 -13
  33. package/eventcatalog/src/pages/docs/users/[id]/index.astro +7 -13
  34. package/eventcatalog/src/pages/index.astro +237 -72
  35. package/eventcatalog/src/utils/url-builder.ts +20 -0
  36. package/eventcatalog/src/utils/users.ts +2 -2
  37. package/eventcatalog/tailwind.config.mjs +11 -0
  38. package/package.json +2 -1
  39. package/default-files-for-collections/teams.md +0 -11
  40. package/default-files-for-collections/users.md +0 -11
@@ -0,0 +1,457 @@
1
+ import { useState, useMemo, useEffect } from 'react';
2
+ import { EnvelopeIcon, ChevronRightIcon, ServerIcon } from '@heroicons/react/24/outline';
3
+ import { RectangleGroupIcon } from '@heroicons/react/24/outline';
4
+ import { buildUrl, buildUrlWithParams } from '@utils/url-builder';
5
+ import type { CollectionEntry } from 'astro:content';
6
+ import type { CollectionMessageTypes } from '@types';
7
+ import { getCollectionStyles } from './utils';
8
+ import { SearchBar, TypeFilters, Pagination } from './components';
9
+
10
+ interface MessageGridProps {
11
+ messages: CollectionEntry<CollectionMessageTypes>[];
12
+ }
13
+
14
+ interface GroupedMessages {
15
+ all?: CollectionEntry<CollectionMessageTypes>[];
16
+ sends?: CollectionEntry<CollectionMessageTypes>[];
17
+ receives?: CollectionEntry<CollectionMessageTypes>[];
18
+ }
19
+
20
+ export default function MessageGrid({ messages }: MessageGridProps) {
21
+ const [searchQuery, setSearchQuery] = useState('');
22
+ const [urlParams, setUrlParams] = useState<{
23
+ serviceId?: string;
24
+ serviceName?: string;
25
+ domainId?: string;
26
+ domainName?: string;
27
+ } | null>(null);
28
+ const [currentPage, setCurrentPage] = useState(1);
29
+ const [selectedTypes, setSelectedTypes] = useState<CollectionMessageTypes[]>([]);
30
+ const [producerConsumerFilter, setProducerConsumerFilter] = useState<'all' | 'no-producers' | 'no-consumers'>('all');
31
+ const ITEMS_PER_PAGE = 15;
32
+
33
+ // Effect to sync URL params with state
34
+ useEffect(() => {
35
+ const params = new URLSearchParams(window.location.search);
36
+ const serviceId = params.get('serviceId') || undefined;
37
+ const serviceName = params.get('serviceName') ? decodeURIComponent(params.get('serviceName')!) : undefined;
38
+ const domainId = params.get('domainId') || undefined;
39
+ const domainName = params.get('domainName') || undefined;
40
+ setUrlParams({
41
+ serviceId,
42
+ serviceName,
43
+ domainId,
44
+ domainName,
45
+ });
46
+ }, []);
47
+
48
+ const filteredAndSortedMessages = useMemo(() => {
49
+ if (urlParams === null) return [];
50
+
51
+ let result = [...messages];
52
+
53
+ // Filter by message type
54
+ if (selectedTypes.length > 0) {
55
+ result = result.filter((message) => selectedTypes.includes(message.collection));
56
+ }
57
+
58
+ // Apply producer/consumer filters
59
+ if (producerConsumerFilter === 'no-producers') {
60
+ result = result.filter((message) => !message.data.producers || message.data.producers.length === 0);
61
+ } else if (producerConsumerFilter === 'no-consumers') {
62
+ result = result.filter((message) => !message.data.consumers || message.data.consumers.length === 0);
63
+ }
64
+
65
+ // Filter by service ID or name if present
66
+ if (urlParams.serviceId) {
67
+ result = result.filter(
68
+ (message) =>
69
+ message.data.producers?.some(
70
+ (producer: any) => producer.data.id === urlParams.serviceId && !producer.id.includes('/versioned/')
71
+ ) ||
72
+ message.data.consumers?.some(
73
+ (consumer: any) => consumer.data.id === urlParams.serviceId && !consumer.id.includes('/versioned/')
74
+ )
75
+ );
76
+ }
77
+
78
+ // Filter by search query
79
+ if (searchQuery) {
80
+ const query = searchQuery.toLowerCase();
81
+ result = result.filter(
82
+ (message) =>
83
+ message.data.name?.toLowerCase().includes(query) ||
84
+ message.data.summary?.toLowerCase().includes(query) ||
85
+ message.data.producers?.some((producer: any) => producer.data.id?.toLowerCase().includes(query)) ||
86
+ message.data.consumers?.some((consumer: any) => consumer.data.id?.toLowerCase().includes(query))
87
+ );
88
+ }
89
+
90
+ // Sort by name by default
91
+ result.sort((a, b) => a.data.name.localeCompare(b.data.name));
92
+
93
+ return result;
94
+ }, [messages, searchQuery, urlParams, selectedTypes, producerConsumerFilter]);
95
+
96
+ // Add totalPages calculation
97
+ const totalPages = useMemo(() => {
98
+ if (urlParams?.serviceId || urlParams?.domainId) return 1;
99
+ return Math.ceil(filteredAndSortedMessages.length / ITEMS_PER_PAGE);
100
+ }, [filteredAndSortedMessages.length, urlParams]);
101
+
102
+ // Add paginatedMessages calculation
103
+ const paginatedMessages = useMemo(() => {
104
+ if (urlParams?.serviceId || urlParams?.domainId) {
105
+ return filteredAndSortedMessages;
106
+ }
107
+
108
+ const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
109
+ return filteredAndSortedMessages.slice(startIndex, startIndex + ITEMS_PER_PAGE);
110
+ }, [filteredAndSortedMessages, currentPage, urlParams]);
111
+
112
+ // Reset pagination when search query or filters change
113
+ useEffect(() => {
114
+ setCurrentPage(1);
115
+ }, [searchQuery, selectedTypes]);
116
+
117
+ // Group messages by sends/receives when a service is selected
118
+ const groupedMessages = useMemo<GroupedMessages>(() => {
119
+ if (!urlParams?.serviceId) return { all: filteredAndSortedMessages };
120
+
121
+ const serviceIdentifier = urlParams.serviceId;
122
+ const sends = filteredAndSortedMessages.filter((message) =>
123
+ message.data.producers?.some((producer: any) => producer.data.id === serviceIdentifier)
124
+ );
125
+ const receives = filteredAndSortedMessages.filter((message) =>
126
+ message.data.consumers?.some((consumer: any) => consumer.data.id === serviceIdentifier)
127
+ );
128
+
129
+ return { sends, receives };
130
+ }, [filteredAndSortedMessages, urlParams]);
131
+
132
+ const renderTypeFilters = () => {
133
+ return (
134
+ <div className="flex flex-col gap-4 sm:flex-row sm:items-center">
135
+ <div className="flex items-center gap-2">
136
+ <TypeFilters
137
+ selectedTypes={selectedTypes}
138
+ onTypeChange={setSelectedTypes}
139
+ filteredCount={filteredAndSortedMessages.filter((m) => selectedTypes.includes(m.collection)).length}
140
+ />
141
+ </div>
142
+
143
+ <div className="flex items-center gap-2 border-l border-gray-200 pl-4">
144
+ <div className="flex items-center gap-2">
145
+ <select
146
+ value={producerConsumerFilter}
147
+ onChange={(e) => setProducerConsumerFilter(e.target.value as typeof producerConsumerFilter)}
148
+ className="block rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-primary sm:text-sm sm:leading-6"
149
+ >
150
+ <option value="all">All Messages</option>
151
+ <option value="no-producers">Without Producers</option>
152
+ <option value="no-consumers">Without Consumers</option>
153
+ </select>
154
+ {producerConsumerFilter !== 'all' && (
155
+ <button
156
+ onClick={() => setProducerConsumerFilter('all')}
157
+ className="text-xs text-gray-500 hover:text-gray-700 hover:underline"
158
+ >
159
+ Clear
160
+ </button>
161
+ )}
162
+ </div>
163
+ </div>
164
+ </div>
165
+ );
166
+ };
167
+
168
+ const renderMessageGrid = (messages: CollectionEntry<CollectionMessageTypes>[]) => (
169
+ <div
170
+ className={`grid ${urlParams?.serviceName ? 'grid-cols-1' : 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3'} gap-6`}
171
+ >
172
+ {messages.map((message) => {
173
+ const { color, Icon } = getCollectionStyles(message.collection);
174
+ const hasProducers = message.data.producers && message.data.producers.length > 0;
175
+ const hasConsumers = message.data.consumers && message.data.consumers.length > 0;
176
+ return (
177
+ <a
178
+ key={message.data.name}
179
+ href={buildUrl(`/docs/${message.collection}/${message.data.id}/${message.data.version}`)}
180
+ className={`group bg-white border hover:bg-${color}-100 rounded-lg shadow-sm hover:shadow-lg transition-all duration-200 overflow-hidden border-${color}-500 `}
181
+ >
182
+ <div className="p-4 flex-1">
183
+ <div className="flex items-center justify-between mb-3">
184
+ <div className="flex items-center gap-2">
185
+ <Icon className={`h-5 w-5 text-${color}-500`} />
186
+ <h3
187
+ className={`text-md font-semibold text-gray-900 truncate group-hover:text-${color}-500 transition-colors duration-200`}
188
+ >
189
+ {message.data.name} (v{message.data.version})
190
+ </h3>
191
+ </div>
192
+ </div>
193
+
194
+ {message.data.summary && <p className="text-gray-600 text-xs line-clamp-2 mb-4">{message.data.summary}</p>}
195
+
196
+ {/* Only show stats in non-service view */}
197
+ {!urlParams?.serviceName && (
198
+ <div className="space-y-4">
199
+ <div className="grid grid-cols-2 gap-2 p-3 bg-gray-50 rounded-lg">
200
+ <div className="text-center">
201
+ <div className="flex items-center justify-center mb-1">
202
+ <ServerIcon className={`h-5 w-5 text-pink-500`} />
203
+ </div>
204
+ <div className="text-sm font-medium text-gray-900">{message.data.producers?.length ?? 0}</div>
205
+ <div className="text-xs text-gray-500">Producers</div>
206
+ </div>
207
+ <div className="text-center border-l border-gray-200">
208
+ <div className="flex items-center justify-center mb-1">
209
+ <ServerIcon className={`h-5 w-5 text-pink-500`} />
210
+ </div>
211
+ <div className="text-sm font-medium text-gray-900">{message.data.consumers?.length ?? 0}</div>
212
+ <div className="text-xs text-gray-500">Consumers</div>
213
+ </div>
214
+ </div>
215
+ </div>
216
+ )}
217
+ </div>
218
+ </a>
219
+ );
220
+ })}
221
+ </div>
222
+ );
223
+
224
+ const renderPaginationControls = () => {
225
+ if (totalPages <= 1 || urlParams?.serviceName || urlParams?.domainId) return null;
226
+
227
+ return (
228
+ <Pagination
229
+ currentPage={currentPage}
230
+ totalPages={totalPages}
231
+ totalItems={filteredAndSortedMessages.length}
232
+ itemsPerPage={ITEMS_PER_PAGE}
233
+ onPageChange={setCurrentPage}
234
+ />
235
+ );
236
+ };
237
+
238
+ return (
239
+ <div>
240
+ {/* Breadcrumb */}
241
+ <nav className="mb-4 flex items-center space-x-2 text-sm text-gray-500">
242
+ <a href={buildUrl('/architecture/domains')} className="hover:text-gray-700 hover:underline flex items-center gap-2">
243
+ <RectangleGroupIcon className="h-4 w-4" />
244
+ Domains
245
+ </a>
246
+ <ChevronRightIcon className="h-4 w-4" />
247
+ <a href={buildUrl('/architecture/services')} className="hover:text-gray-700 hover:underline flex items-center gap-2">
248
+ <ServerIcon className="h-4 w-4" />
249
+ Services
250
+ </a>
251
+ <ChevronRightIcon className="h-4 w-4" />
252
+ <a href={buildUrl('/architecture/messages')} className="hover:text-gray-700 hover:underline flex items-center gap-2">
253
+ <EnvelopeIcon className="h-4 w-4" />
254
+ Messages
255
+ </a>
256
+ {urlParams?.domainId && (
257
+ <>
258
+ <ChevronRightIcon className="h-4 w-4" />
259
+ <a
260
+ href={buildUrlWithParams(`/architecture/services`, {
261
+ domainName: urlParams.domainName,
262
+ domainId: urlParams.domainId,
263
+ serviceName: urlParams.serviceName,
264
+ serviceId: urlParams.serviceId,
265
+ })}
266
+ className="hover:text-gray-700 hover:underline"
267
+ >
268
+ {urlParams.domainId}
269
+ </a>
270
+ </>
271
+ )}
272
+ {urlParams?.serviceName && (
273
+ <>
274
+ <ChevronRightIcon className="h-4 w-4" />
275
+ <span className="text-gray-900">{urlParams.serviceName}</span>
276
+ </>
277
+ )}
278
+ </nav>
279
+
280
+ {/* Title Section */}
281
+ <div className="relative border-b border-gray-200 mb-4 pb-4">
282
+ <div className="md:flex md:items-start md:justify-between">
283
+ <div className="min-w-0 flex-1 max-w-lg">
284
+ <div className="flex items-center gap-2">
285
+ <h1 className="text-2xl font-bold leading-7 text-gray-900 sm:text-3xl sm:truncate">
286
+ {urlParams?.domainName ? `Messages in ${urlParams.serviceName}` : 'All Messages'}
287
+ </h1>
288
+ </div>
289
+ <p className="mt-2 text-sm text-gray-500">
290
+ {urlParams?.domainName
291
+ ? `Browse messages in the ${urlParams.serviceName} service`
292
+ : 'Browse and discover messages in your event-driven architecture'}
293
+ </p>
294
+ </div>
295
+
296
+ <div className="mt-6 md:mt-0 md:ml-4 flex-shrink-0">
297
+ <SearchBar
298
+ searchQuery={searchQuery}
299
+ onSearchChange={setSearchQuery}
300
+ placeholder="Search messages by name, summary, or services..."
301
+ totalResults={filteredAndSortedMessages.length}
302
+ totalItems={messages.length}
303
+ />
304
+ </div>
305
+ </div>
306
+ </div>
307
+
308
+ <div className="mb-8">
309
+ {/* Results count and top pagination */}
310
+ <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
311
+ {renderTypeFilters()}
312
+ {renderPaginationControls()}
313
+ </div>
314
+ </div>
315
+
316
+ {filteredAndSortedMessages.length > 0 && (
317
+ <div className={`rounded-xl overflow-hidden ${urlParams?.domainId ? 'bg-yellow-50 p-8 border-2 border-yellow-300' : ''}`}>
318
+ {urlParams?.domainName && (
319
+ <>
320
+ <div className="mb-6 flex items-center justify-between">
321
+ <div className="flex items-center gap-2">
322
+ <RectangleGroupIcon className="h-5 w-5 text-yellow-500" />
323
+ <a
324
+ href={buildUrlWithParams(`/architecture/services`, {
325
+ domainName: urlParams.domainName,
326
+ domainId: urlParams.domainId,
327
+ })}
328
+ className="text-2xl font-semibold text-gray-900 hover:underline"
329
+ >
330
+ {urlParams.domainName}
331
+ </a>
332
+ </div>
333
+ <div className="flex gap-2">
334
+ <a
335
+ href={buildUrl(`/visualiser/domains/${urlParams.domainId}`)}
336
+ className="inline-flex items-center px-3 py-2 text-sm font-medium bg-white border border-gray-300 rounded-md transition-colors duration-200"
337
+ >
338
+ View in visualizer
339
+ </a>
340
+ <a
341
+ href={buildUrl(`/docs/domains/${urlParams.domainId}`)}
342
+ className="inline-flex items-center px-3 py-2 text-sm font-medium text-black border border-gray-300 bg-white rounded-md transition-colors duration-200"
343
+ >
344
+ Read documentation
345
+ </a>
346
+ </div>
347
+ </div>
348
+ </>
349
+ )}
350
+
351
+ <div
352
+ className={`rounded-xl overflow-hidden ${urlParams?.serviceName ? 'bg-pink-50 p-8 border-2 border-dashed border-pink-300' : ''}`}
353
+ >
354
+ {urlParams?.serviceName ? (
355
+ <>
356
+ {/* <div className="h-2 bg-pink-500 -mx-8 -mt-8 mb-8"></div> */}
357
+ {/* Service Title */}
358
+ <div className="flex items-center gap-2 mb-8">
359
+ <ServerIcon className="h-6 w-6 text-pink-500" />
360
+ <h2 className="text-2xl font-semibold text-gray-900">{urlParams.serviceName}</h2>
361
+ <div className="flex gap-2 ml-auto">
362
+ <a
363
+ href={buildUrl(`/visualiser/services/${urlParams.serviceId}`)}
364
+ className="inline-flex items-center px-3 py-2 text-sm font-medium bg-white border border-gray-300 rounded-md transition-colors duration-200 hover:bg-gray-50"
365
+ >
366
+ View in visualizer
367
+ </a>
368
+ <a
369
+ href={buildUrl(`/docs/services/${urlParams.serviceId}`)}
370
+ className="inline-flex items-center px-3 py-2 text-sm font-medium text-black border border-gray-300 bg-white rounded-md transition-colors duration-200 hover:bg-gray-50"
371
+ >
372
+ Read documentation
373
+ </a>
374
+ </div>
375
+ </div>
376
+ <div className="grid grid-cols-3 gap-8 relative">
377
+ {/* Receives Section */}
378
+ <div className="bg-blue-50 bg-opacity-50 border border-blue-300 border-dashed rounded-lg p-4">
379
+ <div className="mb-6">
380
+ <h2 className="text-xl font-semibold text-gray-900 flex items-center gap-2">
381
+ <ServerIcon className="h-5 w-5 text-blue-500" />
382
+ Receives messages ({groupedMessages.receives?.length || 0})
383
+ </h2>
384
+ </div>
385
+ {groupedMessages.receives && groupedMessages.receives.length > 0 ? (
386
+ renderMessageGrid(groupedMessages.receives)
387
+ ) : (
388
+ <div className="text-center py-12">
389
+ <p className="text-gray-500">
390
+ {selectedTypes.length > 0
391
+ ? `Service does not receive ${selectedTypes.join(' or ')}`
392
+ : 'Service does not receive any messages'}
393
+ </p>
394
+ </div>
395
+ )}
396
+ </div>
397
+
398
+ {/* Arrow from Receives to Service */}
399
+ <div className="absolute left-[30%] top-1/2 -translate-y-1/2 flex items-center justify-center w-16">
400
+ <div className="w-full h-[3px] bg-blue-200 shadow-[0_0_0_1px_rgba(0,0,0,0.1)]"></div>
401
+ <div className="absolute right-0 w-4 h-4 border-t-[3px] border-r-[3px] border-blue-200 transform rotate-45 translate-x-1 translate-y-[-1px] shadow-[1px_-1px_0_1px_rgba(0,0,0,0.1)]"></div>
402
+ </div>
403
+
404
+ {/* Service Information */}
405
+ <div className="bg-white border-2 border-pink-100 rounded-lg p-3 flex items-center justify-center min-h-[80px]">
406
+ <div className="flex flex-col items-center gap-2">
407
+ <ServerIcon className="h-10 w-10 text-pink-500" />
408
+ <p className="text-lg font-medium text-gray-900">{urlParams.serviceName}</p>
409
+ </div>
410
+ </div>
411
+
412
+ {/* Arrow from Service to Sends */}
413
+ <div className="absolute right-[30%] top-1/2 -translate-y-1/2 flex items-center justify-center w-16">
414
+ <div className="w-full h-[3px] bg-green-200 shadow-[0_0_0_1px_rgba(0,0,0,0.1)]"></div>
415
+ <div className="absolute right-0 w-4 h-4 border-t-[3px] border-r-[3px] border-green-200 transform rotate-45 translate-x-1 translate-y-[-1px] shadow-[1px_-1px_0_1px_rgba(0,0,0,0.1)]"></div>
416
+ </div>
417
+
418
+ {/* Sends Section */}
419
+ <div className="bg-green-50 border border-green-300 border-dashed rounded-lg p-4">
420
+ <div className="mb-6">
421
+ <h2 className="text-xl font-semibold text-gray-900 flex items-center gap-2">
422
+ <ServerIcon className="h-5 w-5 text-emerald-500" />
423
+ Sends messages ({groupedMessages.sends?.length || 0})
424
+ </h2>
425
+ </div>
426
+ {groupedMessages.sends && groupedMessages.sends.length > 0 ? (
427
+ renderMessageGrid(groupedMessages.sends)
428
+ ) : (
429
+ <div className="text-center py-8">
430
+ <p className="text-gray-500">
431
+ {selectedTypes.length > 0
432
+ ? `Service does not send ${selectedTypes.join(' or ')}`
433
+ : 'Service does not send any messages'}
434
+ </p>
435
+ </div>
436
+ )}
437
+ </div>
438
+ </div>
439
+ </>
440
+ ) : (
441
+ <>
442
+ {renderMessageGrid(paginatedMessages)}
443
+ <div className="mt-8 border-t border-gray-200">{renderPaginationControls()}</div>
444
+ </>
445
+ )}
446
+ </div>
447
+ </div>
448
+ )}
449
+
450
+ {filteredAndSortedMessages.length === 0 && (
451
+ <div className="text-center py-12">
452
+ <p className="text-gray-500 text-lg">No messages found matching your criteria</p>
453
+ </div>
454
+ )}
455
+ </div>
456
+ );
457
+ }