@eventcatalog/core 2.53.1 → 2.54.1
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-Q363JHA2.js → chunk-7NKHMS4K.js} +1 -1
- package/dist/{chunk-K5LYWIYR.js → chunk-EUDMSTHT.js} +1 -1
- package/dist/{chunk-LUUBKWYP.js → chunk-MCSZBCOG.js} +5 -0
- package/dist/{chunk-BQDTPLND.js → chunk-ONQOSPKG.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog.cjs +22 -1
- package/dist/eventcatalog.js +22 -4
- package/dist/features.cjs +6 -0
- package/dist/features.d.cts +2 -1
- package/dist/features.d.ts +2 -1
- package/dist/features.js +3 -1
- package/eventcatalog/astro.config.mjs +0 -2
- package/eventcatalog/src/components/Header.astro +1 -1
- package/eventcatalog/src/components/Search/Search.astro +102 -0
- package/eventcatalog/src/components/Search/SearchModal.tsx +816 -0
- package/eventcatalog/src/pages/auth/error.astro +47 -35
- package/eventcatalog/src/pages/chat/feature.astro +137 -125
- package/package.json +3 -2
- package/eventcatalog/src/components/Search.astro +0 -128
|
@@ -0,0 +1,816 @@
|
|
|
1
|
+
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
MagnifyingGlassIcon,
|
|
4
|
+
QueueListIcon,
|
|
5
|
+
RectangleGroupIcon,
|
|
6
|
+
BoltIcon,
|
|
7
|
+
ChatBubbleLeftIcon,
|
|
8
|
+
ServerIcon,
|
|
9
|
+
UserGroupIcon,
|
|
10
|
+
UserIcon,
|
|
11
|
+
BookOpenIcon,
|
|
12
|
+
DocumentTextIcon,
|
|
13
|
+
CubeIcon,
|
|
14
|
+
} from '@heroicons/react/24/outline';
|
|
15
|
+
|
|
16
|
+
interface SearchResult {
|
|
17
|
+
id: string;
|
|
18
|
+
name: string;
|
|
19
|
+
type: string;
|
|
20
|
+
description: string;
|
|
21
|
+
url: string;
|
|
22
|
+
tags: string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Memoized SearchResult component for better performance
|
|
26
|
+
const SearchResultItem = React.memo<{
|
|
27
|
+
item: SearchResult;
|
|
28
|
+
typeConfig: any;
|
|
29
|
+
currentSearch: string;
|
|
30
|
+
}>(({ item, typeConfig, currentSearch }) => {
|
|
31
|
+
const config = typeConfig[item.type as keyof typeof typeConfig] || typeConfig.other;
|
|
32
|
+
const getDisplayType = (type: string) => {
|
|
33
|
+
if (type === 'other') return 'Page';
|
|
34
|
+
if (type === 'asyncapi') return 'AsyncAPI';
|
|
35
|
+
if (type === 'openapi') return 'OpenAPI';
|
|
36
|
+
if (type === 'language') return 'Language';
|
|
37
|
+
if (type === 'entities') return 'Entity';
|
|
38
|
+
// For plurals, remove 's' at the end, otherwise just capitalize
|
|
39
|
+
if (type.endsWith('s')) {
|
|
40
|
+
return type.charAt(0).toUpperCase() + type.slice(1, -1);
|
|
41
|
+
}
|
|
42
|
+
return type.charAt(0).toUpperCase() + type.slice(1);
|
|
43
|
+
};
|
|
44
|
+
const displayType = getDisplayType(item.type);
|
|
45
|
+
const IconComponent = config.icon;
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<a href={item.url} className="block group">
|
|
49
|
+
<div
|
|
50
|
+
className={`bg-gradient-to-br ${config.bg} to-white border rounded-lg p-3 hover:border-purple-300 hover:shadow-md transition-all duration-200 group-hover:shadow-lg ${config.border}`}
|
|
51
|
+
>
|
|
52
|
+
<div className="flex items-start justify-between mb-2">
|
|
53
|
+
<h3 className="text-base font-semibold text-gray-900 group-hover:text-purple-700 transition-colors">{item.name}</h3>
|
|
54
|
+
<span
|
|
55
|
+
className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium ${config.badgeBg} ${config.text} ${config.border} border`}
|
|
56
|
+
>
|
|
57
|
+
<IconComponent className="h-3 w-3" />
|
|
58
|
+
{displayType}
|
|
59
|
+
</span>
|
|
60
|
+
</div>
|
|
61
|
+
{item.description && (
|
|
62
|
+
<div className="text-xs text-gray-500 mb-2 line-clamp-2 opacity-80">
|
|
63
|
+
{currentSearch.trim() ? (
|
|
64
|
+
<span dangerouslySetInnerHTML={{ __html: item.description }} />
|
|
65
|
+
) : (
|
|
66
|
+
<span>{item.description.replace(/<[^>]*>/g, '')}</span>
|
|
67
|
+
)}
|
|
68
|
+
</div>
|
|
69
|
+
)}
|
|
70
|
+
{item.tags.length > 0 && (
|
|
71
|
+
<div className="flex flex-wrap gap-1">
|
|
72
|
+
{item.tags.map((tag, index) => (
|
|
73
|
+
<span
|
|
74
|
+
key={index}
|
|
75
|
+
className="inline-flex items-center px-1.5 py-0.5 rounded-md text-xs font-medium bg-gray-100 text-gray-700"
|
|
76
|
+
>
|
|
77
|
+
{tag}
|
|
78
|
+
</span>
|
|
79
|
+
))}
|
|
80
|
+
</div>
|
|
81
|
+
)}
|
|
82
|
+
</div>
|
|
83
|
+
</a>
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Memoized SearchResults component
|
|
88
|
+
const SearchResults = React.memo<{
|
|
89
|
+
results: SearchResult[];
|
|
90
|
+
typeConfig: any;
|
|
91
|
+
currentSearch: string;
|
|
92
|
+
}>(({ results, typeConfig, currentSearch }) => {
|
|
93
|
+
return (
|
|
94
|
+
<>
|
|
95
|
+
{results.map((item) => (
|
|
96
|
+
<SearchResultItem key={item.id} item={item} typeConfig={typeConfig} currentSearch={currentSearch} />
|
|
97
|
+
))}
|
|
98
|
+
</>
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const SearchModal: React.FC = () => {
|
|
103
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
104
|
+
const [pagefind, setPagefind] = useState<any>(null);
|
|
105
|
+
const [pagefindLoadError, setPagefindLoadError] = useState(false);
|
|
106
|
+
const [currentSearch, setCurrentSearch] = useState('');
|
|
107
|
+
const [currentFilter, setCurrentFilter] = useState('all');
|
|
108
|
+
const [allResults, setAllResults] = useState<SearchResult[]>([]);
|
|
109
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
110
|
+
const [exactMatch, setExactMatch] = useState(false);
|
|
111
|
+
|
|
112
|
+
// Listen for modal state changes from Astro component
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
const handleModalToggle = (event: CustomEvent) => {
|
|
115
|
+
setIsOpen(event.detail.isOpen);
|
|
116
|
+
|
|
117
|
+
// Load all results when modal opens - will be handled by another effect
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
window.addEventListener('searchModalToggle', handleModalToggle as EventListener);
|
|
121
|
+
return () => window.removeEventListener('searchModalToggle', handleModalToggle as EventListener);
|
|
122
|
+
}, []);
|
|
123
|
+
|
|
124
|
+
const onClose = () => {
|
|
125
|
+
if ((window as any).searchModalState) {
|
|
126
|
+
(window as any).searchModalState.close();
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// Type colors and icons - memoized to prevent recreating on every render
|
|
131
|
+
const typeConfig = useMemo(
|
|
132
|
+
() => ({
|
|
133
|
+
domains: {
|
|
134
|
+
bg: 'bg-orange-50/10',
|
|
135
|
+
badgeBg: 'bg-orange-500/20',
|
|
136
|
+
text: 'text-orange-800',
|
|
137
|
+
border: 'border-orange-200',
|
|
138
|
+
icon: RectangleGroupIcon,
|
|
139
|
+
},
|
|
140
|
+
services: {
|
|
141
|
+
bg: 'bg-pink-50/10',
|
|
142
|
+
badgeBg: 'bg-pink-500/20',
|
|
143
|
+
text: 'text-pink-800',
|
|
144
|
+
border: 'border-pink-200',
|
|
145
|
+
icon: ServerIcon,
|
|
146
|
+
},
|
|
147
|
+
events: {
|
|
148
|
+
bg: 'bg-orange-50/10',
|
|
149
|
+
badgeBg: 'bg-orange-500/20',
|
|
150
|
+
text: 'text-orange-800',
|
|
151
|
+
border: 'border-orange-200',
|
|
152
|
+
icon: BoltIcon,
|
|
153
|
+
},
|
|
154
|
+
commands: {
|
|
155
|
+
bg: 'bg-blue-50/10',
|
|
156
|
+
badgeBg: 'bg-blue-500/20',
|
|
157
|
+
text: 'text-blue-800',
|
|
158
|
+
border: 'border-blue-200',
|
|
159
|
+
icon: ChatBubbleLeftIcon,
|
|
160
|
+
},
|
|
161
|
+
queries: {
|
|
162
|
+
bg: 'bg-green-50/10',
|
|
163
|
+
badgeBg: 'bg-green-500/20',
|
|
164
|
+
text: 'text-green-800',
|
|
165
|
+
border: 'border-green-200',
|
|
166
|
+
icon: MagnifyingGlassIcon,
|
|
167
|
+
},
|
|
168
|
+
entities: {
|
|
169
|
+
bg: 'bg-purple-50/10',
|
|
170
|
+
badgeBg: 'bg-purple-500/20',
|
|
171
|
+
text: 'text-purple-800',
|
|
172
|
+
border: 'border-purple-200',
|
|
173
|
+
icon: CubeIcon,
|
|
174
|
+
},
|
|
175
|
+
channels: {
|
|
176
|
+
bg: 'bg-indigo-50/10',
|
|
177
|
+
badgeBg: 'bg-indigo-500/20',
|
|
178
|
+
text: 'text-indigo-800',
|
|
179
|
+
border: 'border-indigo-200',
|
|
180
|
+
icon: QueueListIcon,
|
|
181
|
+
},
|
|
182
|
+
teams: {
|
|
183
|
+
bg: 'bg-teal-50/10',
|
|
184
|
+
badgeBg: 'bg-teal-500/20',
|
|
185
|
+
text: 'text-teal-800',
|
|
186
|
+
border: 'border-teal-200',
|
|
187
|
+
icon: UserGroupIcon,
|
|
188
|
+
},
|
|
189
|
+
users: {
|
|
190
|
+
bg: 'bg-cyan-50/10',
|
|
191
|
+
badgeBg: 'bg-cyan-500/20',
|
|
192
|
+
text: 'text-cyan-800',
|
|
193
|
+
border: 'border-cyan-200',
|
|
194
|
+
icon: UserIcon,
|
|
195
|
+
},
|
|
196
|
+
language: {
|
|
197
|
+
bg: 'bg-amber-50/10',
|
|
198
|
+
badgeBg: 'bg-amber-500/20',
|
|
199
|
+
text: 'text-amber-800',
|
|
200
|
+
border: 'border-amber-200',
|
|
201
|
+
icon: BookOpenIcon,
|
|
202
|
+
},
|
|
203
|
+
openapi: {
|
|
204
|
+
bg: 'bg-emerald-50/10',
|
|
205
|
+
badgeBg: 'bg-emerald-500/20',
|
|
206
|
+
text: 'text-emerald-800',
|
|
207
|
+
border: 'border-emerald-200',
|
|
208
|
+
icon: DocumentTextIcon,
|
|
209
|
+
},
|
|
210
|
+
asyncapi: {
|
|
211
|
+
bg: 'bg-violet-50/10',
|
|
212
|
+
badgeBg: 'bg-violet-500/20',
|
|
213
|
+
text: 'text-violet-800',
|
|
214
|
+
border: 'border-violet-200',
|
|
215
|
+
icon: DocumentTextIcon,
|
|
216
|
+
},
|
|
217
|
+
other: {
|
|
218
|
+
bg: 'bg-gray-50/10',
|
|
219
|
+
badgeBg: 'bg-gray-500/20',
|
|
220
|
+
text: 'text-gray-800',
|
|
221
|
+
border: 'border-gray-200',
|
|
222
|
+
icon: DocumentTextIcon,
|
|
223
|
+
},
|
|
224
|
+
}),
|
|
225
|
+
[]
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
// Initialize Pagefind
|
|
229
|
+
useEffect(() => {
|
|
230
|
+
if (typeof window !== 'undefined') {
|
|
231
|
+
const initPagefind = async () => {
|
|
232
|
+
try {
|
|
233
|
+
// Wait for Pagefind to be loaded by the Astro script
|
|
234
|
+
const waitForPagefind = () =>
|
|
235
|
+
new Promise<any>((resolve, reject) => {
|
|
236
|
+
if ((window as any).pagefind) {
|
|
237
|
+
resolve((window as any).pagefind);
|
|
238
|
+
} else {
|
|
239
|
+
const handler = () => {
|
|
240
|
+
window.removeEventListener('pagefindLoaded', handler);
|
|
241
|
+
resolve((window as any).pagefind);
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
// Set a timeout to detect if pagefind fails to load
|
|
245
|
+
const timeout = setTimeout(() => {
|
|
246
|
+
window.removeEventListener('pagefindLoaded', handler);
|
|
247
|
+
reject(new Error('Pagefind failed to load - catalog may not be indexed'));
|
|
248
|
+
}, 2000); // 5 second timeout
|
|
249
|
+
|
|
250
|
+
window.addEventListener('pagefindLoaded', () => {
|
|
251
|
+
clearTimeout(timeout);
|
|
252
|
+
handler();
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const pagefindModule = await waitForPagefind();
|
|
258
|
+
await pagefindModule.init();
|
|
259
|
+
setPagefind(pagefindModule);
|
|
260
|
+
} catch (error) {
|
|
261
|
+
console.error('Failed to initialize Pagefind:', error);
|
|
262
|
+
setPagefindLoadError(true);
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
initPagefind();
|
|
267
|
+
}
|
|
268
|
+
}, []);
|
|
269
|
+
|
|
270
|
+
// Type mapping based on URL patterns - memoized callback
|
|
271
|
+
const getTypeFromUrl = useCallback((url: string): string => {
|
|
272
|
+
// Check for language first since it can be nested under other paths
|
|
273
|
+
if (url.includes('/language/')) return 'language';
|
|
274
|
+
// Check for spec types after language but before other types since they can be nested
|
|
275
|
+
if (url.includes('/spec/')) return 'openapi';
|
|
276
|
+
if (url.includes('/asyncapi')) return 'asyncapi';
|
|
277
|
+
if (url.includes('/domains/')) return 'domains';
|
|
278
|
+
if (url.includes('/services/')) return 'services';
|
|
279
|
+
if (url.includes('/events/')) return 'events';
|
|
280
|
+
if (url.includes('/commands/')) return 'commands';
|
|
281
|
+
if (url.includes('/queries/')) return 'queries';
|
|
282
|
+
if (url.includes('/entities/')) return 'entities';
|
|
283
|
+
if (url.includes('/channels/')) return 'channels';
|
|
284
|
+
if (url.includes('/teams/')) return 'teams';
|
|
285
|
+
if (url.includes('/users/')) return 'users';
|
|
286
|
+
return 'other';
|
|
287
|
+
}, []);
|
|
288
|
+
|
|
289
|
+
// Load all results using a wildcard search
|
|
290
|
+
const loadAllResults = useCallback(async () => {
|
|
291
|
+
if (!pagefind) return;
|
|
292
|
+
|
|
293
|
+
setIsLoading(true);
|
|
294
|
+
try {
|
|
295
|
+
// Use a common word or wildcard to get all indexed content
|
|
296
|
+
const search = await pagefind.search('a');
|
|
297
|
+
if (!search || !search.results) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const processedResults: SearchResult[] = [];
|
|
301
|
+
|
|
302
|
+
for (const result of search.results) {
|
|
303
|
+
const data = await result.data();
|
|
304
|
+
const type = getTypeFromUrl(data.url);
|
|
305
|
+
|
|
306
|
+
// Clean the title by removing any "Type | " prefix if it exists
|
|
307
|
+
let cleanTitle = data.meta?.title || 'Untitled';
|
|
308
|
+
|
|
309
|
+
// Use regex for more efficient prefix removal
|
|
310
|
+
cleanTitle = cleanTitle.replace(
|
|
311
|
+
/^(Domains?|Services?|Events?|Commands?|Queries?|Entities?|Channels?|Teams?|Users?|Language) \| /,
|
|
312
|
+
''
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
processedResults.push({
|
|
316
|
+
id: result.id,
|
|
317
|
+
name: cleanTitle,
|
|
318
|
+
type: type,
|
|
319
|
+
description: data.excerpt || '',
|
|
320
|
+
url: data.url,
|
|
321
|
+
tags: data.meta?.tags ? data.meta.tags.split(',').map((tag: string) => tag.trim()) : [],
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
setAllResults(processedResults);
|
|
326
|
+
} catch (error) {
|
|
327
|
+
console.error('Error loading all results:', error);
|
|
328
|
+
} finally {
|
|
329
|
+
setIsLoading(false);
|
|
330
|
+
}
|
|
331
|
+
}, [pagefind, getTypeFromUrl]);
|
|
332
|
+
|
|
333
|
+
// Load results when modal opens and pagefind is available
|
|
334
|
+
useEffect(() => {
|
|
335
|
+
if (isOpen && pagefind && !pagefindLoadError) {
|
|
336
|
+
loadAllResults();
|
|
337
|
+
}
|
|
338
|
+
}, [isOpen, pagefind, pagefindLoadError, loadAllResults]);
|
|
339
|
+
|
|
340
|
+
// Perform search
|
|
341
|
+
const performSearch = useCallback(
|
|
342
|
+
async (searchTerm: string): Promise<SearchResult[]> => {
|
|
343
|
+
if (!pagefind || !searchTerm.trim()) {
|
|
344
|
+
return [];
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
setIsLoading(true);
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
const search = await pagefind.debouncedSearch(searchTerm);
|
|
351
|
+
if (!search || !search.results) {
|
|
352
|
+
return [];
|
|
353
|
+
}
|
|
354
|
+
const processedResults: SearchResult[] = [];
|
|
355
|
+
|
|
356
|
+
for (const result of search.results) {
|
|
357
|
+
const data = await result.data();
|
|
358
|
+
const type = getTypeFromUrl(data.url);
|
|
359
|
+
|
|
360
|
+
// Clean the title by removing any "Type | " prefix if it exists
|
|
361
|
+
let cleanTitle = data.meta?.title || 'Untitled';
|
|
362
|
+
|
|
363
|
+
// Use regex for more efficient prefix removal
|
|
364
|
+
cleanTitle = cleanTitle.replace(
|
|
365
|
+
/^(Domains?|Services?|Events?|Commands?|Queries?|Entities?|Channels?|Teams?|Users?|Language) \| /,
|
|
366
|
+
''
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
processedResults.push({
|
|
370
|
+
id: result.id,
|
|
371
|
+
name: cleanTitle,
|
|
372
|
+
type: type,
|
|
373
|
+
description: data.excerpt || '',
|
|
374
|
+
url: data.url,
|
|
375
|
+
tags: data.meta?.tags ? data.meta.tags.split(',').map((tag: string) => tag.trim()) : [],
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return processedResults;
|
|
380
|
+
} catch (error) {
|
|
381
|
+
console.error('Search error:', error);
|
|
382
|
+
return [];
|
|
383
|
+
} finally {
|
|
384
|
+
setIsLoading(false);
|
|
385
|
+
}
|
|
386
|
+
},
|
|
387
|
+
[pagefind]
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
// Filter results - memoized callback
|
|
391
|
+
const filterResults = useCallback(
|
|
392
|
+
(results: SearchResult[], filterType: string): SearchResult[] => {
|
|
393
|
+
let filteredResults = results;
|
|
394
|
+
|
|
395
|
+
// Apply type filter
|
|
396
|
+
if (filterType !== 'all') {
|
|
397
|
+
filteredResults = filteredResults.filter((item) => item.type === filterType);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Apply exact match filter if enabled
|
|
401
|
+
if (exactMatch && currentSearch.trim()) {
|
|
402
|
+
filteredResults = filteredResults.filter((item) => item.name.toLowerCase().includes(currentSearch.toLowerCase()));
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return filteredResults;
|
|
406
|
+
},
|
|
407
|
+
[exactMatch, currentSearch]
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
// Update results
|
|
411
|
+
const updateResults = useCallback(async () => {
|
|
412
|
+
if (currentSearch.trim()) {
|
|
413
|
+
const results = await performSearch(currentSearch);
|
|
414
|
+
setAllResults(results);
|
|
415
|
+
} else {
|
|
416
|
+
setAllResults([]);
|
|
417
|
+
}
|
|
418
|
+
}, [currentSearch, performSearch]);
|
|
419
|
+
|
|
420
|
+
// Search on input change
|
|
421
|
+
useEffect(() => {
|
|
422
|
+
updateResults();
|
|
423
|
+
}, [updateResults]);
|
|
424
|
+
|
|
425
|
+
// Handle input change
|
|
426
|
+
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
427
|
+
setCurrentSearch(e.target.value);
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
// Handle filter change
|
|
431
|
+
const handleFilterChange = (filter: string) => {
|
|
432
|
+
setCurrentFilter(filter);
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
// Get filtered results - memoized to prevent recalculation
|
|
436
|
+
const filteredResults = useMemo(() => {
|
|
437
|
+
return filterResults(allResults, currentFilter);
|
|
438
|
+
}, [allResults, currentFilter, exactMatch, currentSearch]);
|
|
439
|
+
|
|
440
|
+
// Get filter counts - memoized to prevent recalculation on every render
|
|
441
|
+
const getFilterCounts = useMemo(() => {
|
|
442
|
+
return {
|
|
443
|
+
all: allResults.length,
|
|
444
|
+
domains: allResults.filter((r) => r.type === 'domains').length,
|
|
445
|
+
services: allResults.filter((r) => r.type === 'services').length,
|
|
446
|
+
events: allResults.filter((r) => r.type === 'events').length,
|
|
447
|
+
commands: allResults.filter((r) => r.type === 'commands').length,
|
|
448
|
+
queries: allResults.filter((r) => r.type === 'queries').length,
|
|
449
|
+
entities: allResults.filter((r) => r.type === 'entities').length,
|
|
450
|
+
channels: allResults.filter((r) => r.type === 'channels').length,
|
|
451
|
+
teams: allResults.filter((r) => r.type === 'teams').length,
|
|
452
|
+
users: allResults.filter((r) => r.type === 'users').length,
|
|
453
|
+
language: allResults.filter((r) => r.type === 'language').length,
|
|
454
|
+
openapi: allResults.filter((r) => r.type === 'openapi').length,
|
|
455
|
+
asyncapi: allResults.filter((r) => r.type === 'asyncapi').length,
|
|
456
|
+
};
|
|
457
|
+
}, [allResults]);
|
|
458
|
+
|
|
459
|
+
const counts = getFilterCounts;
|
|
460
|
+
|
|
461
|
+
// Handle escape key
|
|
462
|
+
useEffect(() => {
|
|
463
|
+
const handleEscape = (e: KeyboardEvent) => {
|
|
464
|
+
if (e.key === 'Escape') {
|
|
465
|
+
onClose();
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
if (isOpen) {
|
|
470
|
+
document.addEventListener('keydown', handleEscape);
|
|
471
|
+
return () => document.removeEventListener('keydown', handleEscape);
|
|
472
|
+
}
|
|
473
|
+
}, [isOpen, onClose]);
|
|
474
|
+
|
|
475
|
+
if (!isOpen) return null;
|
|
476
|
+
|
|
477
|
+
return (
|
|
478
|
+
<div>
|
|
479
|
+
<style>{`
|
|
480
|
+
.search-results mark {
|
|
481
|
+
background-color: #fef3c7;
|
|
482
|
+
color: #92400e;
|
|
483
|
+
padding: 0.125rem 0.25rem;
|
|
484
|
+
border-radius: 0.25rem;
|
|
485
|
+
font-weight: 500;
|
|
486
|
+
}
|
|
487
|
+
`}</style>
|
|
488
|
+
<div className="fixed inset-0 z-[9999] overflow-y-auto" role="dialog" aria-modal="true">
|
|
489
|
+
<div
|
|
490
|
+
className="fixed inset-0 bg-gray-500 bg-opacity-25 transition-opacity backdrop-blur-sm bg-black/10 z-[9998]"
|
|
491
|
+
onClick={onClose}
|
|
492
|
+
></div>
|
|
493
|
+
<div className="fixed inset-0 z-[10000] w-screen overflow-y-auto p-4 sm:p-6 md:p-10" onClick={onClose}>
|
|
494
|
+
<div
|
|
495
|
+
className="mx-auto max-w-6xl divide-y divide-gray-100 overflow-hidden rounded-xl bg-white shadow-2xl ring-1 ring-black ring-opacity-5 transition-all"
|
|
496
|
+
onClick={(e) => e.stopPropagation()}
|
|
497
|
+
>
|
|
498
|
+
{pagefindLoadError ? (
|
|
499
|
+
// Show indexing required message when Pagefind fails to load - full modal content
|
|
500
|
+
<div className="flex items-center justify-center py-10 px-8">
|
|
501
|
+
<div className="text-left max-w-lg">
|
|
502
|
+
<div className="mb-8">
|
|
503
|
+
<h2 className="text-2xl font-bold text-gray-900 mb-3">Search Index Not Found</h2>
|
|
504
|
+
<p className="text-gray-600 mb-8 leading-relaxed text-sm">
|
|
505
|
+
Your EventCatalog needs to be built to generate the search index. This enables fast searching across all
|
|
506
|
+
your domains, services, events, and documentation.
|
|
507
|
+
</p>
|
|
508
|
+
</div>
|
|
509
|
+
|
|
510
|
+
<div className="bg-gradient-to-r from-gray-50 to-gray-100 rounded-xl p-6 mb-6 border border-gray-200">
|
|
511
|
+
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center justify-center">
|
|
512
|
+
<svg className="h-5 w-5 text-green-600 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
513
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
514
|
+
</svg>
|
|
515
|
+
Build Your Catalog
|
|
516
|
+
</h3>
|
|
517
|
+
<div className="bg-gray-900 rounded-lg p-4 mb-4">
|
|
518
|
+
<code className="text-green-400 font-mono text-sm">npm run build</code>
|
|
519
|
+
</div>
|
|
520
|
+
<p className="text-sm text-gray-600">This will generate your catalog and create the search index</p>
|
|
521
|
+
</div>
|
|
522
|
+
|
|
523
|
+
<div className="flex items-start text-left bg-blue-50 rounded-lg p-4 border border-blue-200">
|
|
524
|
+
<svg
|
|
525
|
+
className="h-5 w-5 text-blue-600 mr-3 mt-0.5 flex-shrink-0"
|
|
526
|
+
fill="none"
|
|
527
|
+
stroke="currentColor"
|
|
528
|
+
viewBox="0 0 24 24"
|
|
529
|
+
>
|
|
530
|
+
<path
|
|
531
|
+
strokeLinecap="round"
|
|
532
|
+
strokeLinejoin="round"
|
|
533
|
+
strokeWidth="2"
|
|
534
|
+
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
535
|
+
/>
|
|
536
|
+
</svg>
|
|
537
|
+
<div>
|
|
538
|
+
<h4 className="font-medium text-blue-900 mb-1">Need to update search results?</h4>
|
|
539
|
+
<p className="text-sm text-blue-700">
|
|
540
|
+
Run <code className="bg-blue-100 px-1 py-0.5 rounded text-xs font-mono">npm run build</code> again after
|
|
541
|
+
making changes to your catalog content.
|
|
542
|
+
</p>
|
|
543
|
+
</div>
|
|
544
|
+
</div>
|
|
545
|
+
</div>
|
|
546
|
+
</div>
|
|
547
|
+
) : (
|
|
548
|
+
<>
|
|
549
|
+
{/* Search Input */}
|
|
550
|
+
<div className="relative px-6 pt-4 pb-2">
|
|
551
|
+
<MagnifyingGlassIcon className="pointer-events-none absolute left-10 top-[25px] h-5 w-5 text-gray-400" />
|
|
552
|
+
<input
|
|
553
|
+
type="text"
|
|
554
|
+
placeholder="Search for domains, services, events..."
|
|
555
|
+
className="w-full border border-gray-200 rounded-lg bg-white pl-12 pr-4 py-2 text-gray-900 placeholder:text-gray-400 focus:ring-2 focus:ring-purple-500 focus:border-purple-500 text-sm"
|
|
556
|
+
value={currentSearch}
|
|
557
|
+
onChange={handleSearchChange}
|
|
558
|
+
autoFocus
|
|
559
|
+
/>
|
|
560
|
+
</div>
|
|
561
|
+
|
|
562
|
+
{/* Main Content Area */}
|
|
563
|
+
<div className="flex h-[500px]">
|
|
564
|
+
{/* Left Filters */}
|
|
565
|
+
<div className="w-56 p-3 border-r border-gray-200 bg-gray-50 overflow-y-auto">
|
|
566
|
+
{/* All Resources */}
|
|
567
|
+
<div className="mb-4">
|
|
568
|
+
<div className="space-y-1">
|
|
569
|
+
<button
|
|
570
|
+
className={`w-full px-2 py-1 text-xs rounded-md transition-colors ${
|
|
571
|
+
currentFilter === 'all'
|
|
572
|
+
? 'bg-purple-200 text-purple-900 font-semibold'
|
|
573
|
+
: 'hover:bg-purple-100 text-gray-700 hover:text-purple-800'
|
|
574
|
+
}`}
|
|
575
|
+
onClick={() => handleFilterChange('all')}
|
|
576
|
+
>
|
|
577
|
+
<div className="flex items-center justify-between">
|
|
578
|
+
<div className="flex items-center gap-1.5">
|
|
579
|
+
<span>All Resources</span>
|
|
580
|
+
</div>
|
|
581
|
+
<span className="text-xs text-gray-400">{counts.all}</span>
|
|
582
|
+
</div>
|
|
583
|
+
</button>
|
|
584
|
+
</div>
|
|
585
|
+
</div>
|
|
586
|
+
|
|
587
|
+
{/* Resources Section */}
|
|
588
|
+
<div className="mb-4">
|
|
589
|
+
<h3 className="text-xs font-medium text-gray-600 mb-2">Resources</h3>
|
|
590
|
+
<div className="space-y-1">
|
|
591
|
+
{Object.entries({
|
|
592
|
+
domains: 'Domains',
|
|
593
|
+
services: 'Services',
|
|
594
|
+
entities: 'Entities',
|
|
595
|
+
language: 'Ubiquitous Language',
|
|
596
|
+
}).map(([key, label]) => {
|
|
597
|
+
const config = typeConfig[key as keyof typeof typeConfig];
|
|
598
|
+
const IconComponent = config?.icon;
|
|
599
|
+
|
|
600
|
+
return (
|
|
601
|
+
<button
|
|
602
|
+
key={key}
|
|
603
|
+
className={`w-full px-2 py-1 text-xs rounded-md transition-colors ${
|
|
604
|
+
currentFilter === key
|
|
605
|
+
? 'bg-purple-200 text-purple-900 font-semibold'
|
|
606
|
+
: 'hover:bg-purple-100 text-gray-700 hover:text-purple-800'
|
|
607
|
+
}`}
|
|
608
|
+
onClick={() => handleFilterChange(key)}
|
|
609
|
+
>
|
|
610
|
+
<div className="flex items-center justify-between">
|
|
611
|
+
<div className="flex items-center gap-1.5">
|
|
612
|
+
{IconComponent && <IconComponent className="h-3 w-3" />}
|
|
613
|
+
<span>{label}</span>
|
|
614
|
+
</div>
|
|
615
|
+
<span className="text-xs text-gray-400">{counts[key as keyof typeof counts]}</span>
|
|
616
|
+
</div>
|
|
617
|
+
</button>
|
|
618
|
+
);
|
|
619
|
+
})}
|
|
620
|
+
</div>
|
|
621
|
+
</div>
|
|
622
|
+
|
|
623
|
+
{/* Messages Section */}
|
|
624
|
+
<div className="mb-4">
|
|
625
|
+
<h3 className="text-xs font-medium text-gray-600 mb-2">Messages</h3>
|
|
626
|
+
<div className="space-y-1">
|
|
627
|
+
{Object.entries({
|
|
628
|
+
events: 'Events',
|
|
629
|
+
commands: 'Commands',
|
|
630
|
+
queries: 'Queries',
|
|
631
|
+
channels: 'Channels',
|
|
632
|
+
}).map(([key, label]) => {
|
|
633
|
+
const config = typeConfig[key as keyof typeof typeConfig];
|
|
634
|
+
const IconComponent = config?.icon;
|
|
635
|
+
|
|
636
|
+
return (
|
|
637
|
+
<button
|
|
638
|
+
key={key}
|
|
639
|
+
className={`w-full px-2 py-1 text-xs rounded-md transition-colors ${
|
|
640
|
+
currentFilter === key
|
|
641
|
+
? 'bg-purple-200 text-purple-900 font-semibold'
|
|
642
|
+
: 'hover:bg-purple-100 text-gray-700 hover:text-purple-800'
|
|
643
|
+
}`}
|
|
644
|
+
onClick={() => handleFilterChange(key)}
|
|
645
|
+
>
|
|
646
|
+
<div className="flex items-center justify-between">
|
|
647
|
+
<div className="flex items-center gap-1.5">
|
|
648
|
+
{IconComponent && <IconComponent className="h-3 w-3" />}
|
|
649
|
+
<span>{label}</span>
|
|
650
|
+
</div>
|
|
651
|
+
<span className="text-xs text-gray-400">{counts[key as keyof typeof counts]}</span>
|
|
652
|
+
</div>
|
|
653
|
+
</button>
|
|
654
|
+
);
|
|
655
|
+
})}
|
|
656
|
+
</div>
|
|
657
|
+
</div>
|
|
658
|
+
|
|
659
|
+
{/* Organization Section */}
|
|
660
|
+
<div className="mb-4">
|
|
661
|
+
<h3 className="text-xs font-medium text-gray-600 mb-2">Organization</h3>
|
|
662
|
+
<div className="space-y-1">
|
|
663
|
+
{Object.entries({
|
|
664
|
+
teams: 'Teams',
|
|
665
|
+
users: 'Users',
|
|
666
|
+
}).map(([key, label]) => {
|
|
667
|
+
const config = typeConfig[key as keyof typeof typeConfig];
|
|
668
|
+
const IconComponent = config?.icon;
|
|
669
|
+
|
|
670
|
+
return (
|
|
671
|
+
<button
|
|
672
|
+
key={key}
|
|
673
|
+
className={`w-full px-2 py-1 text-xs rounded-md transition-colors ${
|
|
674
|
+
currentFilter === key
|
|
675
|
+
? 'bg-purple-200 text-purple-900 font-semibold'
|
|
676
|
+
: 'hover:bg-purple-100 text-gray-700 hover:text-purple-800'
|
|
677
|
+
}`}
|
|
678
|
+
onClick={() => handleFilterChange(key)}
|
|
679
|
+
>
|
|
680
|
+
<div className="flex items-center justify-between">
|
|
681
|
+
<div className="flex items-center gap-1.5">
|
|
682
|
+
{IconComponent && <IconComponent className="h-3 w-3" />}
|
|
683
|
+
<span>{label}</span>
|
|
684
|
+
</div>
|
|
685
|
+
<span className="text-xs text-gray-400">{counts[key as keyof typeof counts]}</span>
|
|
686
|
+
</div>
|
|
687
|
+
</button>
|
|
688
|
+
);
|
|
689
|
+
})}
|
|
690
|
+
</div>
|
|
691
|
+
</div>
|
|
692
|
+
|
|
693
|
+
{/* Specifications Section */}
|
|
694
|
+
<div className="mb-4">
|
|
695
|
+
<h3 className="text-xs font-medium text-gray-600 mb-2">Specifications</h3>
|
|
696
|
+
<div className="space-y-1">
|
|
697
|
+
{Object.entries({
|
|
698
|
+
openapi: 'OpenAPI Specification',
|
|
699
|
+
asyncapi: 'AsyncAPI Specification',
|
|
700
|
+
}).map(([key, label]) => {
|
|
701
|
+
const config = typeConfig[key as keyof typeof typeConfig];
|
|
702
|
+
const IconComponent = config.icon;
|
|
703
|
+
|
|
704
|
+
return (
|
|
705
|
+
<button
|
|
706
|
+
key={key}
|
|
707
|
+
className={`w-full px-2 py-1 text-xs rounded-md transition-colors ${
|
|
708
|
+
currentFilter === key
|
|
709
|
+
? 'bg-purple-200 text-purple-900 font-semibold'
|
|
710
|
+
: 'hover:bg-purple-100 text-gray-700 hover:text-purple-800'
|
|
711
|
+
}`}
|
|
712
|
+
onClick={() => handleFilterChange(key)}
|
|
713
|
+
>
|
|
714
|
+
<div className="flex items-center justify-between">
|
|
715
|
+
<div className="flex items-center gap-1.5">
|
|
716
|
+
<IconComponent className="h-3 w-3" />
|
|
717
|
+
<span>{label}</span>
|
|
718
|
+
</div>
|
|
719
|
+
<span className="text-xs text-gray-400">{counts[key as keyof typeof counts]}</span>
|
|
720
|
+
</div>
|
|
721
|
+
</button>
|
|
722
|
+
);
|
|
723
|
+
})}
|
|
724
|
+
</div>
|
|
725
|
+
</div>
|
|
726
|
+
</div>
|
|
727
|
+
|
|
728
|
+
{/* Right Results */}
|
|
729
|
+
<div className="flex-1 p-4 overflow-y-auto">
|
|
730
|
+
{/* Show stats and exact match toggle */}
|
|
731
|
+
<div className="mb-4 flex items-center justify-between">
|
|
732
|
+
<div className="text-sm text-gray-500">
|
|
733
|
+
{currentSearch.trim() ? (
|
|
734
|
+
<>
|
|
735
|
+
<span>{filteredResults.length} results</span> for "{currentSearch}"
|
|
736
|
+
{isLoading && <span className="ml-2">Loading...</span>}
|
|
737
|
+
</>
|
|
738
|
+
) : (
|
|
739
|
+
<>
|
|
740
|
+
<span>{filteredResults.length} resources</span> in EventCatalog
|
|
741
|
+
{isLoading && <span className="ml-2">Loading...</span>}
|
|
742
|
+
</>
|
|
743
|
+
)}
|
|
744
|
+
</div>
|
|
745
|
+
|
|
746
|
+
{/* Exact Match Checkbox - moved here */}
|
|
747
|
+
{currentSearch.trim() && (
|
|
748
|
+
<div className="flex items-center">
|
|
749
|
+
<input
|
|
750
|
+
id="exact-match-results"
|
|
751
|
+
type="checkbox"
|
|
752
|
+
checked={exactMatch}
|
|
753
|
+
onChange={(e) => setExactMatch(e.target.checked)}
|
|
754
|
+
className="h-4 w-4 text-purple-600 focus:ring-purple-500 border-gray-300 rounded"
|
|
755
|
+
/>
|
|
756
|
+
<label htmlFor="exact-match-results" className="ml-2 text-sm text-gray-600">
|
|
757
|
+
Exact match in title
|
|
758
|
+
</label>
|
|
759
|
+
</div>
|
|
760
|
+
)}
|
|
761
|
+
</div>
|
|
762
|
+
|
|
763
|
+
<div className="search-results grid grid-cols-1 lg:grid-cols-2 gap-3">
|
|
764
|
+
{!currentSearch.trim() && filteredResults.length === 0 ? (
|
|
765
|
+
// Show when no search term is entered and no results loaded yet
|
|
766
|
+
<div className="col-span-full text-center py-20">
|
|
767
|
+
<svg className="mx-auto h-16 w-16 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
768
|
+
<path
|
|
769
|
+
strokeLinecap="round"
|
|
770
|
+
strokeLinejoin="round"
|
|
771
|
+
strokeWidth="1.5"
|
|
772
|
+
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
|
773
|
+
/>
|
|
774
|
+
</svg>
|
|
775
|
+
<h3 className="mt-4 text-lg font-medium text-gray-900">
|
|
776
|
+
{isLoading ? 'Loading resources...' : 'Discover your EventCatalog'}
|
|
777
|
+
</h3>
|
|
778
|
+
<p className="mt-2 text-sm text-gray-300">
|
|
779
|
+
{isLoading
|
|
780
|
+
? 'Fetching all available resources from EventCatalog.'
|
|
781
|
+
: 'Start typing to search for domains, services, events, and more.'}
|
|
782
|
+
</p>
|
|
783
|
+
</div>
|
|
784
|
+
) : currentSearch.trim() && filteredResults.length === 0 ? (
|
|
785
|
+
// Show when search term exists but no results
|
|
786
|
+
<div className="col-span-full text-center py-16">
|
|
787
|
+
<svg className="mx-auto h-12 w-12 text-gray-300" stroke="currentColor" fill="none" viewBox="0 0 48 48">
|
|
788
|
+
<path
|
|
789
|
+
d="M21 21l6-6m-6 6l6 6m-6-6h6m-6 0v6"
|
|
790
|
+
strokeWidth="2"
|
|
791
|
+
strokeLinecap="round"
|
|
792
|
+
strokeLinejoin="round"
|
|
793
|
+
/>
|
|
794
|
+
</svg>
|
|
795
|
+
<h3 className="mt-2 text-sm font-medium text-gray-900">No results found</h3>
|
|
796
|
+
<p className="mt-1 text-sm text-gray-500">
|
|
797
|
+
No results found for "<span className="font-medium">{currentSearch}</span>". Try different keywords or
|
|
798
|
+
check your spelling.
|
|
799
|
+
</p>
|
|
800
|
+
</div>
|
|
801
|
+
) : (
|
|
802
|
+
<SearchResults results={filteredResults} typeConfig={typeConfig} currentSearch={currentSearch} />
|
|
803
|
+
)}
|
|
804
|
+
</div>
|
|
805
|
+
</div>
|
|
806
|
+
</div>
|
|
807
|
+
</>
|
|
808
|
+
)}
|
|
809
|
+
</div>
|
|
810
|
+
</div>
|
|
811
|
+
</div>
|
|
812
|
+
</div>
|
|
813
|
+
);
|
|
814
|
+
};
|
|
815
|
+
|
|
816
|
+
export default SearchModal;
|