@eventcatalog/core 3.46.1 → 3.47.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-TZ72NP2W.js → chunk-ALXVETEP.js} +1 -1
- package/dist/{chunk-E4QGTJXO.js → chunk-GBC637Z2.js} +1 -1
- package/dist/{chunk-A7XUEEUA.js → chunk-XYCPSZ4V.js} +1 -1
- package/dist/{chunk-62QAJWPT.js → chunk-ZM5P2252.js} +1 -1
- package/dist/{chunk-VZ6L3G2S.js → chunk-ZRLFPUCO.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 +5 -5
- package/dist/generate.cjs +1 -1
- package/dist/generate.js +3 -3
- package/dist/utils/cli-logger.cjs +1 -1
- package/dist/utils/cli-logger.js +2 -2
- package/eventcatalog/src/components/MDX/SchemaViewer/SchemaViewerRoot.astro +11 -1
- package/eventcatalog/src/components/MDX/SchemaViewer/schema-viewer-utils.spec.ts +63 -0
- package/eventcatalog/src/components/MDX/SchemaViewer/schema-viewer-utils.ts +20 -7
- package/eventcatalog/src/components/SchemaExplorer/ProtobufSchemaViewer.tsx +532 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaContentViewer.tsx +6 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsPanel.tsx +20 -2
- package/eventcatalog/src/components/SchemaExplorer/SchemaExplorer.tsx +14 -5
- package/eventcatalog/src/components/SchemaExplorer/SchemaViewerModal.tsx +8 -2
- package/eventcatalog/src/components/SchemaExplorer/utils.ts +4 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/language/_index.data.ts +19 -4
- package/eventcatalog/src/pages/docs/[type]/[id]/language.mdx.ts +16 -5
- package/eventcatalog/src/stores/sidebar-store/builders/domain.ts +4 -2
- package/eventcatalog/src/stores/sidebar-store/builders/entity.ts +87 -0
- package/eventcatalog/src/stores/sidebar-store/builders/shared.ts +1 -0
- package/eventcatalog/src/stores/sidebar-store/state.ts +15 -14
- package/eventcatalog/src/utils/collections/domains.ts +39 -0
- package/eventcatalog/src/utils/collections/entities.ts +3 -1
- package/eventcatalog/src/utils/files.ts +9 -0
- package/eventcatalog/src/utils/node-graphs/domain-entity-map.ts +24 -12
- package/eventcatalog/src/utils/protobuf-schema.ts +476 -0
- package/package.json +3 -3
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
import { useState, useEffect, useRef, useMemo } from 'react';
|
|
2
|
+
import type { ProtobufSchema, ProtobufMessage, ProtobufEnum, ProtobufField } from '@utils/protobuf-schema';
|
|
3
|
+
|
|
4
|
+
interface ProtobufSchemaViewerProps {
|
|
5
|
+
schema: ProtobufSchema;
|
|
6
|
+
title?: string;
|
|
7
|
+
maxHeight?: string;
|
|
8
|
+
expand?: boolean | string;
|
|
9
|
+
search?: boolean | string;
|
|
10
|
+
showRequired?: boolean | string;
|
|
11
|
+
onOpenFullscreen?: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Proto types are referenced by name (not nested inline like Avro), so we build
|
|
15
|
+
// a registry of every message/enum in the file to resolve field types against.
|
|
16
|
+
interface TypeRegistry {
|
|
17
|
+
messages: Map<string, ProtobufMessage>;
|
|
18
|
+
enums: Map<string, ProtobufEnum>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function buildTypeRegistry(schema: ProtobufSchema): TypeRegistry {
|
|
22
|
+
const registry: TypeRegistry = { messages: new Map(), enums: new Map() };
|
|
23
|
+
|
|
24
|
+
const register = <T,>(map: Map<string, T>, qualifiedName: string, simpleName: string, value: T) => {
|
|
25
|
+
map.set(qualifiedName, value);
|
|
26
|
+
if (!map.has(simpleName)) map.set(simpleName, value);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const walk = (messages: ProtobufMessage[], enums: ProtobufEnum[], prefix: string) => {
|
|
30
|
+
for (const protoEnum of enums) {
|
|
31
|
+
register(registry.enums, prefix ? `${prefix}.${protoEnum.name}` : protoEnum.name, protoEnum.name, protoEnum);
|
|
32
|
+
}
|
|
33
|
+
for (const message of messages) {
|
|
34
|
+
const qualifiedName = prefix ? `${prefix}.${message.name}` : message.name;
|
|
35
|
+
register(registry.messages, qualifiedName, message.name, message);
|
|
36
|
+
walk(message.messages, message.enums, qualifiedName);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
walk(schema.messages, schema.enums, '');
|
|
41
|
+
return registry;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Resolve a field type name against the registry, trying the name as written,
|
|
45
|
+
// without the package prefix, and by its last segment (e.g. "com.example.Order" -> "Order").
|
|
46
|
+
// Leading dots (fully-qualified references like ".com.example.Order") are stripped first.
|
|
47
|
+
function resolveTypeName(typeName: string, packageName: string | undefined): string[] {
|
|
48
|
+
const normalized = typeName.startsWith('.') ? typeName.slice(1) : typeName;
|
|
49
|
+
const candidates = [normalized];
|
|
50
|
+
if (packageName && normalized.startsWith(`${packageName}.`)) {
|
|
51
|
+
candidates.push(normalized.slice(packageName.length + 1));
|
|
52
|
+
}
|
|
53
|
+
const lastSegment = normalized.split('.').pop();
|
|
54
|
+
if (lastSegment && lastSegment !== normalized) candidates.push(lastSegment);
|
|
55
|
+
return candidates;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function lookupType<T>(map: Map<string, T>, typeName: string, packageName?: string): T | undefined {
|
|
59
|
+
for (const candidate of resolveTypeName(typeName, packageName)) {
|
|
60
|
+
const match = map.get(candidate);
|
|
61
|
+
if (match) return match;
|
|
62
|
+
}
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function formatProtobufType(field: ProtobufField): string {
|
|
67
|
+
if (field.map) return field.type;
|
|
68
|
+
return field.label ? `${field.label} ${field.type}` : field.type;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function countFields(messages: ProtobufMessage[]): number {
|
|
72
|
+
return messages.reduce((total, message) => total + message.fields.length + countFields(message.messages), 0);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface ProtobufFieldRowProps {
|
|
76
|
+
field: ProtobufField;
|
|
77
|
+
level: number;
|
|
78
|
+
expand: boolean;
|
|
79
|
+
showRequired?: boolean;
|
|
80
|
+
registry: TypeRegistry;
|
|
81
|
+
packageName?: string;
|
|
82
|
+
ancestors: string[];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const ProtobufFieldRow = ({ field, level, expand, showRequired, registry, packageName, ancestors }: ProtobufFieldRowProps) => {
|
|
86
|
+
const [isExpanded, setIsExpanded] = useState(expand);
|
|
87
|
+
const indentationClass = `pl-${level * 3}`;
|
|
88
|
+
|
|
89
|
+
// The type to drill into: map values and plain types can both reference messages
|
|
90
|
+
const targetTypeName = field.map ? field.map.valueType : field.type;
|
|
91
|
+
const nestedMessage = lookupType(registry.messages, targetTypeName, packageName);
|
|
92
|
+
const nestedEnum = nestedMessage ? undefined : lookupType(registry.enums, targetTypeName, packageName);
|
|
93
|
+
const isCyclic = nestedMessage ? ancestors.includes(nestedMessage.name) : false;
|
|
94
|
+
const hasNested = !!nestedMessage && nestedMessage.fields.length > 0 && !isCyclic;
|
|
95
|
+
const isRequired = field.label === 'required';
|
|
96
|
+
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
setIsExpanded(expand);
|
|
99
|
+
}, [expand]);
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div className={`proto-field-container mb-1.5 border-l border-[rgb(var(--ec-page-border))] relative ${indentationClass}`}>
|
|
103
|
+
<div className="flex items-start space-x-1.5">
|
|
104
|
+
{hasNested ? (
|
|
105
|
+
<button
|
|
106
|
+
type="button"
|
|
107
|
+
aria-expanded={isExpanded}
|
|
108
|
+
onClick={() => setIsExpanded(!isExpanded)}
|
|
109
|
+
className="proto-field-toggle text-[rgb(var(--ec-page-text-muted))] hover:text-[rgb(var(--ec-page-text))] pt-0.5 focus:outline-hidden w-3 text-center flex-shrink-0"
|
|
110
|
+
>
|
|
111
|
+
<span className={`icon-collapsed font-mono text-xs ${isExpanded ? 'hidden' : ''}`}>></span>
|
|
112
|
+
<span className={`icon-expanded font-mono text-xs ${!isExpanded ? 'hidden' : ''}`}>v</span>
|
|
113
|
+
</button>
|
|
114
|
+
) : (
|
|
115
|
+
<div className="w-3 h-4 flex-shrink-0" />
|
|
116
|
+
)}
|
|
117
|
+
|
|
118
|
+
<div className="flex-grow">
|
|
119
|
+
<div className="flex justify-between items-baseline">
|
|
120
|
+
<div>
|
|
121
|
+
<span className="proto-field-name font-semibold text-[rgb(var(--ec-page-text))] text-sm">{field.name}</span>
|
|
122
|
+
<span className="ml-1.5 text-[rgb(var(--ec-accent))] font-mono text-xs">{formatProtobufType(field)}</span>
|
|
123
|
+
{field.number !== undefined && (
|
|
124
|
+
<span className="ml-1.5 text-[rgb(var(--ec-page-text-muted))] font-mono text-xs">= {field.number}</span>
|
|
125
|
+
)}
|
|
126
|
+
{field.oneof && (
|
|
127
|
+
<span className="ml-1.5 text-[rgb(var(--ec-page-text-muted))] text-xs bg-[rgb(var(--ec-content-hover))] px-1 rounded">
|
|
128
|
+
oneof {field.oneof}
|
|
129
|
+
</span>
|
|
130
|
+
)}
|
|
131
|
+
</div>
|
|
132
|
+
{showRequired && isRequired && (
|
|
133
|
+
<span className="text-red-600 dark:text-red-400 text-xs ml-3 flex-shrink-0">required</span>
|
|
134
|
+
)}
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
{field.doc && <p className="text-[rgb(var(--ec-page-text-muted))] text-xs mt-0.5">{field.doc}</p>}
|
|
138
|
+
|
|
139
|
+
{/* Show enum values if the field type resolves to an enum */}
|
|
140
|
+
{nestedEnum && nestedEnum.values.length > 0 && (
|
|
141
|
+
<div className="text-xs text-[rgb(var(--ec-page-text-muted))] mt-0.5">
|
|
142
|
+
<span className="text-xs inline-block">Allowed values:</span>
|
|
143
|
+
{nestedEnum.values.map((value) => (
|
|
144
|
+
<span key={value.name} className="text-xs">
|
|
145
|
+
{' '}
|
|
146
|
+
<code className="bg-[rgb(var(--ec-content-hover))] px-1 rounded text-[rgb(var(--ec-page-text))] font-thin py-0.5">
|
|
147
|
+
{value.name}
|
|
148
|
+
</code>
|
|
149
|
+
</span>
|
|
150
|
+
))}
|
|
151
|
+
</div>
|
|
152
|
+
)}
|
|
153
|
+
|
|
154
|
+
{/* Nested fields for message types */}
|
|
155
|
+
{hasNested && nestedMessage && (
|
|
156
|
+
<div className={`proto-nested-content mt-1 ${!isExpanded ? 'hidden' : ''}`}>
|
|
157
|
+
{nestedMessage.fields.map((nestedField) => (
|
|
158
|
+
<ProtobufFieldRow
|
|
159
|
+
key={nestedField.name}
|
|
160
|
+
field={nestedField}
|
|
161
|
+
level={level + 1}
|
|
162
|
+
expand={expand}
|
|
163
|
+
showRequired={showRequired}
|
|
164
|
+
registry={registry}
|
|
165
|
+
packageName={packageName}
|
|
166
|
+
ancestors={[...ancestors, nestedMessage.name]}
|
|
167
|
+
/>
|
|
168
|
+
))}
|
|
169
|
+
</div>
|
|
170
|
+
)}
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// Main ProtobufSchemaViewer component
|
|
178
|
+
export default function ProtobufSchemaViewer({
|
|
179
|
+
schema,
|
|
180
|
+
title,
|
|
181
|
+
maxHeight,
|
|
182
|
+
expand = false,
|
|
183
|
+
search = true,
|
|
184
|
+
showRequired = false,
|
|
185
|
+
onOpenFullscreen,
|
|
186
|
+
}: ProtobufSchemaViewerProps) {
|
|
187
|
+
const expandBool = expand === true || expand === 'true';
|
|
188
|
+
const searchBool = search !== false && search !== 'false';
|
|
189
|
+
const showRequiredBool = showRequired === true || showRequired === 'true';
|
|
190
|
+
|
|
191
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
192
|
+
const [expandAll, setExpandAll] = useState(expandBool);
|
|
193
|
+
const [currentMatches, setCurrentMatches] = useState<HTMLElement[]>([]);
|
|
194
|
+
const [currentMatchIndex, setCurrentMatchIndex] = useState(-1);
|
|
195
|
+
const searchInputRef = useRef<HTMLInputElement>(null);
|
|
196
|
+
const fieldsContainerRef = useRef<HTMLDivElement>(null);
|
|
197
|
+
|
|
198
|
+
const registry = useMemo(() => (schema ? buildTypeRegistry(schema) : { messages: new Map(), enums: new Map() }), [schema]);
|
|
199
|
+
const totalFields = useMemo(() => (schema ? countFields(schema.messages) : 0), [schema]);
|
|
200
|
+
|
|
201
|
+
// Search functionality with highlighting
|
|
202
|
+
useEffect(() => {
|
|
203
|
+
if (!fieldsContainerRef.current) return;
|
|
204
|
+
|
|
205
|
+
const fieldContainers = fieldsContainerRef.current.querySelectorAll('.proto-field-container');
|
|
206
|
+
const matches: HTMLElement[] = [];
|
|
207
|
+
|
|
208
|
+
if (searchQuery === '') {
|
|
209
|
+
// Reset search
|
|
210
|
+
fieldContainers.forEach((container) => {
|
|
211
|
+
container.classList.remove('search-match', 'search-no-match', 'search-current-match', 'search-dimmed');
|
|
212
|
+
const nameEl = container.querySelector('.proto-field-name');
|
|
213
|
+
if (nameEl) {
|
|
214
|
+
nameEl.innerHTML = nameEl.textContent || '';
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
setCurrentMatches([]);
|
|
218
|
+
setCurrentMatchIndex(-1);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const query = searchQuery.toLowerCase().trim();
|
|
223
|
+
|
|
224
|
+
fieldContainers.forEach((container) => {
|
|
225
|
+
const nameEl = container.querySelector('.proto-field-name');
|
|
226
|
+
if (!nameEl) return;
|
|
227
|
+
|
|
228
|
+
const fieldName = (nameEl.textContent || '').toLowerCase();
|
|
229
|
+
|
|
230
|
+
if (fieldName.includes(query)) {
|
|
231
|
+
container.classList.add('search-match');
|
|
232
|
+
container.classList.remove('search-dimmed');
|
|
233
|
+
matches.push(container as HTMLElement);
|
|
234
|
+
|
|
235
|
+
// Highlight the search term
|
|
236
|
+
const regex = new RegExp(`(${query})`, 'gi');
|
|
237
|
+
nameEl.innerHTML = (nameEl.textContent || '').replace(regex, '<mark class="bg-yellow-200 rounded px-0.5">$1</mark>');
|
|
238
|
+
|
|
239
|
+
// Expand parent containers
|
|
240
|
+
let parent = container.parentElement;
|
|
241
|
+
while (parent && parent !== fieldsContainerRef.current) {
|
|
242
|
+
if (parent.classList.contains('proto-nested-content') && parent.classList.contains('hidden')) {
|
|
243
|
+
const parentFieldContainer = parent.closest('.proto-field-container');
|
|
244
|
+
if (parentFieldContainer) {
|
|
245
|
+
const toggleBtn = parentFieldContainer.querySelector('.proto-field-toggle');
|
|
246
|
+
if (toggleBtn && toggleBtn.getAttribute('aria-expanded') === 'false') {
|
|
247
|
+
(toggleBtn as HTMLButtonElement).click();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// Remove dimming from parent containers
|
|
252
|
+
if (parent.classList.contains('proto-field-container')) {
|
|
253
|
+
parent.classList.remove('search-dimmed');
|
|
254
|
+
}
|
|
255
|
+
parent = parent.parentElement;
|
|
256
|
+
}
|
|
257
|
+
} else {
|
|
258
|
+
container.classList.remove('search-match', 'search-current-match');
|
|
259
|
+
container.classList.add('search-dimmed');
|
|
260
|
+
nameEl.innerHTML = nameEl.textContent || '';
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
setCurrentMatches(matches);
|
|
265
|
+
if (matches.length > 0) {
|
|
266
|
+
setCurrentMatchIndex(0);
|
|
267
|
+
matches[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
268
|
+
} else {
|
|
269
|
+
setCurrentMatchIndex(-1);
|
|
270
|
+
}
|
|
271
|
+
}, [searchQuery]);
|
|
272
|
+
|
|
273
|
+
// Update match highlighting
|
|
274
|
+
useEffect(() => {
|
|
275
|
+
currentMatches.forEach((match, index) => {
|
|
276
|
+
if (index === currentMatchIndex) {
|
|
277
|
+
match.classList.add('search-current-match');
|
|
278
|
+
} else {
|
|
279
|
+
match.classList.remove('search-current-match');
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
if (currentMatchIndex >= 0 && currentMatches[currentMatchIndex]) {
|
|
284
|
+
currentMatches[currentMatchIndex].scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
285
|
+
}
|
|
286
|
+
}, [currentMatchIndex, currentMatches]);
|
|
287
|
+
|
|
288
|
+
const handleExpandAll = () => {
|
|
289
|
+
setExpandAll(true);
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const handleCollapseAll = () => {
|
|
293
|
+
setExpandAll(false);
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const handlePrevMatch = () => {
|
|
297
|
+
if (currentMatchIndex > 0) {
|
|
298
|
+
setCurrentMatchIndex(currentMatchIndex - 1);
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const handleNextMatch = () => {
|
|
303
|
+
if (currentMatchIndex < currentMatches.length - 1) {
|
|
304
|
+
setCurrentMatchIndex(currentMatchIndex + 1);
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
if (!schema || !Array.isArray(schema.messages)) {
|
|
309
|
+
return (
|
|
310
|
+
<div className="flex items-center justify-center p-8 text-[rgb(var(--ec-page-text-muted))]">
|
|
311
|
+
<p className="text-sm">Invalid Protobuf schema format</p>
|
|
312
|
+
</div>
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const containerStyle = maxHeight
|
|
317
|
+
? { maxHeight: maxHeight.includes('px') ? maxHeight : `${maxHeight}px`, minHeight: '15em' }
|
|
318
|
+
: {};
|
|
319
|
+
|
|
320
|
+
const heightClass = maxHeight ? '' : 'h-full';
|
|
321
|
+
const overflowClass = maxHeight ? 'overflow-hidden' : '';
|
|
322
|
+
|
|
323
|
+
return (
|
|
324
|
+
<div
|
|
325
|
+
className={`${heightClass} ${overflowClass} flex flex-col bg-[rgb(var(--ec-card-bg,var(--ec-page-bg)))] border border-[rgb(var(--ec-page-border))] rounded-md shadow-xs`}
|
|
326
|
+
style={containerStyle}
|
|
327
|
+
>
|
|
328
|
+
{/* Toolbar */}
|
|
329
|
+
{searchBool && (
|
|
330
|
+
<div className="flex-shrink-0 bg-[rgb(var(--ec-card-bg,var(--ec-page-bg)))] pt-4 px-4 mb-4 pb-3 border-b border-[rgb(var(--ec-page-border))] shadow-xs">
|
|
331
|
+
<div className="flex flex-col sm:flex-row gap-3">
|
|
332
|
+
<div className="flex-1 relative">
|
|
333
|
+
<input
|
|
334
|
+
ref={searchInputRef}
|
|
335
|
+
type="text"
|
|
336
|
+
value={searchQuery}
|
|
337
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
338
|
+
placeholder="Search fields..."
|
|
339
|
+
className="w-full px-3 py-1.5 pr-20 text-sm border border-[rgb(var(--ec-input-border))] bg-[rgb(var(--ec-input-bg))] text-[rgb(var(--ec-input-text))] placeholder:text-[rgb(var(--ec-input-placeholder))] rounded-md focus:outline-hidden focus:ring-2 focus:ring-[rgb(var(--ec-accent))] focus:border-transparent"
|
|
340
|
+
onKeyDown={(e) => {
|
|
341
|
+
if (e.key === 'Enter') {
|
|
342
|
+
e.preventDefault();
|
|
343
|
+
if (e.shiftKey) {
|
|
344
|
+
handlePrevMatch();
|
|
345
|
+
} else {
|
|
346
|
+
handleNextMatch();
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}}
|
|
350
|
+
/>
|
|
351
|
+
<div className="absolute right-2 top-1/2 transform -translate-y-1/2 flex items-center gap-1">
|
|
352
|
+
<button
|
|
353
|
+
onClick={handlePrevMatch}
|
|
354
|
+
disabled={currentMatches.length === 0 || currentMatchIndex <= 0}
|
|
355
|
+
className="p-1 text-[rgb(var(--ec-icon-color))] hover:text-[rgb(var(--ec-page-text))] disabled:opacity-30 disabled:cursor-not-allowed"
|
|
356
|
+
title="Previous match"
|
|
357
|
+
>
|
|
358
|
+
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
359
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 19l-7-7 7-7" />
|
|
360
|
+
</svg>
|
|
361
|
+
</button>
|
|
362
|
+
<button
|
|
363
|
+
onClick={handleNextMatch}
|
|
364
|
+
disabled={currentMatches.length === 0 || currentMatchIndex >= currentMatches.length - 1}
|
|
365
|
+
className="p-1 text-[rgb(var(--ec-icon-color))] hover:text-[rgb(var(--ec-page-text))] disabled:opacity-30 disabled:cursor-not-allowed"
|
|
366
|
+
title="Next match"
|
|
367
|
+
>
|
|
368
|
+
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
369
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5l7 7-7 7" />
|
|
370
|
+
</svg>
|
|
371
|
+
</button>
|
|
372
|
+
</div>
|
|
373
|
+
</div>
|
|
374
|
+
<div className="flex items-center gap-2">
|
|
375
|
+
{onOpenFullscreen && (
|
|
376
|
+
<button
|
|
377
|
+
onClick={onOpenFullscreen}
|
|
378
|
+
className="px-3 py-1.5 text-xs font-medium text-[rgb(var(--ec-page-text))] bg-[rgb(var(--ec-content-hover))] rounded-md hover:bg-[rgb(var(--ec-content-active))] focus:outline-hidden focus:ring-2 focus:ring-[rgb(var(--ec-accent))]"
|
|
379
|
+
title="Open in fullscreen"
|
|
380
|
+
>
|
|
381
|
+
<svg
|
|
382
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
383
|
+
className="h-3.5 w-3.5 inline-block"
|
|
384
|
+
fill="none"
|
|
385
|
+
viewBox="0 0 24 24"
|
|
386
|
+
stroke="currentColor"
|
|
387
|
+
>
|
|
388
|
+
<path
|
|
389
|
+
strokeLinecap="round"
|
|
390
|
+
strokeLinejoin="round"
|
|
391
|
+
strokeWidth={2}
|
|
392
|
+
d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5l-5-5m5 5v-4m0 4h-4"
|
|
393
|
+
/>
|
|
394
|
+
</svg>
|
|
395
|
+
</button>
|
|
396
|
+
)}
|
|
397
|
+
<button
|
|
398
|
+
onClick={handleExpandAll}
|
|
399
|
+
className="px-3 py-1.5 text-xs font-medium text-[rgb(var(--ec-page-text))] bg-[rgb(var(--ec-content-hover))] rounded-md hover:bg-[rgb(var(--ec-content-active))] focus:outline-hidden focus:ring-2 focus:ring-[rgb(var(--ec-accent))]"
|
|
400
|
+
>
|
|
401
|
+
Expand All
|
|
402
|
+
</button>
|
|
403
|
+
<button
|
|
404
|
+
onClick={handleCollapseAll}
|
|
405
|
+
className="px-3 py-1.5 text-xs font-medium text-[rgb(var(--ec-page-text))] bg-[rgb(var(--ec-content-hover))] rounded-md hover:bg-[rgb(var(--ec-content-active))] focus:outline-hidden focus:ring-2 focus:ring-[rgb(var(--ec-accent))]"
|
|
406
|
+
>
|
|
407
|
+
Collapse All
|
|
408
|
+
</button>
|
|
409
|
+
<div className="text-xs text-[rgb(var(--ec-page-text-muted))]">
|
|
410
|
+
{totalFields} {totalFields === 1 ? 'field' : 'fields'}
|
|
411
|
+
</div>
|
|
412
|
+
</div>
|
|
413
|
+
</div>
|
|
414
|
+
{searchQuery && (
|
|
415
|
+
<div className="mt-2 text-xs text-[rgb(var(--ec-page-text-muted))]">
|
|
416
|
+
{currentMatches.length > 0
|
|
417
|
+
? `${currentMatchIndex + 1} of ${currentMatches.length} ${currentMatches.length === 1 ? 'match' : 'matches'}`
|
|
418
|
+
: 'No fields found'}
|
|
419
|
+
</div>
|
|
420
|
+
)}
|
|
421
|
+
</div>
|
|
422
|
+
)}
|
|
423
|
+
|
|
424
|
+
{/* Messages and enums */}
|
|
425
|
+
<div ref={fieldsContainerRef} className="flex-1 px-4 pb-4 overflow-auto">
|
|
426
|
+
{(schema.package || schema.syntax) && (
|
|
427
|
+
<div className="flex items-baseline gap-2 mb-4">
|
|
428
|
+
{schema.package && (
|
|
429
|
+
<>
|
|
430
|
+
<span className="text-sm font-medium text-[rgb(var(--ec-page-text-muted))]">Package:</span>
|
|
431
|
+
<span className="font-mono text-sm text-blue-600 dark:text-blue-400">{schema.package}</span>
|
|
432
|
+
</>
|
|
433
|
+
)}
|
|
434
|
+
{schema.syntax && <span className="font-mono text-xs text-[rgb(var(--ec-page-text-muted))]">({schema.syntax})</span>}
|
|
435
|
+
</div>
|
|
436
|
+
)}
|
|
437
|
+
|
|
438
|
+
{schema.messages.length > 0 || schema.enums.length > 0 ? (
|
|
439
|
+
<>
|
|
440
|
+
{schema.messages.map((message) => (
|
|
441
|
+
<div key={message.name} className="mt-4 first:mt-0">
|
|
442
|
+
<div className="flex items-baseline gap-2 mb-2">
|
|
443
|
+
<span className="text-sm font-medium text-[rgb(var(--ec-page-text-muted))]">Message:</span>
|
|
444
|
+
<span className="font-mono text-sm text-blue-600 dark:text-blue-400">{message.name}</span>
|
|
445
|
+
</div>
|
|
446
|
+
{message.doc && <p className="text-[rgb(var(--ec-page-text-muted))] text-xs mb-3">{message.doc}</p>}
|
|
447
|
+
{message.fields.length > 0 ? (
|
|
448
|
+
message.fields.map((field) => (
|
|
449
|
+
<ProtobufFieldRow
|
|
450
|
+
key={field.name}
|
|
451
|
+
field={field}
|
|
452
|
+
level={0}
|
|
453
|
+
expand={expandAll}
|
|
454
|
+
showRequired={showRequiredBool}
|
|
455
|
+
registry={registry}
|
|
456
|
+
packageName={schema.package}
|
|
457
|
+
ancestors={[message.name]}
|
|
458
|
+
/>
|
|
459
|
+
))
|
|
460
|
+
) : (
|
|
461
|
+
<p className="text-xs text-[rgb(var(--ec-page-text-muted))] mb-2">No fields defined</p>
|
|
462
|
+
)}
|
|
463
|
+
</div>
|
|
464
|
+
))}
|
|
465
|
+
|
|
466
|
+
{schema.enums.map((protoEnum) => (
|
|
467
|
+
<div key={protoEnum.name} className="mt-4">
|
|
468
|
+
<div className="flex items-baseline gap-2 mb-2">
|
|
469
|
+
<span className="text-sm font-medium text-[rgb(var(--ec-page-text-muted))]">Enum:</span>
|
|
470
|
+
<span className="font-mono text-sm text-blue-600 dark:text-blue-400">{protoEnum.name}</span>
|
|
471
|
+
</div>
|
|
472
|
+
{protoEnum.doc && <p className="text-[rgb(var(--ec-page-text-muted))] text-xs mb-2">{protoEnum.doc}</p>}
|
|
473
|
+
<div className="text-xs text-[rgb(var(--ec-page-text-muted))]">
|
|
474
|
+
<span className="text-xs inline-block">Allowed values:</span>
|
|
475
|
+
{protoEnum.values.map((value) => (
|
|
476
|
+
<span key={value.name} className="text-xs">
|
|
477
|
+
{' '}
|
|
478
|
+
<code className="bg-[rgb(var(--ec-content-hover))] px-1 rounded text-[rgb(var(--ec-page-text))] font-thin py-0.5">
|
|
479
|
+
{value.name}
|
|
480
|
+
</code>
|
|
481
|
+
</span>
|
|
482
|
+
))}
|
|
483
|
+
</div>
|
|
484
|
+
</div>
|
|
485
|
+
))}
|
|
486
|
+
</>
|
|
487
|
+
) : (
|
|
488
|
+
<div className="text-center py-8 text-[rgb(var(--ec-icon-color))]">
|
|
489
|
+
<p className="text-sm">No messages defined</p>
|
|
490
|
+
</div>
|
|
491
|
+
)}
|
|
492
|
+
|
|
493
|
+
{searchQuery && currentMatches.length === 0 && (
|
|
494
|
+
<div className="text-center py-8">
|
|
495
|
+
<div className="text-[rgb(var(--ec-icon-color))] text-sm">
|
|
496
|
+
<svg className="mx-auto h-12 w-12 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
497
|
+
<path
|
|
498
|
+
strokeLinecap="round"
|
|
499
|
+
strokeLinejoin="round"
|
|
500
|
+
strokeWidth="2"
|
|
501
|
+
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
|
502
|
+
/>
|
|
503
|
+
</svg>
|
|
504
|
+
<p>No fields match your search</p>
|
|
505
|
+
<p className="text-xs mt-1">Try a different search term or clear the search to see all fields</p>
|
|
506
|
+
</div>
|
|
507
|
+
</div>
|
|
508
|
+
)}
|
|
509
|
+
</div>
|
|
510
|
+
|
|
511
|
+
<style>{`
|
|
512
|
+
.search-dimmed {
|
|
513
|
+
opacity: 0.4;
|
|
514
|
+
transition: opacity 0.2s ease;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
.search-match {
|
|
518
|
+
opacity: 1;
|
|
519
|
+
transition: opacity 0.2s ease;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
.search-current-match {
|
|
523
|
+
background-color: rgba(59, 130, 246, 0.1);
|
|
524
|
+
border-left: 3px solid #3b82f6;
|
|
525
|
+
padding-left: 8px;
|
|
526
|
+
margin-left: -11px;
|
|
527
|
+
transition: all 0.2s ease;
|
|
528
|
+
}
|
|
529
|
+
`}</style>
|
|
530
|
+
</div>
|
|
531
|
+
);
|
|
532
|
+
}
|
|
@@ -5,6 +5,7 @@ import { oneLight } from 'react-syntax-highlighter/dist/cjs/styles/prism';
|
|
|
5
5
|
import { buildUrl } from '@utils/url-builder';
|
|
6
6
|
import JSONSchemaViewer from './JSONSchemaViewer';
|
|
7
7
|
import AvroSchemaViewer from './AvroSchemaViewer';
|
|
8
|
+
import ProtobufSchemaViewer from './ProtobufSchemaViewer';
|
|
8
9
|
import { getLanguageForHighlight } from './utils';
|
|
9
10
|
import type { SchemaItem } from './types';
|
|
10
11
|
import { useDarkMode } from './useDarkMode';
|
|
@@ -16,6 +17,7 @@ interface SchemaContentViewerProps {
|
|
|
16
17
|
viewMode: 'code' | 'schema' | 'diff';
|
|
17
18
|
parsedSchema: any;
|
|
18
19
|
parsedAvroSchema?: any;
|
|
20
|
+
parsedProtoSchema?: any;
|
|
19
21
|
onOpenFullscreen?: () => void;
|
|
20
22
|
showRequired?: boolean;
|
|
21
23
|
}
|
|
@@ -27,6 +29,7 @@ export default function SchemaContentViewer({
|
|
|
27
29
|
viewMode,
|
|
28
30
|
parsedSchema,
|
|
29
31
|
parsedAvroSchema,
|
|
32
|
+
parsedProtoSchema,
|
|
30
33
|
showRequired = false,
|
|
31
34
|
onOpenFullscreen,
|
|
32
35
|
}: SchemaContentViewerProps) {
|
|
@@ -45,6 +48,9 @@ export default function SchemaContentViewer({
|
|
|
45
48
|
if (parsedAvroSchema) {
|
|
46
49
|
return <AvroSchemaViewer schema={parsedAvroSchema} onOpenFullscreen={onOpenFullscreen} showRequired={showRequired} />;
|
|
47
50
|
}
|
|
51
|
+
if (parsedProtoSchema) {
|
|
52
|
+
return <ProtobufSchemaViewer schema={parsedProtoSchema} onOpenFullscreen={onOpenFullscreen} showRequired={showRequired} />;
|
|
53
|
+
}
|
|
48
54
|
if (parsedSchema) {
|
|
49
55
|
return <JSONSchemaViewer schema={parsedSchema} onOpenFullscreen={onOpenFullscreen} />;
|
|
50
56
|
}
|
|
@@ -25,6 +25,7 @@ import VersionHistoryModal from './VersionHistoryModal';
|
|
|
25
25
|
import SchemaCodeModal from './SchemaCodeModal';
|
|
26
26
|
import SchemaViewerModal from './SchemaViewerModal';
|
|
27
27
|
import { copyToClipboard, downloadSchema, getSchemaTypeLabel, ICON_SPECS, extractServiceName } from './utils';
|
|
28
|
+
import { parseProtobufSchema } from '@utils/protobuf-schema';
|
|
28
29
|
import type { SchemaItem, VersionDiff, Owner, Producer, Consumer } from './types';
|
|
29
30
|
|
|
30
31
|
interface SchemaDetailsPanelProps {
|
|
@@ -136,6 +137,21 @@ export default function SchemaDetailsPanel({
|
|
|
136
137
|
}
|
|
137
138
|
}, [message.schemaContent, message.schemaExtension]);
|
|
138
139
|
|
|
140
|
+
// Check if this is a Protobuf schema. The extension falls back to the schema
|
|
141
|
+
// format when the schema has no file path, so accept both 'proto' and 'protobuf'.
|
|
142
|
+
const parsedProtoSchema = useMemo(() => {
|
|
143
|
+
const extLower = message.schemaExtension?.toLowerCase();
|
|
144
|
+
const isProtoSchema =
|
|
145
|
+
(extLower === 'proto' || extLower === 'protobuf') && message.schemaContent && message.schemaContent.trim() !== '';
|
|
146
|
+
if (!isProtoSchema) return null;
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
return parseProtobufSchema(message.schemaContent ?? '');
|
|
150
|
+
} catch {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}, [message.schemaContent, message.schemaExtension]);
|
|
154
|
+
|
|
139
155
|
const handleCopy = async () => {
|
|
140
156
|
if (!message.schemaContent) return;
|
|
141
157
|
const success = await copyToClipboard(message.schemaContent);
|
|
@@ -184,7 +200,7 @@ export default function SchemaDetailsPanel({
|
|
|
184
200
|
};
|
|
185
201
|
}, []);
|
|
186
202
|
|
|
187
|
-
const hasParsedSchema = !!parsedSchema || !!parsedAvroSchema;
|
|
203
|
+
const hasParsedSchema = !!parsedSchema || !!parsedAvroSchema || !!parsedProtoSchema;
|
|
188
204
|
// Build tabs
|
|
189
205
|
const tabs: { id: string; label: string; icon: React.ReactNode }[] = [
|
|
190
206
|
{ id: 'code', label: 'Schema', icon: <CodeBracketIcon className="h-3.5 w-3.5" /> },
|
|
@@ -335,11 +351,12 @@ export default function SchemaDetailsPanel({
|
|
|
335
351
|
viewMode={activeTab === 'schema' ? 'schema' : 'code'}
|
|
336
352
|
parsedSchema={parsedSchema}
|
|
337
353
|
parsedAvroSchema={parsedAvroSchema}
|
|
354
|
+
parsedProtoSchema={parsedProtoSchema}
|
|
338
355
|
showRequired={true}
|
|
339
356
|
onOpenFullscreen={
|
|
340
357
|
activeTab === 'code'
|
|
341
358
|
? () => setIsCodeModalOpen(true)
|
|
342
|
-
: activeTab === 'schema' && (parsedSchema || parsedAvroSchema)
|
|
359
|
+
: activeTab === 'schema' && (parsedSchema || parsedAvroSchema || parsedProtoSchema)
|
|
343
360
|
? () => setIsSchemaViewerModalOpen(true)
|
|
344
361
|
: undefined
|
|
345
362
|
}
|
|
@@ -507,6 +524,7 @@ export default function SchemaDetailsPanel({
|
|
|
507
524
|
message={message}
|
|
508
525
|
parsedSchema={parsedSchema}
|
|
509
526
|
parsedAvroSchema={parsedAvroSchema}
|
|
527
|
+
parsedProtoSchema={parsedProtoSchema}
|
|
510
528
|
/>
|
|
511
529
|
</div>
|
|
512
530
|
);
|
|
@@ -18,6 +18,7 @@ const SCHEMA_TYPE_LABELS: Record<string, string> = {
|
|
|
18
18
|
avro: 'Avro',
|
|
19
19
|
avsc: 'Avro',
|
|
20
20
|
proto: 'Protobuf',
|
|
21
|
+
protobuf: 'Protobuf',
|
|
21
22
|
yaml: 'YAML',
|
|
22
23
|
yml: 'YAML',
|
|
23
24
|
xml: 'XML',
|
|
@@ -80,8 +81,16 @@ export default function SchemaExplorer({ schemas, apiAccessEnabled = false }: Sc
|
|
|
80
81
|
|
|
81
82
|
const [showFormatFilters, setShowFormatFilters] = useState(() => {
|
|
82
83
|
if (typeof window !== 'undefined') {
|
|
83
|
-
const
|
|
84
|
-
|
|
84
|
+
const storedSchemaType = localStorage.getItem('schemaRegistrySelectedSchemaType');
|
|
85
|
+
if (storedSchemaType !== null && storedSchemaType !== 'all') return true;
|
|
86
|
+
const storedTypes = localStorage.getItem('schemaRegistrySelectedTypes');
|
|
87
|
+
if (storedTypes) {
|
|
88
|
+
try {
|
|
89
|
+
return JSON.parse(storedTypes).length > 0;
|
|
90
|
+
} catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
85
94
|
}
|
|
86
95
|
return false;
|
|
87
96
|
});
|
|
@@ -396,9 +405,9 @@ export default function SchemaExplorer({ schemas, apiAccessEnabled = false }: Sc
|
|
|
396
405
|
{schemaTypes.length > 0 && (
|
|
397
406
|
<button
|
|
398
407
|
onClick={() => setShowFormatFilters((prev) => !prev)}
|
|
399
|
-
aria-pressed={showFormatFilters
|
|
408
|
+
aria-pressed={showFormatFilters}
|
|
400
409
|
className={`relative inline-flex h-9 w-9 items-center justify-center rounded-lg border transition-all ${
|
|
401
|
-
showFormatFilters ||
|
|
410
|
+
showFormatFilters || activeFilterCount > 0
|
|
402
411
|
? 'border-[rgb(var(--ec-accent)/0.5)] bg-[rgb(var(--ec-accent))] text-white shadow-[0_10px_28px_rgb(var(--ec-accent)/0.3)]'
|
|
403
412
|
: 'border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-dropdown-bg))] text-[rgb(var(--ec-page-text-muted))] hover:border-[rgb(var(--ec-page-text-muted)/0.45)] hover:text-[rgb(var(--ec-page-text))]'
|
|
404
413
|
}`}
|
|
@@ -413,7 +422,7 @@ export default function SchemaExplorer({ schemas, apiAccessEnabled = false }: Sc
|
|
|
413
422
|
)}
|
|
414
423
|
</div>
|
|
415
424
|
|
|
416
|
-
{
|
|
425
|
+
{showFormatFilters && (
|
|
417
426
|
<div className="flex-shrink-0 border-b border-[rgb(var(--ec-page-border))] px-4 py-3">
|
|
418
427
|
<div className="rounded-xl border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-content-hover)/0.45)] p-3">
|
|
419
428
|
<div className="mb-3 flex items-center justify-between gap-3">
|