@eventcatalog/core 2.63.0 → 2.64.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analytics/analytics.cjs +1 -1
- package/dist/analytics/analytics.js +2 -2
- package/dist/analytics/log-build.cjs +1 -1
- package/dist/analytics/log-build.js +3 -3
- package/dist/{chunk-I2FMV7LN.js → chunk-6AMZOBWI.js} +1 -1
- package/dist/{chunk-IRFM5IS7.js → chunk-CWGFHLMX.js} +1 -1
- package/dist/{chunk-GA274FBN.js → chunk-PLMTJHGH.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog.cjs +1 -1
- package/dist/eventcatalog.js +3 -3
- package/eventcatalog/astro.config.mjs +2 -1
- package/eventcatalog/public/icons/avro.svg +21 -0
- package/eventcatalog/public/icons/json-schema.svg +6 -0
- package/eventcatalog/public/icons/proto.svg +10 -0
- package/eventcatalog/src/components/Grids/utils.tsx +5 -3
- package/eventcatalog/src/components/MDX/RemoteFile.astro +5 -11
- package/eventcatalog/src/components/MDX/SchemaViewer/SchemaViewerRoot.astro +41 -6
- package/eventcatalog/src/components/SchemaExplorer/ApiAccessSection.tsx +139 -0
- package/eventcatalog/src/components/SchemaExplorer/AvroSchemaViewer.tsx +423 -0
- package/eventcatalog/src/components/SchemaExplorer/DiffViewer.tsx +102 -0
- package/eventcatalog/src/components/SchemaExplorer/JSONSchemaViewer.tsx +740 -0
- package/eventcatalog/src/components/SchemaExplorer/OwnersSection.tsx +56 -0
- package/eventcatalog/src/components/SchemaExplorer/Pagination.tsx +33 -0
- package/eventcatalog/src/components/SchemaExplorer/ProducersConsumersSection.tsx +91 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaCodeModal.tsx +93 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaContentViewer.tsx +130 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsHeader.tsx +181 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsPanel.tsx +232 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaExplorer.tsx +415 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaFilters.tsx +174 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaListItem.tsx +73 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaViewerModal.tsx +77 -0
- package/eventcatalog/src/components/SchemaExplorer/VersionHistoryModal.tsx +72 -0
- package/eventcatalog/src/components/SchemaExplorer/types.ts +45 -0
- package/eventcatalog/src/components/SchemaExplorer/utils.ts +81 -0
- package/eventcatalog/src/components/SideNav/ListViewSideBar/index.tsx +33 -2
- package/eventcatalog/src/components/Tables/columns/MessageTableColumns.tsx +2 -2
- package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +10 -0
- package/eventcatalog/src/pages/api/schemas/[collection]/[id]/[version]/index.ts +45 -0
- package/eventcatalog/src/pages/api/schemas/services/[id]/[version]/[specification]/index.ts +51 -0
- package/eventcatalog/src/pages/docs/llm/schemas.txt.ts +86 -0
- package/eventcatalog/src/pages/schemas/index.astro +175 -0
- package/eventcatalog/src/utils/files.ts +9 -0
- package/package.json +1 -1
- package/eventcatalog/src/components/MDX/SchemaViewer/SchemaProperty.astro +0 -204
- package/eventcatalog/src/components/MDX/SchemaViewer/SchemaViewer.astro +0 -705
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { useState, useMemo } from 'react';
|
|
2
|
+
import * as Diff from 'diff';
|
|
3
|
+
import { html } from 'diff2html';
|
|
4
|
+
import 'diff2html/bundles/css/diff2html.min.css';
|
|
5
|
+
import SchemaDetailsHeader from './SchemaDetailsHeader';
|
|
6
|
+
import ApiAccessSection from './ApiAccessSection';
|
|
7
|
+
import OwnersSection from './OwnersSection';
|
|
8
|
+
import ProducersConsumersSection from './ProducersConsumersSection';
|
|
9
|
+
import SchemaContentViewer from './SchemaContentViewer';
|
|
10
|
+
import DiffViewer from './DiffViewer';
|
|
11
|
+
import VersionHistoryModal from './VersionHistoryModal';
|
|
12
|
+
import SchemaCodeModal from './SchemaCodeModal';
|
|
13
|
+
import SchemaViewerModal from './SchemaViewerModal';
|
|
14
|
+
import { copyToClipboard, downloadSchema } from './utils';
|
|
15
|
+
import type { SchemaItem, VersionDiff } from './types';
|
|
16
|
+
|
|
17
|
+
interface SchemaDetailsPanelProps {
|
|
18
|
+
message: SchemaItem;
|
|
19
|
+
availableVersions: SchemaItem[];
|
|
20
|
+
selectedVersion: string | null;
|
|
21
|
+
onVersionChange: (version: string) => void;
|
|
22
|
+
apiAccessEnabled?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default function SchemaDetailsPanel({
|
|
26
|
+
message,
|
|
27
|
+
availableVersions,
|
|
28
|
+
selectedVersion,
|
|
29
|
+
onVersionChange,
|
|
30
|
+
apiAccessEnabled = false,
|
|
31
|
+
}: SchemaDetailsPanelProps) {
|
|
32
|
+
const [copiedId, setCopiedId] = useState<string | null>(null);
|
|
33
|
+
const [schemaViewMode, setSchemaViewMode] = useState<'code' | 'schema' | 'diff'>('code');
|
|
34
|
+
const [apiAccessExpanded, setApiAccessExpanded] = useState(false);
|
|
35
|
+
const [ownersExpanded, setOwnersExpanded] = useState(false);
|
|
36
|
+
const [producersConsumersExpanded, setProducersConsumersExpanded] = useState(false);
|
|
37
|
+
const [isDiffModalOpen, setIsDiffModalOpen] = useState(false);
|
|
38
|
+
const [isCodeModalOpen, setIsCodeModalOpen] = useState(false);
|
|
39
|
+
const [isSchemaViewerModalOpen, setIsSchemaViewerModalOpen] = useState(false);
|
|
40
|
+
|
|
41
|
+
const hasMultipleVersions = availableVersions.length > 1;
|
|
42
|
+
|
|
43
|
+
// Generate diffs between all consecutive versions
|
|
44
|
+
const allDiffs: VersionDiff[] = useMemo(() => {
|
|
45
|
+
const diffs: VersionDiff[] = [];
|
|
46
|
+
if (!hasMultipleVersions) return diffs;
|
|
47
|
+
|
|
48
|
+
for (let i = 0; i < availableVersions.length - 1; i++) {
|
|
49
|
+
const newerVersion = availableVersions[i];
|
|
50
|
+
const olderVersion = availableVersions[i + 1];
|
|
51
|
+
|
|
52
|
+
if (newerVersion.schemaContent && olderVersion.schemaContent) {
|
|
53
|
+
const diff = Diff.createTwoFilesPatch(
|
|
54
|
+
`v${olderVersion.data.version}`,
|
|
55
|
+
`v${newerVersion.data.version}`,
|
|
56
|
+
olderVersion.schemaContent,
|
|
57
|
+
newerVersion.schemaContent,
|
|
58
|
+
'',
|
|
59
|
+
'',
|
|
60
|
+
{ context: 3 }
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const diffHtml = html(diff, {
|
|
64
|
+
drawFileList: false,
|
|
65
|
+
matching: 'lines',
|
|
66
|
+
outputFormat: 'side-by-side',
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
diffs.push({
|
|
70
|
+
newerVersion: newerVersion.data.version,
|
|
71
|
+
olderVersion: olderVersion.data.version,
|
|
72
|
+
diffHtml,
|
|
73
|
+
newerContent: newerVersion.schemaContent,
|
|
74
|
+
olderContent: olderVersion.schemaContent,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return diffs;
|
|
79
|
+
}, [availableVersions, hasMultipleVersions]);
|
|
80
|
+
|
|
81
|
+
// Check if this is a JSON schema
|
|
82
|
+
const parsedSchema = useMemo(() => {
|
|
83
|
+
const isJSONSchema =
|
|
84
|
+
message.schemaExtension?.toLowerCase() === 'json' && message.schemaContent && message.schemaContent.trim() !== '';
|
|
85
|
+
if (!isJSONSchema) return null;
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const parsed = JSON.parse(message.schemaContent ?? '');
|
|
89
|
+
// Check if it's actually a JSON Schema (has properties or $schema field)
|
|
90
|
+
if (!parsed.properties && !parsed.$schema && !parsed.type) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
return parsed;
|
|
94
|
+
} catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}, [message.schemaContent, message.schemaExtension]);
|
|
98
|
+
|
|
99
|
+
// Check if this is an Avro schema
|
|
100
|
+
const parsedAvroSchema = useMemo(() => {
|
|
101
|
+
const ext = message.schemaExtension?.toLowerCase();
|
|
102
|
+
const isAvroSchema = (ext === 'avro' || ext === 'avsc') && message.schemaContent && message.schemaContent.trim() !== '';
|
|
103
|
+
if (!isAvroSchema) return null;
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const parsed = JSON.parse(message.schemaContent ?? '');
|
|
107
|
+
// Check if it's actually an Avro Schema (has type field, typically "record")
|
|
108
|
+
if (!parsed.type) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
return parsed;
|
|
112
|
+
} catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}, [message.schemaContent, message.schemaExtension]);
|
|
116
|
+
|
|
117
|
+
const handleCopy = async () => {
|
|
118
|
+
if (!message.schemaContent) return;
|
|
119
|
+
const success = await copyToClipboard(message.schemaContent);
|
|
120
|
+
if (success) {
|
|
121
|
+
setCopiedId(message.data.id);
|
|
122
|
+
setTimeout(() => setCopiedId(null), 2000);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const handleCopyCustom = async (content: string, id: string) => {
|
|
127
|
+
const success = await copyToClipboard(content);
|
|
128
|
+
if (success) {
|
|
129
|
+
setCopiedId(id);
|
|
130
|
+
setTimeout(() => setCopiedId(null), 2000);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const handleDownload = () => {
|
|
135
|
+
if (!message.schemaContent) return;
|
|
136
|
+
downloadSchema(message.schemaContent, message.data.id, message.schemaExtension || 'json');
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const isCopied = copiedId === message.data.id;
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<div className="h-full flex flex-col bg-white overflow-hidden">
|
|
143
|
+
{/* Header */}
|
|
144
|
+
<SchemaDetailsHeader
|
|
145
|
+
message={message}
|
|
146
|
+
availableVersions={availableVersions}
|
|
147
|
+
selectedVersion={selectedVersion}
|
|
148
|
+
onVersionChange={onVersionChange}
|
|
149
|
+
onCopy={handleCopy}
|
|
150
|
+
onDownload={handleDownload}
|
|
151
|
+
isCopied={isCopied}
|
|
152
|
+
schemaViewMode={schemaViewMode}
|
|
153
|
+
onViewModeChange={setSchemaViewMode}
|
|
154
|
+
hasParsedSchema={!!parsedSchema || !!parsedAvroSchema}
|
|
155
|
+
hasDiffs={allDiffs.length > 0}
|
|
156
|
+
diffCount={allDiffs.length}
|
|
157
|
+
/>
|
|
158
|
+
|
|
159
|
+
{/* API Access Section - Always show, but content changes based on Scale access */}
|
|
160
|
+
<ApiAccessSection
|
|
161
|
+
message={message}
|
|
162
|
+
isExpanded={apiAccessExpanded}
|
|
163
|
+
onToggle={() => setApiAccessExpanded(!apiAccessExpanded)}
|
|
164
|
+
onCopy={handleCopyCustom}
|
|
165
|
+
copiedId={copiedId}
|
|
166
|
+
apiAccessEnabled={apiAccessEnabled}
|
|
167
|
+
/>
|
|
168
|
+
|
|
169
|
+
{/* Producers and Consumers Section - Only show for messages (not services) */}
|
|
170
|
+
{message.collection !== 'services' && (
|
|
171
|
+
<ProducersConsumersSection
|
|
172
|
+
message={message}
|
|
173
|
+
isExpanded={producersConsumersExpanded}
|
|
174
|
+
onToggle={() => setProducersConsumersExpanded(!producersConsumersExpanded)}
|
|
175
|
+
/>
|
|
176
|
+
)}
|
|
177
|
+
|
|
178
|
+
{/* Owners Section */}
|
|
179
|
+
<OwnersSection message={message} isExpanded={ownersExpanded} onToggle={() => setOwnersExpanded(!ownersExpanded)} />
|
|
180
|
+
|
|
181
|
+
{/* Schema Content - Takes full remaining height */}
|
|
182
|
+
<div className="flex-1 overflow-hidden">
|
|
183
|
+
{schemaViewMode === 'diff' && allDiffs.length > 0 ? (
|
|
184
|
+
<DiffViewer diffs={allDiffs} onOpenFullscreen={() => setIsDiffModalOpen(true)} apiAccessEnabled={apiAccessEnabled} />
|
|
185
|
+
) : (
|
|
186
|
+
<SchemaContentViewer
|
|
187
|
+
message={message}
|
|
188
|
+
onCopy={handleCopy}
|
|
189
|
+
isCopied={isCopied}
|
|
190
|
+
viewMode={schemaViewMode}
|
|
191
|
+
parsedSchema={parsedSchema}
|
|
192
|
+
parsedAvroSchema={parsedAvroSchema}
|
|
193
|
+
onOpenFullscreen={
|
|
194
|
+
schemaViewMode === 'code'
|
|
195
|
+
? () => setIsCodeModalOpen(true)
|
|
196
|
+
: schemaViewMode === 'schema' && (parsedSchema || parsedAvroSchema)
|
|
197
|
+
? () => setIsSchemaViewerModalOpen(true)
|
|
198
|
+
: undefined
|
|
199
|
+
}
|
|
200
|
+
/>
|
|
201
|
+
)}
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
{/* Version History Modal */}
|
|
205
|
+
<VersionHistoryModal
|
|
206
|
+
isOpen={isDiffModalOpen}
|
|
207
|
+
onOpenChange={setIsDiffModalOpen}
|
|
208
|
+
diffs={allDiffs}
|
|
209
|
+
messageName={message.data.name}
|
|
210
|
+
apiAccessEnabled={apiAccessEnabled}
|
|
211
|
+
/>
|
|
212
|
+
|
|
213
|
+
{/* Schema Code Modal */}
|
|
214
|
+
<SchemaCodeModal
|
|
215
|
+
isOpen={isCodeModalOpen}
|
|
216
|
+
onOpenChange={setIsCodeModalOpen}
|
|
217
|
+
message={message}
|
|
218
|
+
onCopy={handleCopy}
|
|
219
|
+
isCopied={isCopied}
|
|
220
|
+
/>
|
|
221
|
+
|
|
222
|
+
{/* Schema Viewer Modal */}
|
|
223
|
+
<SchemaViewerModal
|
|
224
|
+
isOpen={isSchemaViewerModalOpen}
|
|
225
|
+
onOpenChange={setIsSchemaViewerModalOpen}
|
|
226
|
+
message={message}
|
|
227
|
+
parsedSchema={parsedSchema}
|
|
228
|
+
parsedAvroSchema={parsedAvroSchema}
|
|
229
|
+
/>
|
|
230
|
+
</div>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import { useState, useMemo, useEffect, useRef } from 'react';
|
|
2
|
+
import { DocumentTextIcon, FunnelIcon } from '@heroicons/react/24/outline';
|
|
3
|
+
import type { CollectionMessageTypes } from '@types';
|
|
4
|
+
import semver from 'semver';
|
|
5
|
+
import SchemaFilters from './SchemaFilters';
|
|
6
|
+
import SchemaListItem from './SchemaListItem';
|
|
7
|
+
import SchemaDetailsPanel from './SchemaDetailsPanel';
|
|
8
|
+
import Pagination from './Pagination';
|
|
9
|
+
import type { SchemaItem } from './types';
|
|
10
|
+
|
|
11
|
+
interface SchemaExplorerProps {
|
|
12
|
+
schemas: SchemaItem[];
|
|
13
|
+
apiAccessEnabled?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default function SchemaExplorer({ schemas, apiAccessEnabled = false }: SchemaExplorerProps) {
|
|
17
|
+
const [searchQuery, setSearchQuery] = useState(() => {
|
|
18
|
+
// Load from localStorage
|
|
19
|
+
if (typeof window !== 'undefined') {
|
|
20
|
+
const stored = localStorage.getItem('schemaRegistrySearchQuery');
|
|
21
|
+
return stored !== null ? stored : '';
|
|
22
|
+
}
|
|
23
|
+
return '';
|
|
24
|
+
});
|
|
25
|
+
const [selectedType, setSelectedType] = useState<'all' | CollectionMessageTypes | 'services'>(() => {
|
|
26
|
+
// Load from localStorage
|
|
27
|
+
if (typeof window !== 'undefined') {
|
|
28
|
+
const stored = localStorage.getItem('schemaRegistrySelectedType');
|
|
29
|
+
return stored !== null ? (stored as 'all' | CollectionMessageTypes | 'services') : 'all';
|
|
30
|
+
}
|
|
31
|
+
return 'all';
|
|
32
|
+
});
|
|
33
|
+
const [selectedSchemaType, setSelectedSchemaType] = useState<'all' | string>(() => {
|
|
34
|
+
// Load from localStorage
|
|
35
|
+
if (typeof window !== 'undefined') {
|
|
36
|
+
const stored = localStorage.getItem('schemaRegistrySelectedSchemaType');
|
|
37
|
+
return stored !== null ? stored : 'all';
|
|
38
|
+
}
|
|
39
|
+
return 'all';
|
|
40
|
+
});
|
|
41
|
+
const [selectedMessage, setSelectedMessage] = useState<SchemaItem | null>(null);
|
|
42
|
+
const [selectedVersion, setSelectedVersion] = useState<string | null>(null);
|
|
43
|
+
const [currentPage, setCurrentPage] = useState(1);
|
|
44
|
+
const [filtersExpanded, setFiltersExpanded] = useState(() => {
|
|
45
|
+
if (typeof window !== 'undefined') {
|
|
46
|
+
const stored = localStorage.getItem('schemaRegistryFiltersExpanded');
|
|
47
|
+
return stored !== null ? stored === 'true' : true;
|
|
48
|
+
}
|
|
49
|
+
return true;
|
|
50
|
+
});
|
|
51
|
+
const [isMounted, setIsMounted] = useState(false);
|
|
52
|
+
const searchInputRef = useRef<HTMLInputElement>(null);
|
|
53
|
+
const selectedItemRef = useRef<HTMLButtonElement>(null);
|
|
54
|
+
const ITEMS_PER_PAGE = 50;
|
|
55
|
+
|
|
56
|
+
// Set mounted state after hydration to prevent FOUC
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
setIsMounted(true);
|
|
59
|
+
}, []);
|
|
60
|
+
|
|
61
|
+
// Function to update URL with query params
|
|
62
|
+
const updateUrlParams = (message: SchemaItem) => {
|
|
63
|
+
if (typeof window === 'undefined') return;
|
|
64
|
+
|
|
65
|
+
const params = new URLSearchParams();
|
|
66
|
+
params.set('id', message.data.id);
|
|
67
|
+
params.set('version', message.data.version);
|
|
68
|
+
params.set('collection', message.collection);
|
|
69
|
+
|
|
70
|
+
// For services, add spec type
|
|
71
|
+
if (message.collection === 'services') {
|
|
72
|
+
params.set('specType', message.specType || 'unknown');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const newUrl = `${window.location.pathname}?${params.toString()}`;
|
|
76
|
+
window.history.pushState({}, '', newUrl);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Group messages by ID (and spec type for services) and get all versions
|
|
80
|
+
const messagesByIdAndVersions = useMemo(() => {
|
|
81
|
+
const grouped = new Map<string, SchemaItem[]>();
|
|
82
|
+
schemas.forEach((message) => {
|
|
83
|
+
// For services, group by ID + spec type to keep different specs separate
|
|
84
|
+
const groupKey =
|
|
85
|
+
message.collection === 'services' ? `${message.data.id}__${message.specType || 'unknown'}` : message.data.id;
|
|
86
|
+
|
|
87
|
+
const existingVersions = grouped.get(groupKey) || [];
|
|
88
|
+
grouped.set(groupKey, [...existingVersions, message]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Sort versions for each ID (descending - latest first)
|
|
92
|
+
grouped.forEach((versions, id) => {
|
|
93
|
+
versions.sort((a, b) => {
|
|
94
|
+
const aVersion = a.data.version;
|
|
95
|
+
const bVersion = b.data.version;
|
|
96
|
+
|
|
97
|
+
// Try to use semver for comparison
|
|
98
|
+
const aValid = semver.valid(semver.coerce(aVersion));
|
|
99
|
+
const bValid = semver.valid(semver.coerce(bVersion));
|
|
100
|
+
|
|
101
|
+
if (aValid && bValid) {
|
|
102
|
+
return semver.rcompare(aValid, bValid); // descending order
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Fall back to numeric comparison
|
|
106
|
+
const aNum = parseFloat(aVersion);
|
|
107
|
+
const bNum = parseFloat(bVersion);
|
|
108
|
+
if (!isNaN(aNum) && !isNaN(bNum)) {
|
|
109
|
+
return bNum - aNum;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Final fallback to string comparison
|
|
113
|
+
return bVersion.localeCompare(aVersion);
|
|
114
|
+
});
|
|
115
|
+
grouped.set(id, versions);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
return grouped;
|
|
119
|
+
}, [schemas]);
|
|
120
|
+
|
|
121
|
+
// Get latest version for each message (for sidebar display)
|
|
122
|
+
const latestMessages = useMemo(() => {
|
|
123
|
+
return Array.from(messagesByIdAndVersions.values()).map((versions) => versions[0]);
|
|
124
|
+
}, [messagesByIdAndVersions]);
|
|
125
|
+
|
|
126
|
+
// Get unique schema types
|
|
127
|
+
const schemaTypes = useMemo(() => {
|
|
128
|
+
const types = new Set<string>();
|
|
129
|
+
latestMessages.forEach((msg) => {
|
|
130
|
+
if (msg.schemaExtension) {
|
|
131
|
+
types.add(msg.schemaExtension.toLowerCase());
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
return Array.from(types).sort();
|
|
135
|
+
}, [latestMessages]);
|
|
136
|
+
|
|
137
|
+
// Filter messages (using latest versions only)
|
|
138
|
+
const filteredMessages = useMemo(() => {
|
|
139
|
+
let result = [...latestMessages];
|
|
140
|
+
|
|
141
|
+
// Filter by message type
|
|
142
|
+
if (selectedType !== 'all') {
|
|
143
|
+
result = result.filter((msg) => msg.collection === selectedType);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Filter by schema type
|
|
147
|
+
if (selectedSchemaType !== 'all') {
|
|
148
|
+
result = result.filter((msg) => msg.schemaExtension?.toLowerCase() === selectedSchemaType);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Filter by search query
|
|
152
|
+
if (searchQuery) {
|
|
153
|
+
const query = searchQuery.toLowerCase();
|
|
154
|
+
result = result.filter(
|
|
155
|
+
(msg) =>
|
|
156
|
+
msg.data.name?.toLowerCase().includes(query) ||
|
|
157
|
+
msg.data.summary?.toLowerCase().includes(query) ||
|
|
158
|
+
msg.data.id?.toLowerCase().includes(query)
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Sort by name alphabetically
|
|
163
|
+
result.sort((a, b) => {
|
|
164
|
+
const nameA = a.data.name?.toLowerCase() || '';
|
|
165
|
+
const nameB = b.data.name?.toLowerCase() || '';
|
|
166
|
+
return nameA.localeCompare(nameB);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
return result;
|
|
170
|
+
}, [latestMessages, searchQuery, selectedType, selectedSchemaType]);
|
|
171
|
+
|
|
172
|
+
// Pagination
|
|
173
|
+
const totalPages = Math.ceil(filteredMessages.length / ITEMS_PER_PAGE);
|
|
174
|
+
const paginatedMessages = useMemo(() => {
|
|
175
|
+
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
|
|
176
|
+
return filteredMessages.slice(startIndex, startIndex + ITEMS_PER_PAGE);
|
|
177
|
+
}, [filteredMessages, currentPage]);
|
|
178
|
+
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
setCurrentPage(1);
|
|
181
|
+
}, [searchQuery, selectedType, selectedSchemaType]);
|
|
182
|
+
|
|
183
|
+
// Load from query string on mount
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
if (typeof window === 'undefined') return;
|
|
186
|
+
|
|
187
|
+
const params = new URLSearchParams(window.location.search);
|
|
188
|
+
const id = params.get('id');
|
|
189
|
+
const version = params.get('version');
|
|
190
|
+
const collection = params.get('collection');
|
|
191
|
+
const specType = params.get('specType');
|
|
192
|
+
|
|
193
|
+
if (id && version) {
|
|
194
|
+
// Find the matching message
|
|
195
|
+
const matchingMessage = schemas.find((msg) => {
|
|
196
|
+
const idMatch = msg.data.id === id;
|
|
197
|
+
const versionMatch = msg.data.version === version;
|
|
198
|
+
const collectionMatch = !collection || msg.collection === collection;
|
|
199
|
+
|
|
200
|
+
// For services, also match spec type
|
|
201
|
+
if (msg.collection === 'services') {
|
|
202
|
+
const specTypeMatch = !specType || msg.specType === specType;
|
|
203
|
+
return idMatch && versionMatch && collectionMatch && specTypeMatch;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return idMatch && versionMatch && collectionMatch;
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
if (matchingMessage) {
|
|
210
|
+
setSelectedMessage(matchingMessage);
|
|
211
|
+
setSelectedVersion(matchingMessage.data.version);
|
|
212
|
+
|
|
213
|
+
// Scroll to the selected item after a brief delay to ensure DOM is ready
|
|
214
|
+
setTimeout(() => {
|
|
215
|
+
if (selectedItemRef.current) {
|
|
216
|
+
selectedItemRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
217
|
+
}
|
|
218
|
+
}, 100);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}, [schemas]);
|
|
222
|
+
|
|
223
|
+
// Auto-select first message when filters change (only if no query params)
|
|
224
|
+
useEffect(() => {
|
|
225
|
+
if (typeof window === 'undefined') return;
|
|
226
|
+
|
|
227
|
+
const params = new URLSearchParams(window.location.search);
|
|
228
|
+
const hasQueryParams = params.has('id');
|
|
229
|
+
|
|
230
|
+
if (filteredMessages.length > 0 && !selectedMessage && !hasQueryParams) {
|
|
231
|
+
const firstMessage = filteredMessages[0];
|
|
232
|
+
setSelectedMessage(firstMessage);
|
|
233
|
+
setSelectedVersion(firstMessage.data.version);
|
|
234
|
+
}
|
|
235
|
+
}, [filteredMessages, selectedMessage]);
|
|
236
|
+
|
|
237
|
+
// Get the message to display (based on selected version)
|
|
238
|
+
const displayMessage = useMemo(() => {
|
|
239
|
+
if (!selectedMessage) return null;
|
|
240
|
+
|
|
241
|
+
// For services, use compound key (ID + spec type), otherwise just ID
|
|
242
|
+
const groupKey =
|
|
243
|
+
selectedMessage.collection === 'services'
|
|
244
|
+
? `${selectedMessage.data.id}__${selectedMessage.specType || 'unknown'}`
|
|
245
|
+
: selectedMessage.data.id;
|
|
246
|
+
|
|
247
|
+
const versions = messagesByIdAndVersions.get(groupKey);
|
|
248
|
+
if (!versions) return selectedMessage;
|
|
249
|
+
|
|
250
|
+
// If no version selected, use the latest (which is the first in the sorted array)
|
|
251
|
+
if (!selectedVersion) return versions[0];
|
|
252
|
+
|
|
253
|
+
// Find the message with the selected version
|
|
254
|
+
const versionedMessage = versions.find((v) => v.data.version === selectedVersion);
|
|
255
|
+
return versionedMessage || versions[0];
|
|
256
|
+
}, [selectedMessage, selectedVersion, messagesByIdAndVersions]);
|
|
257
|
+
|
|
258
|
+
// Save filter expanded state to localStorage
|
|
259
|
+
useEffect(() => {
|
|
260
|
+
if (typeof window !== 'undefined') {
|
|
261
|
+
localStorage.setItem('schemaRegistryFiltersExpanded', filtersExpanded.toString());
|
|
262
|
+
}
|
|
263
|
+
}, [filtersExpanded]);
|
|
264
|
+
|
|
265
|
+
// Save filter states to localStorage
|
|
266
|
+
useEffect(() => {
|
|
267
|
+
if (typeof window !== 'undefined') {
|
|
268
|
+
localStorage.setItem('schemaRegistrySearchQuery', searchQuery);
|
|
269
|
+
}
|
|
270
|
+
}, [searchQuery]);
|
|
271
|
+
|
|
272
|
+
useEffect(() => {
|
|
273
|
+
if (typeof window !== 'undefined') {
|
|
274
|
+
localStorage.setItem('schemaRegistrySelectedType', selectedType);
|
|
275
|
+
}
|
|
276
|
+
}, [selectedType]);
|
|
277
|
+
|
|
278
|
+
useEffect(() => {
|
|
279
|
+
if (typeof window !== 'undefined') {
|
|
280
|
+
localStorage.setItem('schemaRegistrySelectedSchemaType', selectedSchemaType);
|
|
281
|
+
}
|
|
282
|
+
}, [selectedSchemaType]);
|
|
283
|
+
|
|
284
|
+
// Keyboard shortcut for search (Cmd+K or Ctrl+K)
|
|
285
|
+
useEffect(() => {
|
|
286
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
287
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
288
|
+
e.preventDefault();
|
|
289
|
+
searchInputRef.current?.focus();
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
293
|
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
294
|
+
}, []);
|
|
295
|
+
|
|
296
|
+
// Get available versions for the selected message
|
|
297
|
+
const availableVersions = useMemo(() => {
|
|
298
|
+
if (!displayMessage) return [];
|
|
299
|
+
const groupKey =
|
|
300
|
+
displayMessage.collection === 'services'
|
|
301
|
+
? `${displayMessage.data.id}__${displayMessage.specType || 'unknown'}`
|
|
302
|
+
: displayMessage.data.id;
|
|
303
|
+
return messagesByIdAndVersions.get(groupKey) || [displayMessage];
|
|
304
|
+
}, [displayMessage, messagesByIdAndVersions]);
|
|
305
|
+
|
|
306
|
+
const handleVersionChange = (newVersion: string) => {
|
|
307
|
+
setSelectedVersion(newVersion);
|
|
308
|
+
// Update URL with new version
|
|
309
|
+
const versionedMessage = availableVersions.find((v) => v.data.version === newVersion);
|
|
310
|
+
if (versionedMessage) {
|
|
311
|
+
updateUrlParams(versionedMessage);
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
return (
|
|
316
|
+
<div className="h-full flex flex-col overflow-hidden">
|
|
317
|
+
{/* Compact Header */}
|
|
318
|
+
<div className="flex-shrink-0 border-b border-gray-200 pb-2 mb-3">
|
|
319
|
+
<div>
|
|
320
|
+
<h1 className="text-2xl font-bold text-gray-900">Schema Explorer</h1>
|
|
321
|
+
<p className="mt-0.5 text-xs text-gray-600">
|
|
322
|
+
{filteredMessages.length} schema{filteredMessages.length !== 1 ? 's' : ''} available
|
|
323
|
+
</p>
|
|
324
|
+
</div>
|
|
325
|
+
</div>
|
|
326
|
+
|
|
327
|
+
{/* Split View */}
|
|
328
|
+
<div className="flex-1 flex gap-4 overflow-hidden">
|
|
329
|
+
{/* Left: Filters + Schema List */}
|
|
330
|
+
<div className="w-1/3 flex flex-col bg-white border border-gray-200 rounded-lg overflow-hidden">
|
|
331
|
+
{/* Filters */}
|
|
332
|
+
<SchemaFilters
|
|
333
|
+
searchQuery={searchQuery}
|
|
334
|
+
onSearchChange={setSearchQuery}
|
|
335
|
+
selectedType={selectedType}
|
|
336
|
+
onTypeChange={setSelectedType}
|
|
337
|
+
selectedSchemaType={selectedSchemaType}
|
|
338
|
+
onSchemaTypeChange={setSelectedSchemaType}
|
|
339
|
+
schemaTypes={schemaTypes}
|
|
340
|
+
latestMessages={latestMessages}
|
|
341
|
+
filtersExpanded={filtersExpanded}
|
|
342
|
+
onToggleExpanded={() => setFiltersExpanded(!filtersExpanded)}
|
|
343
|
+
searchInputRef={searchInputRef}
|
|
344
|
+
isMounted={isMounted}
|
|
345
|
+
/>
|
|
346
|
+
|
|
347
|
+
{/* Schema List - Independently Scrollable */}
|
|
348
|
+
<div className="flex-1 overflow-y-auto">
|
|
349
|
+
{paginatedMessages.length > 0 ? (
|
|
350
|
+
<div className="divide-y divide-gray-200">
|
|
351
|
+
{paginatedMessages.map((message) => {
|
|
352
|
+
// For services, also check spec type to determine if selected
|
|
353
|
+
const isSelected =
|
|
354
|
+
message.collection === 'services'
|
|
355
|
+
? selectedMessage?.data.id === message.data.id && selectedMessage?.specType === message.specType
|
|
356
|
+
: selectedMessage?.data.id === message.data.id;
|
|
357
|
+
|
|
358
|
+
// Get versions using compound key for services
|
|
359
|
+
const groupKey =
|
|
360
|
+
message.collection === 'services' ? `${message.data.id}__${message.specType || 'unknown'}` : message.data.id;
|
|
361
|
+
|
|
362
|
+
const versions = messagesByIdAndVersions.get(groupKey) || [message];
|
|
363
|
+
|
|
364
|
+
return (
|
|
365
|
+
<SchemaListItem
|
|
366
|
+
key={groupKey}
|
|
367
|
+
message={message}
|
|
368
|
+
isSelected={isSelected}
|
|
369
|
+
versions={versions}
|
|
370
|
+
onClick={() => {
|
|
371
|
+
setSelectedMessage(message);
|
|
372
|
+
setSelectedVersion(message.data.version);
|
|
373
|
+
updateUrlParams(message);
|
|
374
|
+
}}
|
|
375
|
+
itemRef={isSelected ? selectedItemRef : undefined}
|
|
376
|
+
/>
|
|
377
|
+
);
|
|
378
|
+
})}
|
|
379
|
+
</div>
|
|
380
|
+
) : (
|
|
381
|
+
<div className="flex flex-col items-center justify-center h-full p-8 text-center">
|
|
382
|
+
<FunnelIcon className="h-12 w-12 text-gray-400 mb-3" />
|
|
383
|
+
<h3 className="text-sm font-semibold text-gray-900 mb-1">No schemas found</h3>
|
|
384
|
+
<p className="text-xs text-gray-500">Try adjusting your filters</p>
|
|
385
|
+
</div>
|
|
386
|
+
)}
|
|
387
|
+
</div>
|
|
388
|
+
|
|
389
|
+
{/* Pagination */}
|
|
390
|
+
<Pagination currentPage={currentPage} totalPages={totalPages} onPageChange={setCurrentPage} />
|
|
391
|
+
</div>
|
|
392
|
+
|
|
393
|
+
{/* Right: Schema Details */}
|
|
394
|
+
<div className="flex-1 bg-white border border-gray-200 rounded-lg overflow-hidden">
|
|
395
|
+
{displayMessage ? (
|
|
396
|
+
<SchemaDetailsPanel
|
|
397
|
+
message={displayMessage}
|
|
398
|
+
availableVersions={availableVersions}
|
|
399
|
+
selectedVersion={selectedVersion}
|
|
400
|
+
onVersionChange={handleVersionChange}
|
|
401
|
+
apiAccessEnabled={apiAccessEnabled}
|
|
402
|
+
/>
|
|
403
|
+
) : (
|
|
404
|
+
<div className="h-full flex items-center justify-center text-gray-500">
|
|
405
|
+
<div className="text-center">
|
|
406
|
+
<DocumentTextIcon className="h-12 w-12 mx-auto mb-3 text-gray-400" />
|
|
407
|
+
<p>Select a schema to view details</p>
|
|
408
|
+
</div>
|
|
409
|
+
</div>
|
|
410
|
+
)}
|
|
411
|
+
</div>
|
|
412
|
+
</div>
|
|
413
|
+
</div>
|
|
414
|
+
);
|
|
415
|
+
}
|