@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,465 @@
1
+ import { useState, useEffect, useRef } from 'react';
2
+
3
+ interface AvroSchemaViewerProps {
4
+ schema: any;
5
+ title?: string;
6
+ maxHeight?: string;
7
+ expand?: boolean | string;
8
+ search?: boolean | string;
9
+ showRequired?: boolean | string;
10
+ onOpenFullscreen?: () => void;
11
+ }
12
+
13
+ interface AvroFieldProps {
14
+ field: any;
15
+ level: number;
16
+ expand: boolean;
17
+ showRequired?: boolean;
18
+ }
19
+
20
+ // Format Avro type for display
21
+ function formatAvroType(type: any): string {
22
+ if (typeof type === 'string') {
23
+ return type;
24
+ }
25
+
26
+ if (Array.isArray(type)) {
27
+ // Union type - show all options
28
+ return type.join(' | ');
29
+ }
30
+
31
+ if (typeof type === 'object') {
32
+ if (type.type === 'array') {
33
+ return `array<${formatAvroType(type.items)}>`;
34
+ }
35
+ if (type.type === 'map') {
36
+ return `map<${formatAvroType(type.values)}>`;
37
+ }
38
+ if (type.type === 'record') {
39
+ return `record: ${type.name || 'unnamed'}`;
40
+ }
41
+ if (type.type === 'enum') {
42
+ return `enum: ${type.name || 'unnamed'}`;
43
+ }
44
+ if (type.logicalType) {
45
+ return `${type.type} (${type.logicalType})`;
46
+ }
47
+ return type.type || 'unknown';
48
+ }
49
+
50
+ return 'unknown';
51
+ }
52
+
53
+ // Check if a type has nested fields
54
+ function hasNestedFields(type: any): boolean {
55
+ if (typeof type === 'object' && !Array.isArray(type)) {
56
+ return type.type === 'record' && type.fields && type.fields.length > 0;
57
+ }
58
+ return false;
59
+ }
60
+
61
+ // Check if a field is required (not optional)
62
+ // A field is optional if:
63
+ // - It has a default value, OR
64
+ // - Its type is null or includes null in a union
65
+ function isFieldRequired(field: any): boolean {
66
+ // If field has a default value, it's optional
67
+ if ('default' in field) {
68
+ return false;
69
+ }
70
+
71
+ const fieldType = field.type;
72
+
73
+ // If type is null, field is optional
74
+ if (fieldType === 'null') {
75
+ return false;
76
+ }
77
+
78
+ // If type is a union (array), check if it includes null
79
+ if (Array.isArray(fieldType)) {
80
+ return !fieldType.includes('null');
81
+ }
82
+
83
+ // Otherwise, field is required
84
+ return true;
85
+ }
86
+
87
+ // AvroField component - displays a single field with nested support
88
+ const AvroField = ({ field, level, expand, showRequired }: AvroFieldProps) => {
89
+ const [isExpanded, setIsExpanded] = useState(expand);
90
+ const hasNested = hasNestedFields(field.type);
91
+ const indentClass = `pl-${level * 4}`;
92
+ const isRequired = showRequired ? isFieldRequired(field) : undefined;
93
+
94
+ useEffect(() => {
95
+ setIsExpanded(expand);
96
+ }, [expand]);
97
+
98
+ return (
99
+ <div className={`avro-field-container mb-2 border-l border-gray-200 ${indentClass}`}>
100
+ <div className="flex items-start space-x-2">
101
+ {/* Collapse/Expand button */}
102
+ {hasNested ? (
103
+ <button
104
+ onClick={() => setIsExpanded(!isExpanded)}
105
+ className="avro-field-toggle text-gray-500 hover:text-gray-700 pt-0.5 w-4 text-center flex-shrink-0"
106
+ aria-expanded={isExpanded}
107
+ >
108
+ <span className="font-mono text-xs">{isExpanded ? '▼' : '▶'}</span>
109
+ </button>
110
+ ) : (
111
+ <div className="w-4" />
112
+ )}
113
+
114
+ {/* Field details */}
115
+ <div className="flex-grow">
116
+ <div className="flex justify-between items-baseline">
117
+ <div>
118
+ <span className="avro-field-name font-semibold text-gray-800 text-sm">{field.name}</span>
119
+ <span className="ml-1.5 text-purple-600 font-mono text-xs">{formatAvroType(field.type)}</span>
120
+ </div>
121
+ {showRequired && isRequired && <span className="text-red-600 text-xs ml-3 flex-shrink-0">required</span>}
122
+ </div>
123
+
124
+ {field.doc && <p className="text-gray-600 text-xs mt-1">{field.doc}</p>}
125
+
126
+ {/* Show enum values if present */}
127
+ {field.type?.type === 'enum' && field.type.symbols && (
128
+ <div className="text-xs text-gray-500 mt-1">
129
+ Values:{' '}
130
+ {field.type.symbols.map((s: string) => (
131
+ <code key={s} className="bg-gray-100 px-1 rounded mx-0.5">
132
+ {s}
133
+ </code>
134
+ ))}
135
+ </div>
136
+ )}
137
+
138
+ {/* Nested fields for record types */}
139
+ {hasNested && (
140
+ <div className={`avro-nested-content mt-2 ${!isExpanded ? 'hidden' : ''}`}>
141
+ {field.type.fields.map((nestedField: any) => (
142
+ <AvroField
143
+ key={nestedField.name}
144
+ field={nestedField}
145
+ level={level + 1}
146
+ expand={expand}
147
+ showRequired={showRequired}
148
+ />
149
+ ))}
150
+ </div>
151
+ )}
152
+ </div>
153
+ </div>
154
+ </div>
155
+ );
156
+ };
157
+
158
+ // Main AvroSchemaViewer component
159
+ export default function AvroSchemaViewer({
160
+ schema,
161
+ title,
162
+ maxHeight,
163
+ expand = false,
164
+ search = true,
165
+ showRequired = false,
166
+ onOpenFullscreen,
167
+ }: AvroSchemaViewerProps) {
168
+ const expandBool = expand === true || expand === 'true';
169
+ const searchBool = search !== false && search !== 'false';
170
+ const showRequiredBool = showRequired === true || showRequired === 'true';
171
+
172
+ const [searchQuery, setSearchQuery] = useState('');
173
+ const [expandAll, setExpandAll] = useState(expandBool);
174
+ const [currentMatches, setCurrentMatches] = useState<HTMLElement[]>([]);
175
+ const [currentMatchIndex, setCurrentMatchIndex] = useState(-1);
176
+ const searchInputRef = useRef<HTMLInputElement>(null);
177
+ const fieldsContainerRef = useRef<HTMLDivElement>(null);
178
+
179
+ const totalFields = schema?.fields?.length || 0;
180
+
181
+ // Search functionality with highlighting
182
+ useEffect(() => {
183
+ if (!fieldsContainerRef.current) return;
184
+
185
+ const fieldContainers = fieldsContainerRef.current.querySelectorAll('.avro-field-container');
186
+ const matches: HTMLElement[] = [];
187
+
188
+ if (searchQuery === '') {
189
+ // Reset search
190
+ fieldContainers.forEach((container) => {
191
+ container.classList.remove('search-match', 'search-no-match', 'search-current-match', 'search-dimmed');
192
+ const nameEl = container.querySelector('.avro-field-name');
193
+ if (nameEl) {
194
+ nameEl.innerHTML = nameEl.textContent || '';
195
+ }
196
+ });
197
+ setCurrentMatches([]);
198
+ setCurrentMatchIndex(-1);
199
+ return;
200
+ }
201
+
202
+ const query = searchQuery.toLowerCase().trim();
203
+
204
+ fieldContainers.forEach((container) => {
205
+ const nameEl = container.querySelector('.avro-field-name');
206
+ if (!nameEl) return;
207
+
208
+ const fieldName = (nameEl.textContent || '').toLowerCase();
209
+
210
+ if (fieldName.includes(query)) {
211
+ container.classList.add('search-match');
212
+ container.classList.remove('search-dimmed');
213
+ matches.push(container as HTMLElement);
214
+
215
+ // Highlight the search term
216
+ const regex = new RegExp(`(${query})`, 'gi');
217
+ nameEl.innerHTML = (nameEl.textContent || '').replace(regex, '<mark class="bg-yellow-200 rounded px-0.5">$1</mark>');
218
+
219
+ // Expand parent containers
220
+ let parent = container.parentElement;
221
+ while (parent && parent !== fieldsContainerRef.current) {
222
+ if (parent.classList.contains('avro-nested-content') && parent.classList.contains('hidden')) {
223
+ const parentFieldContainer = parent.closest('.avro-field-container');
224
+ if (parentFieldContainer) {
225
+ const toggleBtn = parentFieldContainer.querySelector('.avro-field-toggle');
226
+ if (toggleBtn && toggleBtn.getAttribute('aria-expanded') === 'false') {
227
+ (toggleBtn as HTMLButtonElement).click();
228
+ }
229
+ }
230
+ }
231
+ // Remove dimming from parent containers
232
+ if (parent.classList.contains('avro-field-container')) {
233
+ parent.classList.remove('search-dimmed');
234
+ }
235
+ parent = parent.parentElement;
236
+ }
237
+ } else {
238
+ container.classList.remove('search-match', 'search-current-match');
239
+ container.classList.add('search-dimmed');
240
+ nameEl.innerHTML = nameEl.textContent || '';
241
+ }
242
+ });
243
+
244
+ setCurrentMatches(matches);
245
+ if (matches.length > 0) {
246
+ setCurrentMatchIndex(0);
247
+ matches[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
248
+ } else {
249
+ setCurrentMatchIndex(-1);
250
+ }
251
+ }, [searchQuery]);
252
+
253
+ // Update match highlighting
254
+ useEffect(() => {
255
+ currentMatches.forEach((match, index) => {
256
+ if (index === currentMatchIndex) {
257
+ match.classList.add('search-current-match');
258
+ } else {
259
+ match.classList.remove('search-current-match');
260
+ }
261
+ });
262
+
263
+ if (currentMatchIndex >= 0 && currentMatches[currentMatchIndex]) {
264
+ currentMatches[currentMatchIndex].scrollIntoView({ behavior: 'smooth', block: 'center' });
265
+ }
266
+ }, [currentMatchIndex, currentMatches]);
267
+
268
+ const handleExpandAll = () => {
269
+ setExpandAll(true);
270
+ };
271
+
272
+ const handleCollapseAll = () => {
273
+ setExpandAll(false);
274
+ };
275
+
276
+ const handlePrevMatch = () => {
277
+ if (currentMatchIndex > 0) {
278
+ setCurrentMatchIndex(currentMatchIndex - 1);
279
+ }
280
+ };
281
+
282
+ const handleNextMatch = () => {
283
+ if (currentMatchIndex < currentMatches.length - 1) {
284
+ setCurrentMatchIndex(currentMatchIndex + 1);
285
+ }
286
+ };
287
+
288
+ if (!schema || schema.type !== 'record') {
289
+ return (
290
+ <div className="flex items-center justify-center p-8 text-gray-500">
291
+ <p className="text-sm">Invalid Avro schema format</p>
292
+ </div>
293
+ );
294
+ }
295
+
296
+ const containerStyle = maxHeight
297
+ ? { maxHeight: maxHeight.includes('px') ? maxHeight : `${maxHeight}px`, minHeight: '15em' }
298
+ : {};
299
+
300
+ const heightClass = maxHeight ? '' : 'h-full';
301
+ const overflowClass = maxHeight ? 'overflow-hidden' : '';
302
+
303
+ return (
304
+ <div
305
+ className={`${heightClass} ${overflowClass} flex flex-col bg-white border border-gray-100 rounded-md shadow-sm`}
306
+ style={containerStyle}
307
+ >
308
+ {/* Toolbar */}
309
+ {searchBool && (
310
+ <div className="flex-shrink-0 bg-white pt-4 px-4 pb-3 border-b border-gray-100 shadow-sm">
311
+ <div className="flex flex-col sm:flex-row gap-3">
312
+ <div className="flex-1 relative">
313
+ <input
314
+ ref={searchInputRef}
315
+ type="text"
316
+ value={searchQuery}
317
+ onChange={(e) => setSearchQuery(e.target.value)}
318
+ placeholder="Search fields..."
319
+ className="w-full px-3 py-1.5 pr-20 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
320
+ onKeyDown={(e) => {
321
+ if (e.key === 'Enter') {
322
+ e.preventDefault();
323
+ if (e.shiftKey) {
324
+ handlePrevMatch();
325
+ } else {
326
+ handleNextMatch();
327
+ }
328
+ }
329
+ }}
330
+ />
331
+ <div className="absolute right-2 top-1/2 transform -translate-y-1/2 flex items-center gap-1">
332
+ <button
333
+ onClick={handlePrevMatch}
334
+ disabled={currentMatches.length === 0 || currentMatchIndex <= 0}
335
+ className="p-1 text-gray-400 hover:text-gray-600 disabled:opacity-30 disabled:cursor-not-allowed"
336
+ title="Previous match"
337
+ >
338
+ <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
339
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 19l-7-7 7-7" />
340
+ </svg>
341
+ </button>
342
+ <button
343
+ onClick={handleNextMatch}
344
+ disabled={currentMatches.length === 0 || currentMatchIndex >= currentMatches.length - 1}
345
+ className="p-1 text-gray-400 hover:text-gray-600 disabled:opacity-30 disabled:cursor-not-allowed"
346
+ title="Next match"
347
+ >
348
+ <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
349
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5l7 7-7 7" />
350
+ </svg>
351
+ </button>
352
+ </div>
353
+ </div>
354
+ <div className="flex items-center gap-2">
355
+ {onOpenFullscreen && (
356
+ <button
357
+ onClick={onOpenFullscreen}
358
+ className="px-3 py-1.5 text-xs font-medium text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-500"
359
+ title="Open in fullscreen"
360
+ >
361
+ <svg
362
+ xmlns="http://www.w3.org/2000/svg"
363
+ className="h-3.5 w-3.5 inline-block"
364
+ fill="none"
365
+ viewBox="0 0 24 24"
366
+ stroke="currentColor"
367
+ >
368
+ <path
369
+ strokeLinecap="round"
370
+ strokeLinejoin="round"
371
+ strokeWidth={2}
372
+ d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"
373
+ />
374
+ </svg>
375
+ </button>
376
+ )}
377
+ <button
378
+ onClick={handleExpandAll}
379
+ className="px-3 py-1.5 text-xs font-medium text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-500"
380
+ >
381
+ Expand All
382
+ </button>
383
+ <button
384
+ onClick={handleCollapseAll}
385
+ className="px-3 py-1.5 text-xs font-medium text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-500"
386
+ >
387
+ Collapse All
388
+ </button>
389
+ <div className="text-xs text-gray-500">
390
+ {totalFields} {totalFields === 1 ? 'field' : 'fields'}
391
+ </div>
392
+ </div>
393
+ </div>
394
+ {searchQuery && (
395
+ <div className="mt-2 text-xs text-gray-600">
396
+ {currentMatches.length > 0
397
+ ? `${currentMatchIndex + 1} of ${currentMatches.length} ${currentMatches.length === 1 ? 'match' : 'matches'}`
398
+ : 'No fields found'}
399
+ </div>
400
+ )}
401
+ </div>
402
+ )}
403
+
404
+ {/* Schema info */}
405
+ <div className="px-4 pt-4">
406
+ <div className="flex items-baseline gap-2 mb-2">
407
+ <span className="text-sm font-medium text-gray-600">Record:</span>
408
+ <span className="font-mono text-sm text-blue-600">{schema.name}</span>
409
+ {schema.namespace && <span className="font-mono text-xs text-gray-500">({schema.namespace})</span>}
410
+ </div>
411
+ {schema.doc && <p className="text-gray-600 text-xs mb-4">{schema.doc}</p>}
412
+ </div>
413
+
414
+ {/* Fields */}
415
+ <div ref={fieldsContainerRef} className="flex-1 px-4 pb-4 overflow-auto">
416
+ {schema.fields && schema.fields.length > 0 ? (
417
+ schema.fields.map((field: any) => (
418
+ <AvroField key={field.name} field={field} level={0} expand={expandAll} showRequired={showRequiredBool} />
419
+ ))
420
+ ) : (
421
+ <div className="text-center py-8 text-gray-400">
422
+ <p className="text-sm">No fields defined</p>
423
+ </div>
424
+ )}
425
+
426
+ {searchQuery && currentMatches.length === 0 && (
427
+ <div className="text-center py-8">
428
+ <div className="text-gray-400 text-sm">
429
+ <svg className="mx-auto h-12 w-12 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
430
+ <path
431
+ strokeLinecap="round"
432
+ strokeLinejoin="round"
433
+ strokeWidth="2"
434
+ d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
435
+ />
436
+ </svg>
437
+ <p>No fields match your search</p>
438
+ <p className="text-xs mt-1">Try a different search term or clear the search to see all fields</p>
439
+ </div>
440
+ </div>
441
+ )}
442
+ </div>
443
+
444
+ <style>{`
445
+ .search-dimmed {
446
+ opacity: 0.4;
447
+ transition: opacity 0.2s ease;
448
+ }
449
+
450
+ .search-match {
451
+ opacity: 1;
452
+ transition: opacity 0.2s ease;
453
+ }
454
+
455
+ .search-current-match {
456
+ background-color: rgba(59, 130, 246, 0.1);
457
+ border-left: 3px solid #3b82f6;
458
+ padding-left: 8px;
459
+ margin-left: -11px;
460
+ transition: all 0.2s ease;
461
+ }
462
+ `}</style>
463
+ </div>
464
+ );
465
+ }
@@ -0,0 +1,102 @@
1
+ import { ArrowsPointingOutIcon } from '@heroicons/react/24/outline';
2
+ import type { VersionDiff } from './types';
3
+
4
+ interface DiffViewerProps {
5
+ diffs: VersionDiff[];
6
+ onOpenFullscreen?: () => void;
7
+ apiAccessEnabled?: boolean;
8
+ }
9
+
10
+ export default function DiffViewer({ diffs, onOpenFullscreen, apiAccessEnabled = false }: DiffViewerProps) {
11
+ if (diffs.length === 0) return null;
12
+
13
+ return (
14
+ <div className="h-full overflow-auto p-4">
15
+ <div className="mb-4 flex items-start justify-between">
16
+ <div className="flex-1">
17
+ <h3 className="text-lg font-semibold text-gray-900 mb-2">Version History</h3>
18
+ <p className="text-sm text-gray-600">
19
+ {apiAccessEnabled
20
+ ? `Showing ${diffs.length} version comparison${diffs.length !== 1 ? 's' : ''}`
21
+ : 'Compare schema versions side-by-side'}
22
+ </p>
23
+ </div>
24
+ {onOpenFullscreen && apiAccessEnabled && (
25
+ <button
26
+ onClick={onOpenFullscreen}
27
+ className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 transition-colors"
28
+ title="Open in fullscreen"
29
+ >
30
+ <ArrowsPointingOutIcon className="h-4 w-4" />
31
+ Fullscreen
32
+ </button>
33
+ )}
34
+ </div>
35
+ {apiAccessEnabled ? (
36
+ <div className="space-y-8">
37
+ {diffs.map((diff, index) => (
38
+ <div key={`${diff.newerVersion}-${diff.olderVersion}`} className="border border-gray-200 rounded-lg overflow-hidden">
39
+ <div className="bg-gray-50 border-b border-gray-200 px-4 py-3">
40
+ <div className="flex items-center justify-between">
41
+ <div className="text-sm">
42
+ <span className="font-semibold text-gray-900">v{diff.newerVersion}</span>
43
+ <span className="text-gray-500 mx-2">→</span>
44
+ <span className="font-semibold text-gray-900">v{diff.olderVersion}</span>
45
+ </div>
46
+ <span className="text-xs text-gray-500">
47
+ {index === 0 ? 'Latest change' : `${index + 1} version${index + 1 !== 1 ? 's' : ''} ago`}
48
+ </span>
49
+ </div>
50
+ </div>
51
+ <div className="bg-white relative">
52
+ <div dangerouslySetInnerHTML={{ __html: diff.diffHtml }} />
53
+ </div>
54
+ </div>
55
+ ))}
56
+ </div>
57
+ ) : (
58
+ <div className="bg-white border border-purple-200 rounded-lg p-8">
59
+ <div className="flex flex-col items-center text-center max-w-md mx-auto">
60
+ <div className="flex-shrink-0 mb-4">
61
+ <svg
62
+ xmlns="http://www.w3.org/2000/svg"
63
+ className="h-16 w-16 text-purple-600"
64
+ fill="none"
65
+ viewBox="0 0 24 24"
66
+ stroke="currentColor"
67
+ >
68
+ <path
69
+ strokeLinecap="round"
70
+ strokeLinejoin="round"
71
+ strokeWidth={2}
72
+ d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
73
+ />
74
+ </svg>
75
+ </div>
76
+ <h4 className="text-xl font-semibold text-gray-900 mb-2">Upgrade to Scale</h4>
77
+ <p className="text-sm text-gray-600 mb-6">
78
+ Compare schema versions side-by-side with visual diffs. Track breaking changes, see exactly what changed between
79
+ versions, and maintain better schema governance.
80
+ </p>
81
+ <a
82
+ href="https://eventcatalog.cloud"
83
+ target="_blank"
84
+ rel="noopener noreferrer"
85
+ className="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-md hover:bg-purple-700 transition-colors"
86
+ >
87
+ Start 14-day free trial
88
+ <svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
89
+ <path
90
+ strokeLinecap="round"
91
+ strokeLinejoin="round"
92
+ strokeWidth={2}
93
+ d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
94
+ />
95
+ </svg>
96
+ </a>
97
+ </div>
98
+ </div>
99
+ )}
100
+ </div>
101
+ );
102
+ }