@eventcatalog/core 2.26.1 → 2.28.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 (37) 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-TOTPAQ4C.js → chunk-KGWJTMWU.js} +1 -1
  6. package/dist/{chunk-M7ERKXSB.js → chunk-KYGD25IE.js} +1 -1
  7. package/dist/{chunk-JCGLXXSE.js → chunk-RCPEAVRY.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.config.d.cts +1 -4
  12. package/dist/eventcatalog.config.d.ts +1 -4
  13. package/dist/eventcatalog.js +3 -3
  14. package/eventcatalog/astro.config.mjs +0 -3
  15. package/eventcatalog/public/logo.svg +14 -0
  16. package/eventcatalog/src/components/Grids/DomainGrid.tsx +233 -0
  17. package/eventcatalog/src/components/Grids/MessageGrid.tsx +457 -0
  18. package/eventcatalog/src/components/Grids/ServiceGrid.tsx +362 -0
  19. package/eventcatalog/src/components/Grids/components.tsx +170 -0
  20. package/eventcatalog/src/components/Grids/utils.tsx +38 -0
  21. package/eventcatalog/src/components/SideNav/ListViewSideBar/components/CollapsibleGroup.tsx +46 -0
  22. package/eventcatalog/src/components/SideNav/ListViewSideBar/components/MessageList.tsx +31 -0
  23. package/eventcatalog/src/components/SideNav/ListViewSideBar/index.tsx +390 -0
  24. package/eventcatalog/src/components/SideNav/ListViewSideBar/types.ts +48 -0
  25. package/eventcatalog/src/components/SideNav/ListViewSideBar/utils.ts +103 -0
  26. package/eventcatalog/src/components/SideNav/SideNav.astro +8 -7
  27. package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +34 -22
  28. package/eventcatalog/src/pages/architecture/[type]/index.astro +14 -0
  29. package/eventcatalog/src/pages/architecture/architecture.astro +81 -0
  30. package/eventcatalog/src/pages/architecture/docs/[type]/index.astro +14 -0
  31. package/eventcatalog/src/pages/index.astro +237 -72
  32. package/eventcatalog/src/utils/url-builder.ts +20 -0
  33. package/eventcatalog/tailwind.config.mjs +11 -0
  34. package/package.json +2 -1
  35. package/eventcatalog/src/components/SideNav/CatalogResourcesSideBar/getCatalogResources.ts +0 -65
  36. package/eventcatalog/src/components/SideNav/CatalogResourcesSideBar/index.tsx +0 -138
  37. package/eventcatalog/src/components/SideNav/CatalogResourcesSideBar/styles.css +0 -8
@@ -0,0 +1,362 @@
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 ? `${urlParams.domainName} Architecture` : 'All Services'}
129
+ </h1>
130
+ </div>
131
+ <p className="mt-2 text-sm text-gray-500">
132
+ {urlParams?.domainId
133
+ ? `Browse services and messages in the ${urlParams.domainId} domain`
134
+ : 'Browse and discover services in your event-driven architecture'}
135
+ </p>
136
+ </div>
137
+
138
+ <div className="mt-6 md:mt-0 md:ml-4 flex-shrink-0">
139
+ <SearchBar
140
+ searchQuery={searchQuery}
141
+ onSearchChange={setSearchQuery}
142
+ placeholder="Search services by name, summary, or messages..."
143
+ totalResults={filteredAndSortedServices.length}
144
+ totalItems={services.length}
145
+ />
146
+ </div>
147
+ </div>
148
+ </div>
149
+
150
+ <div className="mb-8">
151
+ {/* Results count and pagination */}
152
+ <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
153
+ <TypeFilters
154
+ selectedTypes={selectedTypes}
155
+ onTypeChange={setSelectedTypes}
156
+ filteredCount={filteredAndSortedServices.length}
157
+ totalCount={services.length}
158
+ />
159
+ <div className="text-sm text-gray-500">
160
+ {urlParams?.domainId || urlParams?.serviceIds?.length ? (
161
+ <span>
162
+ Showing {filteredAndSortedServices.length} services in the {urlParams.domainId} domain
163
+ </span>
164
+ ) : (
165
+ <span>
166
+ Showing {(currentPage - 1) * ITEMS_PER_PAGE + 1} to{' '}
167
+ {Math.min(currentPage * ITEMS_PER_PAGE, filteredAndSortedServices.length)} of {filteredAndSortedServices.length}{' '}
168
+ services
169
+ </span>
170
+ )}
171
+ </div>
172
+ {!(urlParams?.domainId || urlParams?.serviceIds?.length) && (
173
+ <Pagination
174
+ currentPage={currentPage}
175
+ totalPages={totalPages}
176
+ totalItems={filteredAndSortedServices.length}
177
+ itemsPerPage={ITEMS_PER_PAGE}
178
+ onPageChange={setCurrentPage}
179
+ />
180
+ )}
181
+ </div>
182
+ </div>
183
+
184
+ {filteredAndSortedServices.length > 0 && (
185
+ <div className={`rounded-xl overflow-hidden ${urlParams?.domainId ? 'bg-yellow-50 p-8 border-2 border-yellow-400' : ''}`}>
186
+ {urlParams?.domainName && (
187
+ <>
188
+ <div className="mb-6 flex items-center justify-between">
189
+ <div className="flex items-center gap-2">
190
+ <RectangleGroupIcon className="h-5 w-5 text-yellow-500" />
191
+ <span className="text-2xl font-semibold text-gray-900">{urlParams.domainName}</span>
192
+ </div>
193
+ <div className="flex gap-2">
194
+ <a
195
+ href={buildUrl(`/visualiser/domains/${urlParams.domainId}`)}
196
+ 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"
197
+ >
198
+ View in visualizer
199
+ </a>
200
+ <a
201
+ href={buildUrl(`/docs/domains/${urlParams.domainId}`)}
202
+ 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"
203
+ >
204
+ Read documentation
205
+ </a>
206
+ </div>
207
+ </div>
208
+ </>
209
+ )}
210
+
211
+ <div className="grid grid-cols-1 sm:grid-cols-1 lg:grid-cols-2 xl:grid-cols-2 gap-6">
212
+ {paginatedServices.map((service) => {
213
+ return (
214
+ <a
215
+ key={service.data.id}
216
+ href={buildUrlWithParams('/architecture/messages', {
217
+ serviceName: service.data.name,
218
+ serviceId: service.data.id,
219
+ domainId: urlParams?.domainId,
220
+ domainName: urlParams?.domainName,
221
+ })}
222
+ 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"
223
+ >
224
+ <div className="p-6">
225
+ <div className="flex items-center justify-between mb-3">
226
+ <div className="flex items-center gap-2">
227
+ <ServerIcon className="h-5 w-5 text-pink-500" />
228
+ <h3 className="text-lg font-semibold text-gray-900 truncate group-hover:underline transition-colors duration-200">
229
+ {service.data.name || service.data.id} (v{service.data.version})
230
+ </h3>
231
+ </div>
232
+ </div>
233
+
234
+ {service.data.summary && (
235
+ <p className="text-gray-600 text-sm line-clamp-2 min-h-[2.5rem]">{service.data.summary}</p>
236
+ )}
237
+
238
+ <div className="space-y-4">
239
+ {/* Messages Section */}
240
+ {!urlParams?.serviceName && (
241
+ <div className="flex items-center gap-4">
242
+ <div className="flex-1 h-full flex flex-col bg-blue-100 border border-blue-300 rounded-lg p-4">
243
+ <div className="space-y-2 flex-1">
244
+ {service.data.receives
245
+ ?.filter(
246
+ (message: any) => selectedTypes.length === 0 || selectedTypes.includes(message.collection)
247
+ )
248
+ ?.map((message: any) => {
249
+ const { Icon, color } = getCollectionStyles(message.collection);
250
+ return (
251
+ <a
252
+ key={message.data.name}
253
+ href={buildUrl(`/docs/${message.collection}/${message.data.id}/${message.data.version}`)}
254
+ 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"
255
+ >
256
+ <div className="bg-white border-r border-gray-200 px-2 py-1.5 rounded-l-md">
257
+ <Icon className={`h-3 w-3 text-${color}-500`} />
258
+ </div>
259
+ <span className="px-1 py-1">{message.data.name}</span>
260
+ </a>
261
+ );
262
+ })}
263
+ {(!service.data.receives?.length ||
264
+ (selectedTypes.length > 0 &&
265
+ !service.data.receives?.some((message: any) =>
266
+ selectedTypes.includes(message.collection)
267
+ ))) && (
268
+ <div className="text-center py-4">
269
+ <p className="text-gray-500 text-[10px]">
270
+ {selectedTypes.length > 0
271
+ ? `Service does not receive ${selectedTypes.join(' or ')}`
272
+ : 'Service does not receive any messages'}
273
+ </p>
274
+ </div>
275
+ )}
276
+ </div>
277
+ </div>
278
+
279
+ <div className="flex items-center gap-2">
280
+ <div className="w-4 h-[2px] bg-blue-200"></div>
281
+ <div className="bg-white border-2 border-pink-100 rounded-lg p-4 shadow-sm">
282
+ <div className="flex flex-col items-center gap-3">
283
+ <ServerIcon className="h-8 w-8 text-pink-500" />
284
+ <div className="text-center">
285
+ <p className="text-sm font-medium text-gray-900">{service.data.name || service.data.id}</p>
286
+ <p className="text-xs text-gray-500">v{service.data.version}</p>
287
+ </div>
288
+ </div>
289
+ </div>
290
+ <div className="w-4 h-[2px] bg-emerald-200"></div>
291
+ </div>
292
+
293
+ <div className="flex-1 h-full flex flex-col bg-green-100 border border-green-300 rounded-lg p-4">
294
+ <div className="space-y-2 flex-1">
295
+ {service.data.sends
296
+ ?.filter(
297
+ (message: any) => selectedTypes.length === 0 || selectedTypes.includes(message.collection)
298
+ )
299
+ ?.map((message: any) => {
300
+ const { Icon, color } = getCollectionStyles(message.collection);
301
+ return (
302
+ <a
303
+ key={message.data.name}
304
+ href={buildUrl(`/docs/${message.collection}/${message.data.id}/${message.data.version}`)}
305
+ 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"
306
+ >
307
+ <div className="bg-white border-r border-gray-200 px-2 py-1.5 rounded-l-md">
308
+ <Icon className={`h-3 w-3 text-${color}-500`} />
309
+ </div>
310
+ <span className="px-1 py-1">{message.data.name}</span>
311
+ </a>
312
+ );
313
+ })}
314
+ {(!service.data.sends?.length ||
315
+ (selectedTypes.length > 0 &&
316
+ !service.data.sends?.some((message: any) => selectedTypes.includes(message.collection)))) && (
317
+ <div className="text-center py-4 ">
318
+ <p className="text-gray-500 text-[10px]">
319
+ {selectedTypes.length > 0
320
+ ? `Service does not send ${selectedTypes.join(' or ')}`
321
+ : 'Service does not send any messages'}
322
+ </p>
323
+ </div>
324
+ )}
325
+ </div>
326
+ </div>
327
+ </div>
328
+ )}
329
+ </div>
330
+ </div>
331
+ </a>
332
+ );
333
+ })}
334
+ </div>
335
+ </div>
336
+ )}
337
+
338
+ {filteredAndSortedServices.length === 0 && (
339
+ <div className="text-center py-12 bg-gray-50 rounded-lg">
340
+ <p className="text-gray-500 text-lg">
341
+ {selectedTypes.length > 0
342
+ ? `No services found that ${selectedTypes.length > 1 ? 'handle' : 'handles'} ${selectedTypes.join(' or ')} messages`
343
+ : 'No services found matching your criteria'}
344
+ </p>
345
+ </div>
346
+ )}
347
+
348
+ {/* Bottom pagination */}
349
+ {!(urlParams?.domainId || urlParams?.serviceIds?.length) && (
350
+ <div className="mt-8 border-t border-gray-200">
351
+ <Pagination
352
+ currentPage={currentPage}
353
+ totalPages={totalPages}
354
+ totalItems={filteredAndSortedServices.length}
355
+ itemsPerPage={ITEMS_PER_PAGE}
356
+ onPageChange={setCurrentPage}
357
+ />
358
+ </div>
359
+ )}
360
+ </div>
361
+ );
362
+ }
@@ -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
+ }
@@ -0,0 +1,46 @@
1
+ import React from 'react';
2
+ import { ChevronDownIcon } from '@heroicons/react/24/outline';
3
+
4
+ interface CollapsibleGroupProps {
5
+ isCollapsed: boolean;
6
+ onToggle: () => void;
7
+ title: React.ReactNode;
8
+ children: React.ReactNode;
9
+ className?: string;
10
+ }
11
+
12
+ const CollapsibleGroup: React.FC<CollapsibleGroupProps> = ({ isCollapsed, onToggle, title, children, className = '' }) => (
13
+ <div className={className}>
14
+ <div className="flex items-center">
15
+ <button
16
+ onClick={(e) => {
17
+ e.stopPropagation();
18
+ onToggle();
19
+ }}
20
+ className="p-1 hover:bg-gray-100 rounded-md"
21
+ >
22
+ <div className={`transition-transform duration-150 ${isCollapsed ? '' : 'rotate-180'}`}>
23
+ <ChevronDownIcon className="h-3 w-3 text-gray-500" />
24
+ </div>
25
+ </button>
26
+ {typeof title === 'string' ? (
27
+ <button
28
+ onClick={(e) => {
29
+ e.stopPropagation();
30
+ onToggle();
31
+ }}
32
+ className="flex-grow flex items-center justify-between px-2 py-0.5 text-xs font-bold rounded-md"
33
+ >
34
+ {title}
35
+ </button>
36
+ ) : (
37
+ title
38
+ )}
39
+ </div>
40
+ <div className={`overflow-hidden transition-[height] duration-150 ease-out ${isCollapsed ? 'h-0' : 'h-auto'}`}>
41
+ {children}
42
+ </div>
43
+ </div>
44
+ );
45
+
46
+ export default CollapsibleGroup;
@@ -0,0 +1,31 @@
1
+ import React from 'react';
2
+ import { getMessageColorByCollection, getMessageCollectionName } from '../index';
3
+
4
+ interface MessageListProps {
5
+ messages: any[];
6
+ decodedCurrentPath: string;
7
+ }
8
+
9
+ const MessageList: React.FC<MessageListProps> = ({ messages, decodedCurrentPath }) => (
10
+ <ul className="space-y-0 border-l border-gray-200/80 ml-[9px] pl-4">
11
+ {messages.map((message: any) => (
12
+ <li key={message.id} data-active={decodedCurrentPath === message.href}>
13
+ <a
14
+ href={message.href}
15
+ className={`flex items-center justify-between px-2 py-1.5 text-xs text-gray-600 hover:bg-purple-100 rounded-md ${
16
+ decodedCurrentPath.includes(message.href) ? 'bg-purple-100 ' : 'hover:bg-purple-100'
17
+ }`}
18
+ >
19
+ <span className="truncate">{message.data.name}</span>
20
+ <span
21
+ className={`ml-2 text-[10px] font-medium px-2 uppercase py-0.5 rounded ${getMessageColorByCollection(message.collection)}`}
22
+ >
23
+ {getMessageCollectionName(message.collection)}
24
+ </span>
25
+ </a>
26
+ </li>
27
+ ))}
28
+ </ul>
29
+ );
30
+
31
+ export default MessageList;