@eventcatalog/core 2.26.1 → 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.
- package/dist/analytics/analytics.cjs +1 -1
- package/dist/analytics/analytics.js +2 -2
- package/dist/analytics/log-build.cjs +1 -1
- package/dist/analytics/log-build.js +3 -3
- package/dist/{chunk-M7ERKXSB.js → chunk-2VGR4HMJ.js} +1 -1
- package/dist/{chunk-JCGLXXSE.js → chunk-CTL6CH3C.js} +1 -1
- package/dist/{chunk-TOTPAQ4C.js → chunk-LMNJPHFP.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog.cjs +1 -1
- package/dist/eventcatalog.js +3 -3
- package/eventcatalog/astro.config.mjs +0 -3
- package/eventcatalog/src/components/Grids/DomainGrid.tsx +233 -0
- package/eventcatalog/src/components/Grids/MessageGrid.tsx +457 -0
- package/eventcatalog/src/components/Grids/ServiceGrid.tsx +364 -0
- package/eventcatalog/src/components/Grids/components.tsx +170 -0
- package/eventcatalog/src/components/Grids/utils.tsx +38 -0
- package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +29 -17
- package/eventcatalog/src/pages/architecture/[type]/index.astro +88 -0
- package/eventcatalog/src/pages/index.astro +237 -72
- package/eventcatalog/src/utils/url-builder.ts +20 -0
- package/eventcatalog/tailwind.config.mjs +11 -0
- package/package.json +2 -1
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import { useState, useMemo, useEffect } from 'react';
|
|
2
|
+
import { ServerIcon, ChevronRightIcon } 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 ServiceGridProps {
|
|
11
|
+
services: CollectionEntry<'services'>[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default function ServiceGrid({ services }: ServiceGridProps) {
|
|
15
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
16
|
+
const [currentPage, setCurrentPage] = useState(1);
|
|
17
|
+
const [selectedTypes, setSelectedTypes] = useState<CollectionMessageTypes[]>([]);
|
|
18
|
+
const ITEMS_PER_PAGE = 16;
|
|
19
|
+
const [urlParams, setUrlParams] = useState<{
|
|
20
|
+
serviceIds?: string[];
|
|
21
|
+
domainId?: string;
|
|
22
|
+
domainName?: string;
|
|
23
|
+
serviceName?: string;
|
|
24
|
+
} | null>(null);
|
|
25
|
+
|
|
26
|
+
// Effect to sync URL params with state
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const params = new URLSearchParams(window.location.search);
|
|
29
|
+
const serviceIds = params.get('serviceIds')?.split(',').filter(Boolean);
|
|
30
|
+
const domainId = params.get('domainId') || undefined;
|
|
31
|
+
const domainName = params.get('domainName') || undefined;
|
|
32
|
+
const serviceName = params.get('serviceName') || undefined;
|
|
33
|
+
setUrlParams({
|
|
34
|
+
serviceIds,
|
|
35
|
+
domainId,
|
|
36
|
+
domainName,
|
|
37
|
+
serviceName,
|
|
38
|
+
});
|
|
39
|
+
}, []);
|
|
40
|
+
|
|
41
|
+
const filteredAndSortedServices = useMemo(() => {
|
|
42
|
+
// Don't filter until we have URL params
|
|
43
|
+
if (urlParams === null) return [];
|
|
44
|
+
|
|
45
|
+
let result = [...services];
|
|
46
|
+
|
|
47
|
+
// Filter by service IDs if present
|
|
48
|
+
if (urlParams.serviceIds?.length) {
|
|
49
|
+
result = result.filter(
|
|
50
|
+
(service) => urlParams.serviceIds?.includes(service.data.id) && !service.data.id.includes('/versioned/')
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Filter by search query
|
|
55
|
+
if (searchQuery) {
|
|
56
|
+
const query = searchQuery.toLowerCase();
|
|
57
|
+
result = result.filter(
|
|
58
|
+
(service) =>
|
|
59
|
+
service.data.name?.toLowerCase().includes(query) ||
|
|
60
|
+
service.data.summary?.toLowerCase().includes(query) ||
|
|
61
|
+
service.data.sends?.some((message: any) => message.data.name.toLowerCase().includes(query)) ||
|
|
62
|
+
service.data.receives?.some((message: any) => message.data.name.toLowerCase().includes(query))
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Filter by selected message types
|
|
67
|
+
if (selectedTypes.length > 0) {
|
|
68
|
+
result = result.filter((service) => {
|
|
69
|
+
const hasMatchingSends = service.data.sends?.some((message: any) => selectedTypes.includes(message.collection));
|
|
70
|
+
const hasMatchingReceives = service.data.receives?.some((message: any) => selectedTypes.includes(message.collection));
|
|
71
|
+
return hasMatchingSends || hasMatchingReceives;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Sort by name by default
|
|
76
|
+
result.sort((a, b) => (a.data.name || a.data.id).localeCompare(b.data.name || b.data.id));
|
|
77
|
+
|
|
78
|
+
return result;
|
|
79
|
+
}, [services, searchQuery, urlParams, selectedTypes]);
|
|
80
|
+
|
|
81
|
+
// Add pagination calculation
|
|
82
|
+
const paginatedServices = useMemo(() => {
|
|
83
|
+
if (urlParams?.domainId || urlParams?.serviceIds?.length) {
|
|
84
|
+
return filteredAndSortedServices;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
|
|
88
|
+
return filteredAndSortedServices.slice(startIndex, startIndex + ITEMS_PER_PAGE);
|
|
89
|
+
}, [filteredAndSortedServices, currentPage, urlParams]);
|
|
90
|
+
|
|
91
|
+
const totalPages = useMemo(() => {
|
|
92
|
+
if (urlParams?.domainId || urlParams?.serviceIds?.length) return 1;
|
|
93
|
+
return Math.ceil(filteredAndSortedServices.length / ITEMS_PER_PAGE);
|
|
94
|
+
}, [filteredAndSortedServices.length, urlParams]);
|
|
95
|
+
|
|
96
|
+
// Reset pagination when search query or filters change
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
setCurrentPage(1);
|
|
99
|
+
}, [searchQuery, selectedTypes]);
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div>
|
|
103
|
+
{/* Breadcrumb */}
|
|
104
|
+
<nav className="mb-4 flex items-center space-x-2 text-sm text-gray-500">
|
|
105
|
+
<a href={buildUrl('/architecture/domains')} className="hover:text-gray-700 hover:underline flex items-center gap-2">
|
|
106
|
+
<RectangleGroupIcon className="h-4 w-4" />
|
|
107
|
+
Domains
|
|
108
|
+
</a>
|
|
109
|
+
<ChevronRightIcon className="h-4 w-4" />
|
|
110
|
+
<a href={buildUrl('/architecture/services')} className="hover:text-gray-700 hover:underline flex items-center gap-2">
|
|
111
|
+
<ServerIcon className="h-4 w-4" />
|
|
112
|
+
Services
|
|
113
|
+
</a>
|
|
114
|
+
{urlParams?.domainId && (
|
|
115
|
+
<>
|
|
116
|
+
<ChevronRightIcon className="h-4 w-4" />
|
|
117
|
+
<span className="text-gray-900">{urlParams.domainId}</span>
|
|
118
|
+
</>
|
|
119
|
+
)}
|
|
120
|
+
</nav>
|
|
121
|
+
|
|
122
|
+
{/* Title Section */}
|
|
123
|
+
<div className="relative border-b border-gray-200 mb-4 pb-4">
|
|
124
|
+
<div className="md:flex md:items-start md:justify-between">
|
|
125
|
+
<div className="min-w-0 flex-1 max-w-lg">
|
|
126
|
+
<div className="flex items-center gap-2">
|
|
127
|
+
<h1 className="text-2xl font-bold leading-7 text-gray-900 sm:text-3xl sm:truncate">
|
|
128
|
+
{urlParams?.domainId
|
|
129
|
+
? `Services in the ${urlParams.domainName} domain (${filteredAndSortedServices.length})`
|
|
130
|
+
: 'All Services'}
|
|
131
|
+
</h1>
|
|
132
|
+
</div>
|
|
133
|
+
<p className="mt-2 text-sm text-gray-500">
|
|
134
|
+
{urlParams?.domainId
|
|
135
|
+
? `Browse services in the ${urlParams.domainId} domain`
|
|
136
|
+
: 'Browse and discover services in your event-driven architecture'}
|
|
137
|
+
</p>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<div className="mt-6 md:mt-0 md:ml-4 flex-shrink-0">
|
|
141
|
+
<SearchBar
|
|
142
|
+
searchQuery={searchQuery}
|
|
143
|
+
onSearchChange={setSearchQuery}
|
|
144
|
+
placeholder="Search services by name, summary, or messages..."
|
|
145
|
+
totalResults={filteredAndSortedServices.length}
|
|
146
|
+
totalItems={services.length}
|
|
147
|
+
/>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<div className="mb-8">
|
|
153
|
+
{/* Results count and pagination */}
|
|
154
|
+
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
|
155
|
+
<TypeFilters
|
|
156
|
+
selectedTypes={selectedTypes}
|
|
157
|
+
onTypeChange={setSelectedTypes}
|
|
158
|
+
filteredCount={filteredAndSortedServices.length}
|
|
159
|
+
totalCount={services.length}
|
|
160
|
+
/>
|
|
161
|
+
<div className="text-sm text-gray-500">
|
|
162
|
+
{urlParams?.domainId || urlParams?.serviceIds?.length ? (
|
|
163
|
+
<span>
|
|
164
|
+
Showing {filteredAndSortedServices.length} services in the {urlParams.domainId} domain
|
|
165
|
+
</span>
|
|
166
|
+
) : (
|
|
167
|
+
<span>
|
|
168
|
+
Showing {(currentPage - 1) * ITEMS_PER_PAGE + 1} to{' '}
|
|
169
|
+
{Math.min(currentPage * ITEMS_PER_PAGE, filteredAndSortedServices.length)} of {filteredAndSortedServices.length}{' '}
|
|
170
|
+
services
|
|
171
|
+
</span>
|
|
172
|
+
)}
|
|
173
|
+
</div>
|
|
174
|
+
{!(urlParams?.domainId || urlParams?.serviceIds?.length) && (
|
|
175
|
+
<Pagination
|
|
176
|
+
currentPage={currentPage}
|
|
177
|
+
totalPages={totalPages}
|
|
178
|
+
totalItems={filteredAndSortedServices.length}
|
|
179
|
+
itemsPerPage={ITEMS_PER_PAGE}
|
|
180
|
+
onPageChange={setCurrentPage}
|
|
181
|
+
/>
|
|
182
|
+
)}
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
{filteredAndSortedServices.length > 0 && (
|
|
187
|
+
<div className={`rounded-xl overflow-hidden ${urlParams?.domainId ? 'bg-yellow-50 p-8 border-2 border-yellow-400' : ''}`}>
|
|
188
|
+
{urlParams?.domainName && (
|
|
189
|
+
<>
|
|
190
|
+
<div className="mb-6 flex items-center justify-between">
|
|
191
|
+
<div className="flex items-center gap-2">
|
|
192
|
+
<RectangleGroupIcon className="h-5 w-5 text-yellow-500" />
|
|
193
|
+
<span className="text-2xl font-semibold text-gray-900">{urlParams.domainName}</span>
|
|
194
|
+
</div>
|
|
195
|
+
<div className="flex gap-2">
|
|
196
|
+
<a
|
|
197
|
+
href={buildUrl(`/visualiser/domains/${urlParams.domainId}`)}
|
|
198
|
+
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"
|
|
199
|
+
>
|
|
200
|
+
View in visualizer
|
|
201
|
+
</a>
|
|
202
|
+
<a
|
|
203
|
+
href={buildUrl(`/docs/domains/${urlParams.domainId}`)}
|
|
204
|
+
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"
|
|
205
|
+
>
|
|
206
|
+
Read documentation
|
|
207
|
+
</a>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
</>
|
|
211
|
+
)}
|
|
212
|
+
|
|
213
|
+
<div className="grid grid-cols-1 sm:grid-cols-1 lg:grid-cols-2 xl:grid-cols-2 gap-6">
|
|
214
|
+
{paginatedServices.map((service) => {
|
|
215
|
+
return (
|
|
216
|
+
<a
|
|
217
|
+
key={service.data.id}
|
|
218
|
+
href={buildUrlWithParams('/architecture/messages', {
|
|
219
|
+
serviceName: service.data.name,
|
|
220
|
+
serviceId: service.data.id,
|
|
221
|
+
domainId: urlParams?.domainId,
|
|
222
|
+
domainName: urlParams?.domainName,
|
|
223
|
+
})}
|
|
224
|
+
className="group hover:bg-pink-50 bg-white border-2 border-dashed border-pink-400 rounded-lg shadow-sm hover:shadow-lg transition-all duration-200 overflow-hidden"
|
|
225
|
+
>
|
|
226
|
+
<div className="p-6">
|
|
227
|
+
<div className="flex items-center justify-between mb-3">
|
|
228
|
+
<div className="flex items-center gap-2">
|
|
229
|
+
<ServerIcon className="h-5 w-5 text-pink-500" />
|
|
230
|
+
<h3 className="text-lg font-semibold text-gray-900 truncate group-hover:underline transition-colors duration-200">
|
|
231
|
+
{service.data.name || service.data.id} (v{service.data.version})
|
|
232
|
+
</h3>
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
|
|
236
|
+
{service.data.summary && (
|
|
237
|
+
<p className="text-gray-600 text-sm line-clamp-2 min-h-[2.5rem]">{service.data.summary}</p>
|
|
238
|
+
)}
|
|
239
|
+
|
|
240
|
+
<div className="space-y-4">
|
|
241
|
+
{/* Messages Section */}
|
|
242
|
+
{!urlParams?.serviceName && (
|
|
243
|
+
<div className="flex items-center gap-4">
|
|
244
|
+
<div className="flex-1 h-full flex flex-col bg-blue-100 border border-blue-300 rounded-lg p-4">
|
|
245
|
+
<div className="space-y-2 flex-1">
|
|
246
|
+
{service.data.receives
|
|
247
|
+
?.filter(
|
|
248
|
+
(message: any) => selectedTypes.length === 0 || selectedTypes.includes(message.collection)
|
|
249
|
+
)
|
|
250
|
+
?.map((message: any) => {
|
|
251
|
+
const { Icon, color } = getCollectionStyles(message.collection);
|
|
252
|
+
return (
|
|
253
|
+
<a
|
|
254
|
+
key={message.data.name}
|
|
255
|
+
href={buildUrl(`/docs/${message.collection}/${message.data.id}/${message.data.version}`)}
|
|
256
|
+
className="group flex border border-gray-200 items-center gap-1 rounded-md text-[11px] font-medium hover:bg-gray-50 transition-colors duration-200 bg-white"
|
|
257
|
+
>
|
|
258
|
+
<div className="bg-white border-r border-gray-200 px-2 py-1.5 rounded-l-md">
|
|
259
|
+
<Icon className={`h-3 w-3 text-${color}-500`} />
|
|
260
|
+
</div>
|
|
261
|
+
<span className="px-1 py-1">{message.data.name}</span>
|
|
262
|
+
</a>
|
|
263
|
+
);
|
|
264
|
+
})}
|
|
265
|
+
{(!service.data.receives?.length ||
|
|
266
|
+
(selectedTypes.length > 0 &&
|
|
267
|
+
!service.data.receives?.some((message: any) =>
|
|
268
|
+
selectedTypes.includes(message.collection)
|
|
269
|
+
))) && (
|
|
270
|
+
<div className="text-center py-4">
|
|
271
|
+
<p className="text-gray-500 text-[10px]">
|
|
272
|
+
{selectedTypes.length > 0
|
|
273
|
+
? `Service does not receive ${selectedTypes.join(' or ')}`
|
|
274
|
+
: 'Service does not receive any messages'}
|
|
275
|
+
</p>
|
|
276
|
+
</div>
|
|
277
|
+
)}
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
|
|
281
|
+
<div className="flex items-center gap-2">
|
|
282
|
+
<div className="w-4 h-[2px] bg-blue-200"></div>
|
|
283
|
+
<div className="bg-white border-2 border-pink-100 rounded-lg p-4 shadow-sm">
|
|
284
|
+
<div className="flex flex-col items-center gap-3">
|
|
285
|
+
<ServerIcon className="h-8 w-8 text-pink-500" />
|
|
286
|
+
<div className="text-center">
|
|
287
|
+
<p className="text-sm font-medium text-gray-900">{service.data.name || service.data.id}</p>
|
|
288
|
+
<p className="text-xs text-gray-500">v{service.data.version}</p>
|
|
289
|
+
</div>
|
|
290
|
+
</div>
|
|
291
|
+
</div>
|
|
292
|
+
<div className="w-4 h-[2px] bg-emerald-200"></div>
|
|
293
|
+
</div>
|
|
294
|
+
|
|
295
|
+
<div className="flex-1 h-full flex flex-col bg-green-100 border border-green-300 rounded-lg p-4">
|
|
296
|
+
<div className="space-y-2 flex-1">
|
|
297
|
+
{service.data.sends
|
|
298
|
+
?.filter(
|
|
299
|
+
(message: any) => selectedTypes.length === 0 || selectedTypes.includes(message.collection)
|
|
300
|
+
)
|
|
301
|
+
?.map((message: any) => {
|
|
302
|
+
const { Icon, color } = getCollectionStyles(message.collection);
|
|
303
|
+
return (
|
|
304
|
+
<a
|
|
305
|
+
key={message.data.name}
|
|
306
|
+
href={buildUrl(`/docs/${message.collection}/${message.data.id}/${message.data.version}`)}
|
|
307
|
+
className="group flex border border-gray-200 items-center gap-1 rounded-md text-[11px] font-medium hover:bg-gray-50 transition-colors duration-200 bg-white"
|
|
308
|
+
>
|
|
309
|
+
<div className="bg-white border-r border-gray-200 px-2 py-1.5 rounded-l-md">
|
|
310
|
+
<Icon className={`h-3 w-3 text-${color}-500`} />
|
|
311
|
+
</div>
|
|
312
|
+
<span className="px-1 py-1">{message.data.name}</span>
|
|
313
|
+
</a>
|
|
314
|
+
);
|
|
315
|
+
})}
|
|
316
|
+
{(!service.data.sends?.length ||
|
|
317
|
+
(selectedTypes.length > 0 &&
|
|
318
|
+
!service.data.sends?.some((message: any) => selectedTypes.includes(message.collection)))) && (
|
|
319
|
+
<div className="text-center py-4 ">
|
|
320
|
+
<p className="text-gray-500 text-[10px]">
|
|
321
|
+
{selectedTypes.length > 0
|
|
322
|
+
? `Service does not send ${selectedTypes.join(' or ')}`
|
|
323
|
+
: 'Service does not send any messages'}
|
|
324
|
+
</p>
|
|
325
|
+
</div>
|
|
326
|
+
)}
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
329
|
+
</div>
|
|
330
|
+
)}
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
</a>
|
|
334
|
+
);
|
|
335
|
+
})}
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
)}
|
|
339
|
+
|
|
340
|
+
{filteredAndSortedServices.length === 0 && (
|
|
341
|
+
<div className="text-center py-12 bg-gray-50 rounded-lg">
|
|
342
|
+
<p className="text-gray-500 text-lg">
|
|
343
|
+
{selectedTypes.length > 0
|
|
344
|
+
? `No services found that ${selectedTypes.length > 1 ? 'handle' : 'handles'} ${selectedTypes.join(' or ')} messages`
|
|
345
|
+
: 'No services found matching your criteria'}
|
|
346
|
+
</p>
|
|
347
|
+
</div>
|
|
348
|
+
)}
|
|
349
|
+
|
|
350
|
+
{/* Bottom pagination */}
|
|
351
|
+
{!(urlParams?.domainId || urlParams?.serviceIds?.length) && (
|
|
352
|
+
<div className="mt-8 border-t border-gray-200">
|
|
353
|
+
<Pagination
|
|
354
|
+
currentPage={currentPage}
|
|
355
|
+
totalPages={totalPages}
|
|
356
|
+
totalItems={filteredAndSortedServices.length}
|
|
357
|
+
itemsPerPage={ITEMS_PER_PAGE}
|
|
358
|
+
onPageChange={setCurrentPage}
|
|
359
|
+
/>
|
|
360
|
+
</div>
|
|
361
|
+
)}
|
|
362
|
+
</div>
|
|
363
|
+
);
|
|
364
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
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-sm 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-none">
|
|
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-sm" 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
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { BoltIcon, ChatBubbleLeftIcon, MagnifyingGlassIcon, EnvelopeIcon } from '@heroicons/react/24/outline';
|
|
2
|
+
import type { CollectionMessageTypes } from '@types';
|
|
3
|
+
|
|
4
|
+
export const getCollectionStyles = (collection: CollectionMessageTypes) => {
|
|
5
|
+
switch (collection) {
|
|
6
|
+
case 'events':
|
|
7
|
+
return { color: 'orange', Icon: BoltIcon };
|
|
8
|
+
case 'commands':
|
|
9
|
+
return { color: 'blue', Icon: ChatBubbleLeftIcon };
|
|
10
|
+
case 'queries':
|
|
11
|
+
return { color: 'green', Icon: MagnifyingGlassIcon };
|
|
12
|
+
default:
|
|
13
|
+
return { color: 'gray', Icon: EnvelopeIcon };
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export interface PaginationProps {
|
|
18
|
+
currentPage: number;
|
|
19
|
+
totalPages: number;
|
|
20
|
+
totalItems: number;
|
|
21
|
+
itemsPerPage: number;
|
|
22
|
+
onPageChange: (page: number) => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface SearchBarProps {
|
|
26
|
+
searchQuery: string;
|
|
27
|
+
onSearchChange: (query: string) => void;
|
|
28
|
+
placeholder?: string;
|
|
29
|
+
totalResults?: number;
|
|
30
|
+
totalItems?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface TypeFilterProps {
|
|
34
|
+
selectedTypes: CollectionMessageTypes[];
|
|
35
|
+
onTypeChange: (types: CollectionMessageTypes[]) => void;
|
|
36
|
+
filteredCount?: number;
|
|
37
|
+
totalCount?: number;
|
|
38
|
+
}
|
|
@@ -9,6 +9,7 @@ import { BookOpenText, Workflow, TableProperties, House, BookUser, MessageSquare
|
|
|
9
9
|
import Header from '../components/Header.astro';
|
|
10
10
|
import SEO from '../components/Seo.astro';
|
|
11
11
|
import SideNav from '../components/SideNav/SideNav.astro';
|
|
12
|
+
import '@fontsource/inter';
|
|
12
13
|
|
|
13
14
|
import { getCommands } from '@utils/commands';
|
|
14
15
|
import { getDomains } from '@utils/collections/domains';
|
|
@@ -92,6 +93,15 @@ const navigationItems = [
|
|
|
92
93
|
current: currentPath.includes('/directory'),
|
|
93
94
|
sidebar: false,
|
|
94
95
|
},
|
|
96
|
+
{
|
|
97
|
+
id: '/architecture',
|
|
98
|
+
label: 'Architecture',
|
|
99
|
+
icon: BookUser,
|
|
100
|
+
href: buildUrl('/architecture/domains'),
|
|
101
|
+
current: currentPath.includes('/architecture'),
|
|
102
|
+
sidebar: false,
|
|
103
|
+
hidden: true,
|
|
104
|
+
},
|
|
95
105
|
{
|
|
96
106
|
id: '/chat',
|
|
97
107
|
label: 'AI Assistant',
|
|
@@ -139,23 +149,25 @@ const canPageBeEmbedded = process.env.ENABLE_EMBED === 'true';
|
|
|
139
149
|
<nav class="flex flex-col h-[84vh] justify-between">
|
|
140
150
|
<div class="flex flex-col items-center flex-1 space-y-8">
|
|
141
151
|
{
|
|
142
|
-
navigationItems
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
152
|
+
navigationItems
|
|
153
|
+
.filter((item) => !item.hidden)
|
|
154
|
+
.map((item) => {
|
|
155
|
+
return (
|
|
156
|
+
<a
|
|
157
|
+
id={item.id}
|
|
158
|
+
data-role="nav-item"
|
|
159
|
+
href={item.href}
|
|
160
|
+
class={`p-1.5 inline-block transition-colors duration-200 rounded-lg ${item.current ? 'text-white bg-gradient-to-b from-purple-500 to-purple-700' : 'hover:bg-gradient-to-r hover:from-purple-500 hover:to-purple-700 hover:text-white text-gray-700'}`}
|
|
161
|
+
>
|
|
162
|
+
<div class="has-tooltip">
|
|
163
|
+
<span class="tooltip rounded shadow-lg p-1 text-xs bg-gradient-to-l from-purple-500 to-purple-700 text-white ml-10">
|
|
164
|
+
{item.label}
|
|
165
|
+
</span>
|
|
166
|
+
<item.icon className="h-6 w-6 " />
|
|
167
|
+
</div>
|
|
168
|
+
</a>
|
|
169
|
+
);
|
|
170
|
+
})
|
|
159
171
|
}
|
|
160
172
|
</div>
|
|
161
173
|
</nav>
|