@eventcatalog/core 3.25.6 → 3.26.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 (35) 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-P23BMUBV.js → chunk-6BTN7CY7.js} +1 -1
  6. package/dist/{chunk-ZEOK723Y.js → chunk-EL6ZQNAX.js} +1 -1
  7. package/dist/{chunk-53HXLUNO.js → chunk-LTWPA4SA.js} +1 -1
  8. package/dist/{chunk-R7P4GTFQ.js → chunk-N3QSCVYA.js} +1 -1
  9. package/dist/{chunk-2ILJMBQM.js → chunk-Y736FREK.js} +1 -1
  10. package/dist/constants.cjs +1 -1
  11. package/dist/constants.js +1 -1
  12. package/dist/eventcatalog.cjs +562 -19
  13. package/dist/eventcatalog.js +576 -31
  14. package/dist/generate.cjs +1 -1
  15. package/dist/generate.js +3 -3
  16. package/dist/utils/cli-logger.cjs +1 -1
  17. package/dist/utils/cli-logger.js +2 -2
  18. package/eventcatalog/astro.config.mjs +2 -1
  19. package/eventcatalog/integrations/eventcatalog-features.ts +13 -0
  20. package/eventcatalog/public/icons/graphql.svg +3 -1
  21. package/eventcatalog/src/components/FieldsExplorer/FieldFilters.tsx +225 -0
  22. package/eventcatalog/src/components/FieldsExplorer/FieldNodeGraph.tsx +521 -0
  23. package/eventcatalog/src/components/FieldsExplorer/FieldsExplorer.tsx +501 -0
  24. package/eventcatalog/src/components/FieldsExplorer/FieldsTable.tsx +236 -0
  25. package/eventcatalog/src/enterprise/fields/field-extractor.test.ts +241 -0
  26. package/eventcatalog/src/enterprise/fields/field-extractor.ts +183 -0
  27. package/eventcatalog/src/enterprise/fields/field-indexer.ts +131 -0
  28. package/eventcatalog/src/enterprise/fields/fields-db.test.ts +186 -0
  29. package/eventcatalog/src/enterprise/fields/fields-db.ts +453 -0
  30. package/eventcatalog/src/enterprise/fields/pages/api/fields.ts +43 -0
  31. package/eventcatalog/src/enterprise/fields/pages/fields.astro +19 -0
  32. package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +23 -3
  33. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/graphql/[filename].astro +14 -16
  34. package/eventcatalog/src/utils/node-graphs/field-node-graph.ts +192 -0
  35. package/package.json +6 -4
