@eventcatalog/core 2.63.0 → 2.64.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.
Files changed (47) hide show
  1. package/dist/analytics/analytics.cjs +1 -1
  2. package/dist/analytics/analytics.js +2 -2
  3. package/dist/analytics/log-build.cjs +1 -1
  4. package/dist/analytics/log-build.js +3 -3
  5. package/dist/{chunk-GA274FBN.js → chunk-AHJ4UE33.js} +1 -1
  6. package/dist/{chunk-IRFM5IS7.js → chunk-LCBQ5JUR.js} +1 -1
  7. package/dist/{chunk-I2FMV7LN.js → chunk-SH6FZS4K.js} +1 -1
  8. package/dist/constants.cjs +1 -1
  9. package/dist/constants.js +1 -1
  10. package/dist/eventcatalog.cjs +1 -1
  11. package/dist/eventcatalog.js +3 -3
  12. package/eventcatalog/astro.config.mjs +2 -1
  13. package/eventcatalog/public/icons/avro.svg +21 -0
  14. package/eventcatalog/public/icons/json-schema.svg +6 -0
  15. package/eventcatalog/public/icons/proto.svg +10 -0
  16. package/eventcatalog/src/components/Grids/utils.tsx +5 -3
  17. package/eventcatalog/src/components/MDX/RemoteFile.astro +5 -11
  18. package/eventcatalog/src/components/MDX/SchemaViewer/SchemaViewerRoot.astro +41 -6
  19. package/eventcatalog/src/components/SchemaExplorer/ApiAccessSection.tsx +139 -0
  20. package/eventcatalog/src/components/SchemaExplorer/AvroSchemaViewer.tsx +465 -0
  21. package/eventcatalog/src/components/SchemaExplorer/DiffViewer.tsx +102 -0
  22. package/eventcatalog/src/components/SchemaExplorer/JSONSchemaViewer.tsx +740 -0
  23. package/eventcatalog/src/components/SchemaExplorer/OwnersSection.tsx +56 -0
  24. package/eventcatalog/src/components/SchemaExplorer/Pagination.tsx +33 -0
  25. package/eventcatalog/src/components/SchemaExplorer/ProducersConsumersSection.tsx +91 -0
  26. package/eventcatalog/src/components/SchemaExplorer/SchemaCodeModal.tsx +93 -0
  27. package/eventcatalog/src/components/SchemaExplorer/SchemaContentViewer.tsx +132 -0
  28. package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsHeader.tsx +181 -0
  29. package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsPanel.tsx +233 -0
  30. package/eventcatalog/src/components/SchemaExplorer/SchemaExplorer.tsx +415 -0
  31. package/eventcatalog/src/components/SchemaExplorer/SchemaFilters.tsx +174 -0
  32. package/eventcatalog/src/components/SchemaExplorer/SchemaListItem.tsx +73 -0
  33. package/eventcatalog/src/components/SchemaExplorer/SchemaViewerModal.tsx +77 -0
  34. package/eventcatalog/src/components/SchemaExplorer/VersionHistoryModal.tsx +72 -0
  35. package/eventcatalog/src/components/SchemaExplorer/types.ts +45 -0
  36. package/eventcatalog/src/components/SchemaExplorer/utils.ts +81 -0
  37. package/eventcatalog/src/components/SideNav/ListViewSideBar/index.tsx +33 -2
  38. package/eventcatalog/src/components/Tables/columns/MessageTableColumns.tsx +2 -2
  39. package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +10 -0
  40. package/eventcatalog/src/pages/api/schemas/[collection]/[id]/[version]/index.ts +45 -0
  41. package/eventcatalog/src/pages/api/schemas/services/[id]/[version]/[specification]/index.ts +51 -0
  42. package/eventcatalog/src/pages/docs/llm/schemas.txt.ts +86 -0
  43. package/eventcatalog/src/pages/schemas/index.astro +175 -0
  44. package/eventcatalog/src/utils/files.ts +9 -0
  45. package/package.json +1 -1
  46. package/eventcatalog/src/components/MDX/SchemaViewer/SchemaProperty.astro +0 -204
  47. package/eventcatalog/src/components/MDX/SchemaViewer/SchemaViewer.astro +0 -705
@@ -0,0 +1,233 @@
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
+ showRequired={true}
194
+ onOpenFullscreen={
195
+ schemaViewMode === 'code'
196
+ ? () => setIsCodeModalOpen(true)
197
+ : schemaViewMode === 'schema' && (parsedSchema || parsedAvroSchema)
198
+ ? () => setIsSchemaViewerModalOpen(true)
199
+ : undefined
200
+ }
201
+ />
202
+ )}
203
+ </div>
204
+
205
+ {/* Version History Modal */}
206
+ <VersionHistoryModal
207
+ isOpen={isDiffModalOpen}
208
+ onOpenChange={setIsDiffModalOpen}
209
+ diffs={allDiffs}
210
+ messageName={message.data.name}
211
+ apiAccessEnabled={apiAccessEnabled}
212
+ />
213
+
214
+ {/* Schema Code Modal */}
215
+ <SchemaCodeModal
216
+ isOpen={isCodeModalOpen}
217
+ onOpenChange={setIsCodeModalOpen}
218
+ message={message}
219
+ onCopy={handleCopy}
220
+ isCopied={isCopied}
221
+ />
222
+
223
+ {/* Schema Viewer Modal */}
224
+ <SchemaViewerModal
225
+ isOpen={isSchemaViewerModalOpen}
226
+ onOpenChange={setIsSchemaViewerModalOpen}
227
+ message={message}
228
+ parsedSchema={parsedSchema}
229
+ parsedAvroSchema={parsedAvroSchema}
230
+ />
231
+ </div>
232
+ );
233
+ }
@@ -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
+ }