@eventcatalog/core 5.5.2 → 5.6.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 +5 -5
- package/dist/{chunk-CNN747RO.js → chunk-7TTNTHHN.js} +10 -10
- package/dist/{chunk-7ZEKF2LA.js → chunk-DEO4QEGO.js} +1 -1
- package/dist/{chunk-WG6ZK6L3.js → chunk-ETS4G6RR.js} +1 -1
- package/dist/{chunk-5HSYALWK.js → chunk-T3U27UXC.js} +1 -1
- package/dist/{chunk-MC4F3VVM.js → chunk-XBRKEQ66.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/docs/development/customization/05-resource-sidebar.md +1 -1
- package/dist/docs/development/guides/domains/09-reference.md +5 -1
- package/dist/docs/development/guides/resources/schemas/03-add-specifications-to-services/04-add-generic-specifications.md +139 -0
- package/dist/docs/development/guides/resources/schemas/03-add-specifications-to-services/_category_.json +1 -1
- package/dist/docs/development/guides/resources/services/08-reference.md +6 -1
- package/dist/eventcatalog.cjs +1 -1
- package/dist/eventcatalog.js +16 -16
- package/dist/federation/federate.js +3 -3
- package/dist/generate.cjs +1 -1
- package/dist/generate.js +3 -3
- package/dist/utils/cli-logger.cjs +1 -1
- package/dist/utils/cli-logger.js +2 -2
- package/eventcatalog/public/icons/specification.svg +1 -0
- package/eventcatalog/src/components/ChatPanel/ChatPanel.tsx +38 -20
- package/eventcatalog/src/components/Grids/specification-utils.ts +7 -1
- package/eventcatalog/src/components/MDX/ResourceRef/ResourceRef.astro +1 -1
- package/eventcatalog/src/components/SchemaExplorer/SchemaCodeBlock.tsx +58 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaCodeModal.tsx +2 -1
- package/eventcatalog/src/components/SchemaExplorer/SchemaContentViewer.tsx +38 -76
- package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsPanel.tsx +15 -11
- package/eventcatalog/src/components/SchemaExplorer/SchemaExplorer.tsx +171 -142
- package/eventcatalog/src/components/SchemaExplorer/SchemaListItem.tsx +5 -8
- package/eventcatalog/src/components/SchemaExplorer/types.ts +2 -0
- package/eventcatalog/src/components/SchemaExplorer/utils.ts +70 -1
- package/eventcatalog/src/content.config.ts +3 -1
- package/eventcatalog/src/features/ai/chat-api.ts +6 -1
- package/eventcatalog/src/features/mcp/apps/generated/canvas.html +1 -1
- package/eventcatalog/src/features/mcp/apps/generated/viewer.html +1 -1
- package/eventcatalog/src/features/tools/catalog-tools.ts +8 -3
- package/eventcatalog/src/pages/.well-known/api-catalog.ts +1 -11
- package/eventcatalog/src/pages/api-catalog/specifications/[collection]/[id]/[version]/[specification].ts +1 -11
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/generic/[filename].astro +137 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/generic/_SpecificationSource.tsx +44 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/generic/_[filename].data.ts +75 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +14 -1
- package/eventcatalog/src/stores/sidebar-store/builders/domain.ts +10 -1
- package/eventcatalog/src/stores/sidebar-store/builders/service.ts +8 -1
- package/eventcatalog/src/stores/sidebar-store/custom-sidebar.ts +3 -1
- package/eventcatalog/src/utils/collections/util.ts +38 -1
- package/eventcatalog/src/utils/icon.ts +8 -0
- package/eventcatalog/src/utils/node-graphs/utils/utils.ts +5 -2
- package/eventcatalog/src/utils/schema-explorer.ts +7 -2
- package/package.json +6 -6
- package/dist/{chunk-QJ7KKN2D.js → chunk-NCEZDQSW.js} +7 -7
|
@@ -1,30 +1,121 @@
|
|
|
1
|
-
import { useState, useMemo, useEffect, useRef } from 'react';
|
|
2
|
-
import {
|
|
1
|
+
import { useState, useMemo, useEffect, useRef, type ComponentType, type ReactNode } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
AdjustmentsHorizontalIcon,
|
|
4
|
+
BoltIcon,
|
|
5
|
+
ChatBubbleLeftIcon,
|
|
6
|
+
CheckIcon,
|
|
7
|
+
DocumentCheckIcon,
|
|
8
|
+
DocumentTextIcon,
|
|
9
|
+
MagnifyingGlassIcon,
|
|
10
|
+
} from '@heroicons/react/24/outline';
|
|
3
11
|
import type { CollectionMessageTypes } from '@types';
|
|
4
12
|
import semver from 'semver';
|
|
5
13
|
import SchemaListItem from './SchemaListItem';
|
|
6
14
|
import SchemaDetailsPanel from './SchemaDetailsPanel';
|
|
7
15
|
import Pagination from './Pagination';
|
|
8
16
|
import type { SchemaItem } from './types';
|
|
17
|
+
import { getFormatBadge, getFormatOptions, getSchemaFormat, getSchemaIconUrl, normalizeSchemaFormat } from './utils';
|
|
9
18
|
|
|
10
|
-
//
|
|
11
|
-
const SPEC_TYPES = ['openapi', 'asyncapi', 'graphql'];
|
|
12
|
-
const HIDDEN_FORMAT_FILTERS = new Set(['graphql', 'gql', 'yaml', 'yml']);
|
|
19
|
+
// Labels for the formats getSchemaFormat returns
|
|
13
20
|
const SCHEMA_TYPE_LABELS: Record<string, string> = {
|
|
14
21
|
json: 'JSON Schema',
|
|
22
|
+
'json-document': 'JSON',
|
|
15
23
|
asyncapi: 'AsyncAPI',
|
|
16
24
|
openapi: 'OpenAPI',
|
|
17
25
|
graphql: 'GraphQL',
|
|
18
26
|
avro: 'Avro',
|
|
19
|
-
avsc: 'Avro',
|
|
20
27
|
proto: 'Protobuf',
|
|
21
|
-
protobuf: 'Protobuf',
|
|
22
28
|
yaml: 'YAML',
|
|
23
|
-
yml: 'YAML',
|
|
24
29
|
xml: 'XML',
|
|
25
30
|
xsd: 'XML Schema',
|
|
31
|
+
text: 'Text',
|
|
26
32
|
};
|
|
27
33
|
|
|
34
|
+
const TYPE_ICONS: Record<string, ComponentType<{ className?: string }>> = {
|
|
35
|
+
events: BoltIcon,
|
|
36
|
+
commands: ChatBubbleLeftIcon,
|
|
37
|
+
queries: MagnifyingGlassIcon,
|
|
38
|
+
specifications: DocumentTextIcon,
|
|
39
|
+
'data-contracts': DocumentCheckIcon,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function TypeIcon({ type }: { type: string }) {
|
|
43
|
+
const Icon = TYPE_ICONS[type] ?? DocumentTextIcon;
|
|
44
|
+
return <Icon className="h-3.5 w-3.5" />;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The same icon or badge a schema of this format shows in the list */
|
|
48
|
+
function FormatIcon({ format }: { format: string }) {
|
|
49
|
+
// A generic JSON file gets the plain JSON badge, not the JSON Schema icon
|
|
50
|
+
const iconUrl = format === 'json-document' ? undefined : getSchemaIconUrl({ schemaExtension: format });
|
|
51
|
+
if (iconUrl) return <img src={iconUrl} alt="" className="schema-icon h-3.5 w-3.5" />;
|
|
52
|
+
const { label, color } = getFormatBadge(format === 'json-document' ? 'json' : format);
|
|
53
|
+
return <span className={`font-mono text-[7px] font-bold uppercase ${color}`}>{label}</span>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function FilterSection({ title, role, children }: { title: string; role?: 'radiogroup'; children: ReactNode }) {
|
|
57
|
+
return (
|
|
58
|
+
<fieldset>
|
|
59
|
+
<legend className="mb-1.5 text-[11px] font-semibold text-[rgb(var(--ec-page-text))]">{title}</legend>
|
|
60
|
+
<div role={role} aria-label={title} className="grid grid-cols-2 gap-x-3 gap-y-0.5">
|
|
61
|
+
{children}
|
|
62
|
+
</div>
|
|
63
|
+
</fieldset>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** A checkbox (pick any) or radio (pick one) row in the filter panel */
|
|
68
|
+
function FilterOption({
|
|
69
|
+
label,
|
|
70
|
+
count,
|
|
71
|
+
icon,
|
|
72
|
+
isActive,
|
|
73
|
+
control,
|
|
74
|
+
onClick,
|
|
75
|
+
}: {
|
|
76
|
+
label: string;
|
|
77
|
+
count: number;
|
|
78
|
+
icon?: ReactNode;
|
|
79
|
+
isActive: boolean;
|
|
80
|
+
control: 'checkbox' | 'radio';
|
|
81
|
+
onClick: () => void;
|
|
82
|
+
}) {
|
|
83
|
+
return (
|
|
84
|
+
<button
|
|
85
|
+
type="button"
|
|
86
|
+
role={control}
|
|
87
|
+
aria-checked={isActive}
|
|
88
|
+
onClick={onClick}
|
|
89
|
+
className={`flex min-w-0 items-center gap-2 rounded-md px-1.5 py-1 text-left text-[11px] transition-colors hover:bg-[rgb(var(--ec-content-hover))] ${
|
|
90
|
+
isActive
|
|
91
|
+
? 'text-[rgb(var(--ec-page-text))]'
|
|
92
|
+
: 'text-[rgb(var(--ec-page-text-muted))] hover:text-[rgb(var(--ec-page-text))]'
|
|
93
|
+
}`}
|
|
94
|
+
>
|
|
95
|
+
<span
|
|
96
|
+
aria-hidden
|
|
97
|
+
className={`flex h-3.5 w-3.5 flex-shrink-0 items-center justify-center border transition-colors ${
|
|
98
|
+
control === 'radio' ? 'rounded-full' : 'rounded-[3px]'
|
|
99
|
+
} ${
|
|
100
|
+
isActive
|
|
101
|
+
? 'border-[rgb(var(--ec-accent))] bg-[rgb(var(--ec-accent))] text-white'
|
|
102
|
+
: 'border-[rgb(var(--ec-page-text-muted)/0.5)] bg-[rgb(var(--ec-dropdown-bg))]'
|
|
103
|
+
}`}
|
|
104
|
+
>
|
|
105
|
+
{isActive &&
|
|
106
|
+
(control === 'radio' ? (
|
|
107
|
+
<span className="h-1.5 w-1.5 rounded-full bg-white" />
|
|
108
|
+
) : (
|
|
109
|
+
<CheckIcon className="h-2.5 w-2.5 stroke-[3]" />
|
|
110
|
+
))}
|
|
111
|
+
</span>
|
|
112
|
+
{icon && <span className="flex h-3.5 w-3.5 flex-shrink-0 items-center justify-center">{icon}</span>}
|
|
113
|
+
<span className={`min-w-0 flex-1 truncate ${isActive ? 'font-medium' : ''}`}>{label}</span>
|
|
114
|
+
<span className="flex-shrink-0 text-[10px] tabular-nums text-[rgb(var(--ec-page-text-muted)/0.8)]">{count}</span>
|
|
115
|
+
</button>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
28
119
|
/** Resolve the spec filename for a schema item, with consistent fallback. */
|
|
29
120
|
function getSpecFile(item: SchemaItem): string {
|
|
30
121
|
return item.specFilenameWithoutExtension || item.specName || '';
|
|
@@ -73,7 +164,7 @@ export default function SchemaExplorer({ schemas }: SchemaExplorerProps) {
|
|
|
73
164
|
const [selectedSchemaType, setSelectedSchemaType] = useState<'all' | string>(() => {
|
|
74
165
|
if (typeof window !== 'undefined') {
|
|
75
166
|
const stored = localStorage.getItem('schemaRegistrySelectedSchemaType');
|
|
76
|
-
return stored !== null ? stored : 'all';
|
|
167
|
+
return stored !== null && stored !== 'all' ? normalizeSchemaFormat(stored) : 'all';
|
|
77
168
|
}
|
|
78
169
|
return 'all';
|
|
79
170
|
});
|
|
@@ -164,36 +255,27 @@ export default function SchemaExplorer({ schemas }: SchemaExplorerProps) {
|
|
|
164
255
|
return Array.from(messagesByIdAndVersions.values()).map((versions) => versions[0]);
|
|
165
256
|
}, [messagesByIdAndVersions]);
|
|
166
257
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
258
|
+
// Schemas of the selected types; the format filter offers (and counts) only the formats among them
|
|
259
|
+
const typeFilteredMessages = useMemo(() => {
|
|
260
|
+
if (selectedTypes.size === 0) return latestMessages;
|
|
261
|
+
return latestMessages.filter(
|
|
262
|
+
(msg) =>
|
|
263
|
+
selectedTypes.has(msg.collection as CollectionMessageTypes) ||
|
|
264
|
+
(selectedTypes.has('specifications') && Boolean(msg.specType)) ||
|
|
265
|
+
(selectedTypes.has('data-contracts') && msg.collection === 'data-products')
|
|
266
|
+
);
|
|
267
|
+
}, [latestMessages, selectedTypes]);
|
|
268
|
+
|
|
269
|
+
const formatOptions = useMemo(
|
|
270
|
+
() => getFormatOptions(typeFilteredMessages, selectedSchemaType === 'all' ? undefined : selectedSchemaType),
|
|
271
|
+
[typeFilteredMessages, selectedSchemaType]
|
|
272
|
+
);
|
|
176
273
|
|
|
177
274
|
const filteredMessages = useMemo(() => {
|
|
178
|
-
let result = [...
|
|
179
|
-
|
|
180
|
-
if (selectedTypes.size > 0) {
|
|
181
|
-
result = result.filter((msg) => {
|
|
182
|
-
if (selectedTypes.has(msg.collection as CollectionMessageTypes)) {
|
|
183
|
-
return true;
|
|
184
|
-
}
|
|
185
|
-
if (selectedTypes.has('specifications') && SPEC_TYPES.includes(msg.schemaExtension?.toLowerCase() || '')) {
|
|
186
|
-
return true;
|
|
187
|
-
}
|
|
188
|
-
if (selectedTypes.has('data-contracts') && msg.collection === 'data-products') {
|
|
189
|
-
return true;
|
|
190
|
-
}
|
|
191
|
-
return false;
|
|
192
|
-
});
|
|
193
|
-
}
|
|
275
|
+
let result = [...typeFilteredMessages];
|
|
194
276
|
|
|
195
277
|
if (selectedSchemaType !== 'all') {
|
|
196
|
-
result = result.filter((msg) => msg
|
|
278
|
+
result = result.filter((msg) => getSchemaFormat(msg) === selectedSchemaType);
|
|
197
279
|
}
|
|
198
280
|
|
|
199
281
|
if (searchQuery) {
|
|
@@ -213,7 +295,7 @@ export default function SchemaExplorer({ schemas }: SchemaExplorerProps) {
|
|
|
213
295
|
});
|
|
214
296
|
|
|
215
297
|
return result;
|
|
216
|
-
}, [
|
|
298
|
+
}, [typeFilteredMessages, searchQuery, selectedSchemaType]);
|
|
217
299
|
|
|
218
300
|
const totalPages = Math.ceil(filteredMessages.length / ITEMS_PER_PAGE);
|
|
219
301
|
const paginatedMessages = useMemo(() => {
|
|
@@ -338,11 +420,10 @@ export default function SchemaExplorer({ schemas }: SchemaExplorerProps) {
|
|
|
338
420
|
|
|
339
421
|
const stats = useMemo(() => {
|
|
340
422
|
return {
|
|
341
|
-
total: latestMessages.length,
|
|
342
423
|
events: latestMessages.filter((m) => m.collection === 'events').length,
|
|
343
424
|
commands: latestMessages.filter((m) => m.collection === 'commands').length,
|
|
344
425
|
queries: latestMessages.filter((m) => m.collection === 'queries').length,
|
|
345
|
-
specifications: latestMessages.filter((m) =>
|
|
426
|
+
specifications: latestMessages.filter((m) => Boolean(m.specType)).length,
|
|
346
427
|
dataContracts: latestMessages.filter((m) => m.collection === 'data-products').length,
|
|
347
428
|
};
|
|
348
429
|
}, [latestMessages]);
|
|
@@ -366,7 +447,6 @@ export default function SchemaExplorer({ schemas }: SchemaExplorerProps) {
|
|
|
366
447
|
const hasActiveFilters = searchQuery.length > 0 || selectedTypes.size > 0 || selectedSchemaType !== 'all';
|
|
367
448
|
const activeFilterCount = (selectedTypes.size > 0 ? 1 : 0) + (selectedSchemaType !== 'all' ? 1 : 0);
|
|
368
449
|
const collectionTabs = [
|
|
369
|
-
{ key: 'all', label: 'All', count: stats.total },
|
|
370
450
|
{ key: 'events', label: 'Events', count: stats.events },
|
|
371
451
|
{ key: 'commands', label: 'Commands', count: stats.commands },
|
|
372
452
|
{ key: 'queries', label: 'Queries', count: stats.queries },
|
|
@@ -402,7 +482,7 @@ export default function SchemaExplorer({ schemas }: SchemaExplorerProps) {
|
|
|
402
482
|
/>
|
|
403
483
|
</div>
|
|
404
484
|
|
|
405
|
-
{
|
|
485
|
+
{latestMessages.length > 0 && (
|
|
406
486
|
<button
|
|
407
487
|
onClick={() => setShowFormatFilters((prev) => !prev)}
|
|
408
488
|
aria-pressed={showFormatFilters}
|
|
@@ -423,110 +503,59 @@ export default function SchemaExplorer({ schemas }: SchemaExplorerProps) {
|
|
|
423
503
|
</div>
|
|
424
504
|
|
|
425
505
|
{showFormatFilters && latestMessages.length > 0 && (
|
|
426
|
-
<div className="flex-shrink-0 border-b border-[rgb(var(--ec-page-border))] px-4 py-3">
|
|
427
|
-
<div className="
|
|
428
|
-
<
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
>
|
|
441
|
-
Reset
|
|
442
|
-
</button>
|
|
443
|
-
)}
|
|
444
|
-
</div>
|
|
445
|
-
|
|
446
|
-
<div>
|
|
447
|
-
<div className="mb-2 text-[10px] font-semibold uppercase tracking-[0.18em] text-[rgb(var(--ec-page-text-muted))]">
|
|
448
|
-
Type
|
|
449
|
-
</div>
|
|
450
|
-
<div className="flex flex-wrap gap-1.5">
|
|
451
|
-
{collectionTabs.map((tab) => {
|
|
452
|
-
const isAll = tab.key === 'all';
|
|
453
|
-
const isActive = isAll ? selectedTypes.size === 0 : selectedTypes.has(tab.key as CollectionMessageTypes);
|
|
454
|
-
|
|
455
|
-
return (
|
|
456
|
-
<button
|
|
457
|
-
key={tab.key}
|
|
458
|
-
onClick={() => {
|
|
459
|
-
if (isAll) {
|
|
460
|
-
clearTypeFilters();
|
|
461
|
-
return;
|
|
462
|
-
}
|
|
463
|
-
toggleType(tab.key as CollectionMessageTypes | 'specifications' | 'data-contracts');
|
|
464
|
-
}}
|
|
465
|
-
className={`inline-flex items-center gap-1 rounded-md border px-2 py-1 text-[10px] font-medium transition-all ${
|
|
466
|
-
isActive
|
|
467
|
-
? 'border-[rgb(var(--ec-accent)/0.5)] bg-[rgb(var(--ec-accent)/0.16)] text-[rgb(var(--ec-page-text))] shadow-[0_0_0_1px_rgb(var(--ec-accent)/0.2)]'
|
|
468
|
-
: 'border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-dropdown-bg))] text-[rgb(var(--ec-page-text-muted))] hover:border-[rgb(var(--ec-page-text-muted)/0.3)] hover:text-[rgb(var(--ec-page-text))]'
|
|
469
|
-
}`}
|
|
470
|
-
>
|
|
471
|
-
<span>{tab.label}</span>
|
|
472
|
-
<span
|
|
473
|
-
className={`rounded-sm px-1 py-0.5 text-[8px] font-semibold tabular-nums ${
|
|
474
|
-
isActive
|
|
475
|
-
? 'bg-[rgb(var(--ec-accent)/0.18)] text-[rgb(var(--ec-page-text))]'
|
|
476
|
-
: 'bg-[rgb(var(--ec-content-hover))] text-[rgb(var(--ec-page-text-muted))]'
|
|
477
|
-
}`}
|
|
478
|
-
>
|
|
479
|
-
{tab.count}
|
|
480
|
-
</span>
|
|
481
|
-
</button>
|
|
482
|
-
);
|
|
483
|
-
})}
|
|
484
|
-
</div>
|
|
485
|
-
</div>
|
|
486
|
-
|
|
487
|
-
{schemaTypes.length > 0 && (
|
|
488
|
-
<div className="mt-3">
|
|
489
|
-
<div className="mb-2 flex items-center justify-between gap-3">
|
|
490
|
-
<span className="text-[10px] font-semibold uppercase tracking-[0.18em] text-[rgb(var(--ec-page-text-muted))]">
|
|
491
|
-
Format
|
|
492
|
-
</span>
|
|
493
|
-
{selectedSchemaType !== 'all' && (
|
|
494
|
-
<button
|
|
495
|
-
onClick={() => setSelectedSchemaType('all')}
|
|
496
|
-
className="text-[10px] font-medium text-[rgb(var(--ec-accent))] hover:opacity-80"
|
|
497
|
-
>
|
|
498
|
-
Clear
|
|
499
|
-
</button>
|
|
500
|
-
)}
|
|
501
|
-
</div>
|
|
502
|
-
<div className="flex flex-wrap gap-1.5">
|
|
503
|
-
<button
|
|
504
|
-
onClick={() => setSelectedSchemaType('all')}
|
|
505
|
-
className={`rounded-md border px-2 py-1 text-[10px] font-medium transition-all ${
|
|
506
|
-
selectedSchemaType === 'all'
|
|
507
|
-
? 'border-[rgb(var(--ec-accent)/0.45)] bg-[rgb(var(--ec-accent)/0.16)] text-[rgb(var(--ec-page-text))]'
|
|
508
|
-
: 'border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-dropdown-bg))] text-[rgb(var(--ec-page-text-muted))] hover:text-[rgb(var(--ec-page-text))]'
|
|
509
|
-
}`}
|
|
510
|
-
>
|
|
511
|
-
All formats
|
|
512
|
-
</button>
|
|
513
|
-
{schemaTypes.map((type) => (
|
|
514
|
-
<button
|
|
515
|
-
key={type}
|
|
516
|
-
onClick={() => setSelectedSchemaType(selectedSchemaType === type ? 'all' : type)}
|
|
517
|
-
className={`rounded-md border px-2 py-1 text-[10px] font-medium transition-all ${
|
|
518
|
-
selectedSchemaType === type
|
|
519
|
-
? 'border-[rgb(var(--ec-accent)/0.45)] bg-[rgb(var(--ec-accent)/0.16)] text-[rgb(var(--ec-page-text))]'
|
|
520
|
-
: 'border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-dropdown-bg))] text-[rgb(var(--ec-page-text-muted))] hover:text-[rgb(var(--ec-page-text))]'
|
|
521
|
-
}`}
|
|
522
|
-
>
|
|
523
|
-
{SCHEMA_TYPE_LABELS[type] || type.charAt(0).toUpperCase() + type.slice(1)}
|
|
524
|
-
</button>
|
|
525
|
-
))}
|
|
526
|
-
</div>
|
|
527
|
-
</div>
|
|
506
|
+
<div className="flex-shrink-0 space-y-3 border-b border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-content-hover)/0.3)] px-4 py-3">
|
|
507
|
+
<div className="flex items-center justify-between">
|
|
508
|
+
<span className="text-[12px] font-semibold text-[rgb(var(--ec-page-text))]">Filters</span>
|
|
509
|
+
{hasActiveFilters && (
|
|
510
|
+
<button
|
|
511
|
+
onClick={() => {
|
|
512
|
+
setSearchQuery('');
|
|
513
|
+
clearTypeFilters();
|
|
514
|
+
setSelectedSchemaType('all');
|
|
515
|
+
}}
|
|
516
|
+
className="text-[11px] font-medium text-[rgb(var(--ec-accent))] hover:opacity-80"
|
|
517
|
+
>
|
|
518
|
+
Reset
|
|
519
|
+
</button>
|
|
528
520
|
)}
|
|
529
521
|
</div>
|
|
522
|
+
|
|
523
|
+
<FilterSection title="Type">
|
|
524
|
+
{collectionTabs.map((tab) => (
|
|
525
|
+
<FilterOption
|
|
526
|
+
key={tab.key}
|
|
527
|
+
control="checkbox"
|
|
528
|
+
label={tab.label}
|
|
529
|
+
count={tab.count}
|
|
530
|
+
icon={<TypeIcon type={tab.key} />}
|
|
531
|
+
isActive={selectedTypes.has(tab.key as CollectionMessageTypes)}
|
|
532
|
+
onClick={() => toggleType(tab.key as CollectionMessageTypes | 'specifications' | 'data-contracts')}
|
|
533
|
+
/>
|
|
534
|
+
))}
|
|
535
|
+
</FilterSection>
|
|
536
|
+
|
|
537
|
+
{formatOptions.length > 0 && (
|
|
538
|
+
<FilterSection title="Format" role="radiogroup">
|
|
539
|
+
<FilterOption
|
|
540
|
+
control="radio"
|
|
541
|
+
label="All formats"
|
|
542
|
+
count={typeFilteredMessages.length}
|
|
543
|
+
isActive={selectedSchemaType === 'all'}
|
|
544
|
+
onClick={() => setSelectedSchemaType('all')}
|
|
545
|
+
/>
|
|
546
|
+
{formatOptions.map(({ format, count }) => (
|
|
547
|
+
<FilterOption
|
|
548
|
+
key={format}
|
|
549
|
+
control="radio"
|
|
550
|
+
label={SCHEMA_TYPE_LABELS[format] || format.charAt(0).toUpperCase() + format.slice(1)}
|
|
551
|
+
count={count}
|
|
552
|
+
icon={<FormatIcon format={format} />}
|
|
553
|
+
isActive={selectedSchemaType === format}
|
|
554
|
+
onClick={() => setSelectedSchemaType(format)}
|
|
555
|
+
/>
|
|
556
|
+
))}
|
|
557
|
+
</FilterSection>
|
|
558
|
+
)}
|
|
530
559
|
</div>
|
|
531
560
|
)}
|
|
532
561
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { buildUrl } from '@utils/url-builder';
|
|
2
1
|
import type { SchemaItem } from './types';
|
|
3
|
-
import {
|
|
2
|
+
import { getSchemaDisplayLabel, getSchemaIconUrl, getFormatBadge, extractServiceName } from './utils';
|
|
4
3
|
|
|
5
4
|
interface SchemaListItemProps {
|
|
6
5
|
message: SchemaItem;
|
|
@@ -34,7 +33,7 @@ function getNamespace(message: SchemaItem): string | null {
|
|
|
34
33
|
|
|
35
34
|
export default function SchemaListItem({ message, isSelected, versions, onClick, itemRef }: SchemaListItemProps) {
|
|
36
35
|
const ext = message.schemaExtension?.toLowerCase() || '';
|
|
37
|
-
const
|
|
36
|
+
const iconUrl = getSchemaIconUrl(message);
|
|
38
37
|
const { label: formatLabel, color: formatColor } = getFormatBadge(ext);
|
|
39
38
|
const namespace = getNamespace(message);
|
|
40
39
|
const summary = message.data.summary || `Browse the ${message.data.name} schema.`;
|
|
@@ -52,8 +51,8 @@ export default function SchemaListItem({ message, isSelected, versions, onClick,
|
|
|
52
51
|
>
|
|
53
52
|
<div className="flex items-start justify-between gap-2">
|
|
54
53
|
<div className="mt-0.5 flex h-4.5 w-4.5 flex-shrink-0 items-center justify-center rounded-sm border border-[rgb(var(--ec-page-border)/0.55)] bg-[rgb(var(--ec-content-hover))]">
|
|
55
|
-
{
|
|
56
|
-
<img src={
|
|
54
|
+
{iconUrl ? (
|
|
55
|
+
<img src={iconUrl} alt={`${ext} icon`} className="schema-icon h-2.5 w-2.5" />
|
|
57
56
|
) : (
|
|
58
57
|
<span className={`font-mono text-[6px] font-bold uppercase ${formatColor}`}>{formatLabel}</span>
|
|
59
58
|
)}
|
|
@@ -82,9 +81,7 @@ export default function SchemaListItem({ message, isSelected, versions, onClick,
|
|
|
82
81
|
<div className="mt-1.5 flex items-end justify-between gap-2">
|
|
83
82
|
<div className="min-w-0">
|
|
84
83
|
<div className="flex flex-wrap items-center gap-x-1.5 gap-y-0.5 text-[9px] text-[rgb(var(--ec-page-text-muted))]">
|
|
85
|
-
<span className="font-medium text-[rgb(var(--ec-page-text-muted))]">
|
|
86
|
-
{getSchemaTypeLabel(message.schemaExtension)}
|
|
87
|
-
</span>
|
|
84
|
+
<span className="font-medium text-[rgb(var(--ec-page-text-muted))]">{getSchemaDisplayLabel(message)}</span>
|
|
88
85
|
{namespace && (
|
|
89
86
|
<>
|
|
90
87
|
<span className="opacity-35">›</span>
|
|
@@ -79,6 +79,8 @@ export interface SchemaItem {
|
|
|
79
79
|
schemaName?: string;
|
|
80
80
|
source?: SchemaSourceInfo;
|
|
81
81
|
specType?: string;
|
|
82
|
+
/** A generic specification's custom icon */
|
|
83
|
+
specIcon?: string;
|
|
82
84
|
specName?: string;
|
|
83
85
|
specFilenameWithoutExtension?: string;
|
|
84
86
|
// For data contracts
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { SchemaRelationshipCollection } from './types';
|
|
1
|
+
import type { SchemaItem, SchemaRelationshipCollection } from './types';
|
|
2
2
|
import { buildUrl } from '@utils/url-builder';
|
|
3
|
+
import { getGenericSpecificationIcon, resolveIconUrl } from '@utils/icon';
|
|
3
4
|
|
|
4
5
|
// Schema types that should use SVG icons
|
|
5
6
|
export const ICON_SPECS: Record<string, string> = {
|
|
@@ -14,6 +15,72 @@ export const ICON_SPECS: Record<string, string> = {
|
|
|
14
15
|
json: 'json-schema',
|
|
15
16
|
};
|
|
16
17
|
|
|
18
|
+
type SchemaFormat = Pick<SchemaItem, 'schemaExtension' | 'specType' | 'specIcon'>;
|
|
19
|
+
|
|
20
|
+
/** The icon for a schema, or undefined when it shows a format badge instead */
|
|
21
|
+
export function getSchemaIconUrl(message: SchemaFormat): string | undefined {
|
|
22
|
+
if (message.specType === 'generic') return resolveIconUrl(getGenericSpecificationIcon(message.specIcon));
|
|
23
|
+
const iconSpec = ICON_SPECS[message.schemaExtension?.toLowerCase() || ''];
|
|
24
|
+
return iconSpec ? buildUrl(`/icons/${iconSpec}.svg`, true) : undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// A generic specification's extension is only its syntax, so a JSON file is not called a JSON Schema
|
|
28
|
+
const GENERIC_SPECIFICATION_LABELS: Record<string, string> = { json: 'JSON', yaml: 'YAML', xml: 'XML', text: 'Text' };
|
|
29
|
+
|
|
30
|
+
export const getSchemaDisplayLabel = (message: SchemaFormat): string =>
|
|
31
|
+
(message.specType === 'generic' && GENERIC_SPECIFICATION_LABELS[message.schemaExtension || '']) ||
|
|
32
|
+
getSchemaTypeLabel(message.schemaExtension);
|
|
33
|
+
|
|
34
|
+
// Extensions that are the same format, so they share one filter
|
|
35
|
+
const FORMAT_ALIASES: Record<string, string> = { yml: 'yaml', gql: 'graphql', avsc: 'avro', protobuf: 'proto' };
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The format a schema is filtered by in the Schema Explorer. A generic specification's extension is only its syntax,
|
|
39
|
+
* so a generic JSON file is plain JSON (`json-document`), not a JSON Schema.
|
|
40
|
+
*/
|
|
41
|
+
export function getSchemaFormat(message: Pick<SchemaItem, 'schemaExtension' | 'specType'>): string | undefined {
|
|
42
|
+
if (!message.schemaExtension) return undefined;
|
|
43
|
+
if (message.specType === 'generic' && message.schemaExtension.toLowerCase() === 'json') return 'json-document';
|
|
44
|
+
return normalizeSchemaFormat(message.schemaExtension);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** A format with extensions of the same format merged, so a filter saved as `avsc` still selects Avro */
|
|
48
|
+
export const normalizeSchemaFormat = (format: string): string => FORMAT_ALIASES[format.toLowerCase()] ?? format.toLowerCase();
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The extension to download a schema with. A generic specification's schemaExtension is only the syntax it's
|
|
52
|
+
* highlighted in (a WSDL is `xml`), so it keeps its own file's extension.
|
|
53
|
+
*/
|
|
54
|
+
export function getSchemaDownloadExtension(message: Pick<SchemaItem, 'schemaExtension' | 'specType' | 'data'>): string {
|
|
55
|
+
const fileExtension =
|
|
56
|
+
message.specType === 'generic'
|
|
57
|
+
? message.data.schemaPath
|
|
58
|
+
?.split('/')
|
|
59
|
+
.pop()
|
|
60
|
+
?.match(/\.([^.]+)$/)?.[1]
|
|
61
|
+
: undefined;
|
|
62
|
+
return fileExtension || message.schemaExtension || 'json';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The formats of the given schemas with how many use each, most used first. The selected format is always included
|
|
67
|
+
* (with a count of 0 when none of the schemas use it) so it can be cleared.
|
|
68
|
+
*/
|
|
69
|
+
export function getFormatOptions(
|
|
70
|
+
messages: Pick<SchemaItem, 'schemaExtension' | 'specType'>[],
|
|
71
|
+
selectedFormat?: string
|
|
72
|
+
): { format: string; count: number }[] {
|
|
73
|
+
const counts = new Map<string, number>();
|
|
74
|
+
for (const message of messages) {
|
|
75
|
+
const format = getSchemaFormat(message);
|
|
76
|
+
if (format) counts.set(format, (counts.get(format) ?? 0) + 1);
|
|
77
|
+
}
|
|
78
|
+
if (selectedFormat && !counts.has(selectedFormat)) counts.set(selectedFormat, 0);
|
|
79
|
+
return [...counts]
|
|
80
|
+
.map(([format, count]) => ({ format, count }))
|
|
81
|
+
.sort((a, b) => b.count - a.count || a.format.localeCompare(b.format));
|
|
82
|
+
}
|
|
83
|
+
|
|
17
84
|
// Schema format badge config for non-icon types: short label + color
|
|
18
85
|
export function getFormatBadge(ext?: string): { label: string; color: string } {
|
|
19
86
|
const type = ext?.toLowerCase();
|
|
@@ -105,6 +172,8 @@ export const getLanguageForHighlight = (extension?: string): string => {
|
|
|
105
172
|
case 'ts':
|
|
106
173
|
case 'typescript':
|
|
107
174
|
return 'typescript';
|
|
175
|
+
case 'text':
|
|
176
|
+
return 'text';
|
|
108
177
|
case 'js':
|
|
109
178
|
case 'javascript':
|
|
110
179
|
return 'javascript';
|
|
@@ -146,9 +146,11 @@ const baseSchema = z.object({
|
|
|
146
146
|
}),
|
|
147
147
|
z.array(
|
|
148
148
|
z.object({
|
|
149
|
-
type: z.enum(['openapi', 'asyncapi', 'graphql']),
|
|
149
|
+
type: z.enum(['openapi', 'asyncapi', 'graphql', 'generic']),
|
|
150
150
|
path: z.string(),
|
|
151
151
|
name: z.string().optional(),
|
|
152
|
+
// Used by generic specifications only
|
|
153
|
+
icon: z.string().optional(),
|
|
152
154
|
headers: z.record(z.string(), z.string()).optional(),
|
|
153
155
|
})
|
|
154
156
|
),
|
|
@@ -35,6 +35,10 @@ import {
|
|
|
35
35
|
|
|
36
36
|
const catalogDirectory = process.env.PROJECT_DIR || process.cwd();
|
|
37
37
|
|
|
38
|
+
// How many model calls one answer may take. Questions across several domains need many tool calls, so the last
|
|
39
|
+
// step has tools turned off: the model has to answer with what it found, rather than stop without a reply.
|
|
40
|
+
const MAX_STEPS = 10;
|
|
41
|
+
|
|
38
42
|
export const defaultConfiguration = {
|
|
39
43
|
temperature: 0.4,
|
|
40
44
|
topP: 0.9,
|
|
@@ -394,7 +398,8 @@ export const POST = async ({ request }: APIContext<{ question: string; messages:
|
|
|
394
398
|
instructions: getBaseSystemPrompt(referrer ?? ''),
|
|
395
399
|
messages: await convertToModelMessages(messages),
|
|
396
400
|
temperature: modelConfiguration?.temperature ?? 0.7,
|
|
397
|
-
stopWhen: isStepCount(
|
|
401
|
+
stopWhen: isStepCount(MAX_STEPS),
|
|
402
|
+
prepareStep: ({ stepNumber }) => (stepNumber === MAX_STEPS - 1 ? { toolChoice: 'none' } : undefined),
|
|
398
403
|
onError: (error) => {
|
|
399
404
|
console.error('[Chat] On error', error);
|
|
400
405
|
},
|