@@ -0,0 +1,501 @@
1
+ import { useState, useEffect, useCallback, useRef } from 'react';
2
+ import { ChevronLeft, ChevronRight, Lock, X } from 'lucide-react';
3
+ import FieldFilters from './FieldFilters';
4
+ import FieldsTable from './FieldsTable';
5
+ import type { FieldResult } from './FieldsTable';
6
+ import FieldNodeGraph from './FieldNodeGraph';
7
+ import { buildUrl } from '@utils/url-builder';
8
+
9
+ function DummyNode({
10
+ type,
11
+ name,
12
+ version,
13
+ className,
14
+ }: {
15
+ type: 'service' | 'event' | 'field';
16
+ name: string;
17
+ version?: string;
18
+ className?: string;
19
+ }) {
20
+ const styles = {
21
+ service: { border: 'border-pink-500', badge: 'bg-pink-500', label: 'SERVICE' },
22
+ event: { border: 'border-orange-500', badge: 'bg-orange-500', label: 'EVENT' },
23
+ field: { border: 'border-cyan-500', badge: 'bg-cyan-500', label: 'FIELD' },
24
+ };
25
+ const s = styles[type];
26
+ return (
27
+ <div
28
+ className={`relative rounded-xl border-2 ${s.border} bg-[rgb(var(--ec-card-bg))] px-3 pt-4 pb-2.5 min-w-[140px] ${className || ''}`}
29
+ >
30
+ <div
31
+ className={`absolute -top-2.5 left-2.5 ${s.badge} text-white text-[7px] font-bold uppercase tracking-widest px-1.5 py-0.5 rounded`}
32
+ >
33
+ {s.label}
34
+ </div>
35
+ <div className="text-[11px] font-semibold text-[rgb(var(--ec-page-text))]">{name}</div>
36
+ {version && <div className="text-[9px] text-[rgb(var(--ec-page-text-muted))]">v{version}</div>}
37
+ </div>
38
+ );
39
+ }
40
+
41
+ function DummyEdge({ label }: { label: string }) {
42
+ return (
43
+ <div className="flex items-center gap-1">
44
+ <div className="w-8 h-px bg-[rgb(var(--ec-page-border))]" />
45
+ <span className="text-[8px] text-[rgb(var(--ec-page-text-muted))] whitespace-nowrap">{label}</span>
46
+ <div className="w-8 h-px bg-[rgb(var(--ec-page-border))]" />
47
+ </div>
48
+ );
49
+ }
50
+
51
+ function FieldLineageUpgradeModal({ fieldPath, onClose }: { fieldPath: string; onClose: () => void }) {
52
+ return (
53
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={onClose}>
54
+ <div
55
+ className="w-[90vw] h-[80vh] max-w-[1400px] rounded-xl border shadow-2xl flex flex-col overflow-hidden relative"
56
+ style={{
57
+ backgroundColor: 'rgb(var(--ec-page-bg))',
58
+ borderColor: 'rgb(var(--ec-page-border))',
59
+ }}
60
+ onClick={(e) => e.stopPropagation()}
61
+ >
62
+ {/* Header */}
63
+ <div
64
+ className="flex items-center justify-between px-5 py-3 border-b flex-shrink-0"
65
+ style={{ borderColor: 'rgb(var(--ec-page-border))' }}
66
+ >
67
+ <div className="flex items-center gap-2">
68
+ <h3 className="text-sm font-semibold" style={{ color: 'rgb(var(--ec-page-text))' }}>
69
+ Field Traceability
70
+ </h3>
71
+ <code
72
+ className="px-2 py-0.5 rounded text-xs font-mono"
73
+ style={{
74
+ backgroundColor: 'rgb(var(--ec-accent) / 0.1)',
75
+ color: 'rgb(var(--ec-accent))',
76
+ }}
77
+ >
78
+ {fieldPath}
79
+ </code>
80
+ </div>
81
+ <button
82
+ onClick={onClose}
83
+ className="p-1.5 rounded-lg transition-colors hover:bg-[rgb(var(--ec-content-hover))] flex-shrink-0"
84
+ style={{ color: 'rgb(var(--ec-icon-color))' }}
85
+ >
86
+ <X className="w-4 h-4" />
87
+ </button>
88
+ </div>
89
+
90
+ {/* Blurred dummy content */}
91
+ <div className="flex-1 relative overflow-hidden">
92
+ {/* Fake graph + side panel */}
93
+ <div className="absolute inset-0 flex blur-[2px] opacity-60 select-none pointer-events-none">
94
+ {/* Fake graph area */}
95
+ <div className="flex-1 flex items-center justify-center p-8">
96
+ <div className="flex items-center gap-3">
97
+ {/* Producer services */}
98
+ <div className="flex flex-col gap-6">
99
+ <DummyNode type="service" name="Billing Service" version="1.2.0" />
100
+ <DummyNode type="service" name="Order Service" version="2.0.1" />
101
+ </div>
102
+ {/* Edges to events */}
103
+ <div className="flex flex-col gap-12">
104
+ <DummyEdge label="produces" />
105
+ <DummyEdge label="produces" />
106
+ </div>
107
+ {/* Events */}
108
+ <div className="flex flex-col gap-6">
109
+ <DummyNode type="event" name="Payment Due" version="0.0.1" />
110
+ <DummyNode type="event" name="Order Created" version="1.0.0" />
111
+ </div>
112
+ {/* Edges to field */}
113
+ <div className="flex flex-col gap-12">
114
+ <DummyEdge label="contains" />
115
+ <DummyEdge label="contains" />
116
+ </div>
117
+ {/* Field */}
118
+ <div className="flex flex-col gap-6">
119
+ <DummyNode type="field" name={fieldPath} />
120
+ </div>
121
+ {/* Edges to consumers */}
122
+ <div className="flex flex-col gap-12">
123
+ <DummyEdge label="consumed by" />
124
+ </div>
125
+ {/* Consumer services */}
126
+ <div className="flex flex-col gap-6">
127
+ <DummyNode type="service" name="Notification Service" version="0.0.2" />
128
+ </div>
129
+ </div>
130
+ </div>
131
+ {/* Fake side panel */}
132
+ <div className="w-[280px] flex-shrink-0 border-l border-[rgb(var(--ec-page-border))] p-4 space-y-4">
133
+ <div>
134
+ <div className="text-[11px] font-medium text-[rgb(var(--ec-page-text-muted))] uppercase tracking-wider mb-2">
135
+ Field
136
+ </div>
137
+ <div className="text-sm font-mono font-semibold text-[rgb(var(--ec-page-text))]">{fieldPath}</div>
138
+ <div className="text-xs text-[rgb(var(--ec-page-text-muted))] mt-1">string</div>
139
+ </div>
140
+ <div>
141
+ <div className="text-[11px] font-medium text-amber-500 uppercase tracking-wider mb-2">Type Conflict</div>
142
+ <div className="rounded-lg border border-amber-500/20 bg-amber-500/5 p-2.5 space-y-1">
143
+ <div className="text-xs text-amber-600">Inconsistent types detected</div>
144
+ <div className="flex justify-between text-xs">
145
+ <code className="font-mono">string</code>
146
+ <span className="text-[rgb(var(--ec-page-text-muted))]">3 schemas</span>
147
+ </div>
148
+ <div className="flex justify-between text-xs">
149
+ <code className="font-mono">integer</code>
150
+ <span className="text-[rgb(var(--ec-page-text-muted))]">1 schema</span>
151
+ </div>
152
+ </div>
153
+ </div>
154
+ <div>
155
+ <div className="text-[11px] font-medium text-[rgb(var(--ec-page-text-muted))] uppercase tracking-wider mb-2">
156
+ Messages (3)
157
+ </div>
158
+ <div className="space-y-1.5">
159
+ <div className="rounded-lg border border-[rgb(var(--ec-page-border)/0.5)] bg-[rgb(var(--ec-card-bg))] px-2.5 py-2 text-xs">
160
+ Payment Due
161
+ </div>
162
+ <div className="rounded-lg border border-[rgb(var(--ec-page-border)/0.5)] bg-[rgb(var(--ec-card-bg))] px-2.5 py-2 text-xs">
163
+ Order Created
164
+ </div>
165
+ <div className="rounded-lg border border-[rgb(var(--ec-page-border)/0.5)] bg-[rgb(var(--ec-card-bg))] px-2.5 py-2 text-xs">
166
+ Invoice Generated
167
+ </div>
168
+ </div>
169
+ </div>
170
+ </div>
171
+ </div>
172
+
173
+ {/* Upgrade prompt overlay */}
174
+ <div className="absolute inset-0 flex items-center justify-center bg-[rgb(var(--ec-page-bg)/0.5)]">
175
+ <div className="text-center max-w-lg px-6 py-8 rounded-2xl border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-page-bg))] shadow-2xl">
176
+ <div className="w-12 h-12 rounded-full bg-[rgb(var(--ec-accent)/0.1)] flex items-center justify-center mx-auto mb-4">
177
+ <Lock className="w-6 h-6 text-[rgb(var(--ec-accent))]" />
178
+ </div>
179
+ <h3 className="text-lg font-semibold text-[rgb(var(--ec-page-text))] mb-2">Field Lineage & Traceability</h3>
180
+ <p className="text-sm text-[rgb(var(--ec-page-text-muted))] mb-5 leading-relaxed">
181
+ See which services produce and consume this field, detect type conflicts across schemas, and trace field usage
182
+ across your entire event-driven architecture.
183
+ </p>
184
+ <a
185
+ href="https://eventcatalog.cloud/"
186
+ target="_blank"
187
+ rel="noopener noreferrer"
188
+ className="inline-flex items-center gap-2 px-5 py-2.5 rounded-lg text-sm font-medium text-white bg-[rgb(var(--ec-accent))] hover:opacity-90 transition-opacity"
189
+ >
190
+ Start your 14-day free trial
191
+ </a>
192
+ <p className="text-xs text-[rgb(var(--ec-page-text-muted))] mt-3">
193
+ Available with EventCatalog Scale. No credit card required.
194
+ </p>
195
+ </div>
196
+ </div>
197
+ </div>
198
+ </div>
199
+ </div>
200
+ );
201
+ }
202
+
203
+ interface Facets {
204
+ formats: { value: string; count: number }[];
205
+ types: { value: string; count: number }[];
206
+ messageTypes: { value: string; count: number }[];
207
+ }
208
+
209
+ interface FieldsApiResponse {
210
+ fields: FieldResult[];
211
+ total: number;
212
+ cursor: string | null;
213
+ facets: Facets;
214
+ error?: string;
215
+ }
216
+
217
+ interface SelectedField {
218
+ path: string;
219
+ type: string;
220
+ description: string;
221
+ required: boolean;
222
+ conflicts?: { type: string; count: number }[];
223
+ occurrences: FieldResult[];
224
+ }
225
+
226
+ interface FieldsExplorerProps {
227
+ isScaleEnabled?: boolean;
228
+ }
229
+
230
+ export default function FieldsExplorer({ isScaleEnabled = false }: FieldsExplorerProps) {
231
+ const [fields, setFields] = useState<FieldResult[]>([]);
232
+ const [total, setTotal] = useState(0);
233
+ const [cursor, setCursor] = useState<string | null>(null);
234
+ const [facets, setFacets] = useState<Facets | null>(null);
235
+ const [isLoading, setIsLoading] = useState(true);
236
+ const [error, setError] = useState<string | null>(null);
237
+
238
+ // Filter state
239
+ const [searchQuery, setSearchQuery] = useState('');
240
+ const [selectedFormats, setSelectedFormats] = useState<string[]>([]);
241
+ const [selectedMessageTypes, setSelectedMessageTypes] = useState<string[]>([]);
242
+ const [sharedOnly, setSharedOnly] = useState(false);
243
+ const [conflictingOnly, setConflictingOnly] = useState(false);
244
+
245
+ // Modal state
246
+ const [selectedField, setSelectedField] = useState<SelectedField | null>(null);
247
+
248
+ // Pagination cursors
249
+ const [cursorHistory, setCursorHistory] = useState<string[]>([]);
250
+ const [currentPageIndex, setCurrentPageIndex] = useState(0);
251
+
252
+ // Abort controller for in-flight requests
253
+ const abortControllerRef = useRef<AbortController | null>(null);
254
+
255
+ const buildQueryParams = useCallback(
256
+ (extraParams?: Record<string, string>) => {
257
+ const params = new URLSearchParams();
258
+ if (searchQuery) params.set('q', searchQuery);
259
+ if (selectedFormats.length > 0) params.set('format', selectedFormats.join(','));
260
+ if (selectedMessageTypes.length > 0) params.set('messageType', selectedMessageTypes.join(','));
261
+ if (sharedOnly) params.set('shared', 'true');
262
+ if (conflictingOnly) params.set('conflicting', 'true');
263
+ if (extraParams) {
264
+ Object.entries(extraParams).forEach(([key, value]) => {
265
+ params.set(key, value);
266
+ });
267
+ }
268
+ return params;
269
+ },
270
+ [searchQuery, selectedFormats, selectedMessageTypes, sharedOnly, conflictingOnly]
271
+ );
272
+
273
+ const fetchFields = useCallback(
274
+ async (cursorValue?: string) => {
275
+ // Cancel any in-flight request
276
+ if (abortControllerRef.current) {
277
+ abortControllerRef.current.abort();
278
+ }
279
+ const controller = new AbortController();
280
+ abortControllerRef.current = controller;
281
+
282
+ setIsLoading(true);
283
+ setError(null);
284
+
285
+ try {
286
+ const params = buildQueryParams(cursorValue ? { cursor: cursorValue } : undefined);
287
+ const response = await fetch(buildUrl(`/api/schemas/fields?${params.toString()}`), {
288
+ signal: controller.signal,
289
+ });
290
+
291
+ if (!response.ok) {
292
+ const data = await response.json();
293
+ throw new Error(data.error || `HTTP ${response.status}`);
294
+ }
295
+
296
+ const data: FieldsApiResponse = await response.json();
297
+ setFields(data.fields || []);
298
+ setTotal(data.total || 0);
299
+ setCursor(data.cursor || null);
300
+ if (data.facets) {
301
+ setFacets({
302
+ formats: data.facets.formats || [],
303
+ types: data.facets.types || [],
304
+ messageTypes: data.facets.messageTypes || [],
305
+ });
306
+ }
307
+ } catch (err: any) {
308
+ if (err.name === 'AbortError') return;
309
+ setError(err.message || 'Failed to load fields');
310
+ } finally {
311
+ if (!controller.signal.aborted) {
312
+ setIsLoading(false);
313
+ }
314
+ }
315
+ },
316
+ [buildQueryParams]
317
+ );
318
+
319
+ // Initial fetch and re-fetch when filters change
320
+ useEffect(() => {
321
+ setCursorHistory([]);
322
+ setCurrentPageIndex(0);
323
+ fetchFields();
324
+ }, [searchQuery, selectedFormats, selectedMessageTypes, sharedOnly, conflictingOnly]);
325
+
326
+ // Open modal: fetch all occurrences for the field path, then show modal
327
+ const handleSelectField = useCallback(async (fieldPath: string) => {
328
+ // Non-scale users get the locked upgrade modal
329
+ if (!isScaleEnabled) {
330
+ setSelectedField({
331
+ path: fieldPath,
332
+ type: 'unknown',
333
+ description: '',
334
+ required: false,
335
+ occurrences: [],
336
+ });
337
+ return;
338
+ }
339
+
340
+ try {
341
+ // Fetch all pages of occurrences for this field path
342
+ let allOccurrences: FieldResult[] = [];
343
+ let nextCursor: string | null = null;
344
+
345
+ do {
346
+ const params = new URLSearchParams();
347
+ params.set('path', fieldPath);
348
+ params.set('pageSize', '100');
349
+ if (nextCursor) params.set('cursor', nextCursor);
350
+
351
+ const response = await fetch(buildUrl(`/api/schemas/fields?${params.toString()}`));
352
+ if (!response.ok) {
353
+ throw new Error(`HTTP ${response.status}`);
354
+ }
355
+
356
+ const data: FieldsApiResponse = await response.json();
357
+ allOccurrences = allOccurrences.concat(data.fields || []);
358
+ nextCursor = data.cursor;
359
+ } while (nextCursor);
360
+
361
+ setSelectedField({
362
+ path: fieldPath,
363
+ type: allOccurrences[0]?.type || 'unknown',
364
+ description: allOccurrences[0]?.description || '',
365
+ required: allOccurrences[0]?.required || false,
366
+ conflicts: allOccurrences[0]?.conflicts,
367
+ occurrences: allOccurrences,
368
+ });
369
+ } catch {
370
+ // Silently fail — user can retry by clicking again
371
+ }
372
+ }, []);
373
+
374
+ const handleNextPage = useCallback(() => {
375
+ if (!cursor) return;
376
+ setCursorHistory((prev) => [...prev, cursor]);
377
+ setCurrentPageIndex((prev) => prev + 1);
378
+ fetchFields(cursor);
379
+ }, [cursor, fetchFields]);
380
+
381
+ const handlePrevPage = useCallback(() => {
382
+ if (currentPageIndex === 0) return;
383
+ const newIndex = currentPageIndex - 1;
384
+ setCurrentPageIndex(newIndex);
385
+ if (newIndex === 0) {
386
+ fetchFields();
387
+ } else {
388
+ fetchFields(cursorHistory[newIndex - 1]);
389
+ }
390
+ setCursorHistory((prev) => prev.slice(0, -1));
391
+ }, [currentPageIndex, cursorHistory, fetchFields]);
392
+
393
+ // Map facets for FieldFilters
394
+ const filterFacets = facets
395
+ ? {
396
+ formats: facets.formats,
397
+ messageTypes: facets.messageTypes,
398
+ }
399
+ : null;
400
+
401
+ return (
402
+ <div className="flex h-full">
403
+ {/* Filter Sidebar */}
404
+ <div className="w-[320px] flex-shrink-0 flex flex-col bg-[rgb(var(--ec-page-bg))] bg-gradient-to-bl from-[rgb(var(--ec-page-bg))] via-[rgb(var(--ec-page-bg))] to-[rgb(var(--ec-accent)/0.08)] border-r border-[rgb(var(--ec-page-border))]">
405
+ <div className="flex-1 overflow-y-auto px-4 pt-4 pb-4">
406
+ <FieldFilters
407
+ searchQuery={searchQuery}
408
+ onSearchChange={setSearchQuery}
409
+ selectedFormats={selectedFormats}
410
+ onFormatsChange={setSelectedFormats}
411
+ selectedMessageTypes={selectedMessageTypes}
412
+ onMessageTypesChange={setSelectedMessageTypes}
413
+ sharedOnly={sharedOnly}
414
+ onSharedOnlyChange={setSharedOnly}
415
+ conflictingOnly={conflictingOnly}
416
+ onConflictingOnlyChange={setConflictingOnly}
417
+ facets={filterFacets}
418
+ isScaleEnabled={isScaleEnabled}
419
+ />
420
+ </div>
421
+ </div>
422
+
423
+ {/* Main Content */}
424
+ <div className="flex-1 min-w-0 flex flex-col overflow-hidden">
425
+ {/* Header */}
426
+ <div className="flex items-center justify-between px-6 py-4">
427
+ <h2 className="text-lg font-semibold text-[rgb(var(--ec-page-text))]">
428
+ Fields <span className="text-sm text-[rgb(var(--ec-page-text-muted))] font-normal ml-1">({total})</span>
429
+ </h2>
430
+ </div>
431
+
432
+ {/* Error state */}
433
+ {error && (
434
+ <div className="mx-6 mb-4 px-4 py-3 rounded-lg border border-red-300 bg-red-50 text-red-700 text-sm">{error}</div>
435
+ )}
436
+
437
+ {/* Table */}
438
+ <FieldsTable fields={fields} onSelectField={handleSelectField} isLoading={isLoading} isScaleEnabled={isScaleEnabled} />
439
+
440
+ {/* Pagination */}
441
+ <div className="flex-shrink-0 flex items-center justify-between px-6 py-3 border-t border-[rgb(var(--ec-page-border))]">
442
+ <span className="text-xs text-[rgb(var(--ec-page-text-muted))]">
443
+ {total > 0 && (
444
+ <>
445
+ <span className="font-medium text-[rgb(var(--ec-page-text))]">{fields.length}</span> of{' '}
446
+ <span className="font-medium text-[rgb(var(--ec-page-text))]">{total}</span> fields
447
+ </>
448
+ )}
449
+ </span>
450
+ <div className="flex items-center gap-1.5">
451
+ <button
452
+ className="p-1.5 text-[rgb(var(--ec-icon-color))] hover:text-[rgb(var(--ec-page-text))] disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
453
+ onClick={handlePrevPage}
454
+ disabled={currentPageIndex === 0}
455
+ title="Previous page"
456
+ >
457
+ <ChevronLeft className="w-4 h-4" />
458
+ </button>
459
+ <span className="text-xs tabular-nums text-[rgb(var(--ec-page-text-muted))] min-w-[40px] text-center">
460
+ <span className="font-medium text-[rgb(var(--ec-page-text))]">{currentPageIndex + 1}</span>
461
+ </span>
462
+ <button
463
+ className="p-1.5 text-[rgb(var(--ec-icon-color))] hover:text-[rgb(var(--ec-page-text))] disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
464
+ onClick={handleNextPage}
465
+ disabled={!cursor}
466
+ title="Next page"
467
+ >
468
+ <ChevronRight className="w-4 h-4" />
469
+ </button>
470
+ </div>
471
+ </div>
472
+ </div>
473
+
474
+ {/* Field Detail Modal with Node Graph */}
475
+ {selectedField && !isScaleEnabled && (
476
+ <FieldLineageUpgradeModal fieldPath={selectedField.path} onClose={() => setSelectedField(null)} />
477
+ )}
478
+ {selectedField && isScaleEnabled && (
479
+ <FieldNodeGraph
480
+ fieldPath={selectedField.path}
481
+ fieldType={selectedField.type}
482
+ fieldDescription={selectedField.description}
483
+ fieldRequired={selectedField.required}
484
+ fieldConflicts={selectedField.conflicts}
485
+ occurrences={selectedField.occurrences.map((f) => ({
486
+ messageId: f.messageId,
487
+ messageVersion: f.messageVersion,
488
+ messageType: f.messageType,
489
+ messageName: f.messageName,
490
+ messageSummary: f.messageSummary,
491
+ messageOwners: f.messageOwners,
492
+ fieldType: f.type,
493
+ producers: f.producers,
494
+ consumers: f.consumers,
495
+ }))}
496
+ onClose={() => setSelectedField(null)}
497
+ />
498
+ )}
499
+ </div>
500
+ );
501
+ }