@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.
- 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-GA274FBN.js → chunk-AHJ4UE33.js} +1 -1
- package/dist/{chunk-IRFM5IS7.js → chunk-LCBQ5JUR.js} +1 -1
- package/dist/{chunk-I2FMV7LN.js → chunk-SH6FZS4K.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 +465 -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 +132 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsHeader.tsx +181 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsPanel.tsx +233 -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,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
|
+
}
|