@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.
- 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-P23BMUBV.js → chunk-6BTN7CY7.js} +1 -1
- package/dist/{chunk-ZEOK723Y.js → chunk-EL6ZQNAX.js} +1 -1
- package/dist/{chunk-53HXLUNO.js → chunk-LTWPA4SA.js} +1 -1
- package/dist/{chunk-R7P4GTFQ.js → chunk-N3QSCVYA.js} +1 -1
- package/dist/{chunk-2ILJMBQM.js → chunk-Y736FREK.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog.cjs +562 -19
- package/dist/eventcatalog.js +576 -31
- 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/astro.config.mjs +2 -1
- package/eventcatalog/integrations/eventcatalog-features.ts +13 -0
- package/eventcatalog/public/icons/graphql.svg +3 -1
- package/eventcatalog/src/components/FieldsExplorer/FieldFilters.tsx +225 -0
- package/eventcatalog/src/components/FieldsExplorer/FieldNodeGraph.tsx +521 -0
- package/eventcatalog/src/components/FieldsExplorer/FieldsExplorer.tsx +501 -0
- package/eventcatalog/src/components/FieldsExplorer/FieldsTable.tsx +236 -0
- package/eventcatalog/src/enterprise/fields/field-extractor.test.ts +241 -0
- package/eventcatalog/src/enterprise/fields/field-extractor.ts +183 -0
- package/eventcatalog/src/enterprise/fields/field-indexer.ts +131 -0
- package/eventcatalog/src/enterprise/fields/fields-db.test.ts +186 -0
- package/eventcatalog/src/enterprise/fields/fields-db.ts +453 -0
- package/eventcatalog/src/enterprise/fields/pages/api/fields.ts +43 -0
- package/eventcatalog/src/enterprise/fields/pages/fields.astro +19 -0
- package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +23 -3
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/graphql/[filename].astro +14 -16
- package/eventcatalog/src/utils/node-graphs/field-node-graph.ts +192 -0
- package/package.json +6 -4
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
ReactFlow,
|
|
4
|
+
ReactFlowProvider,
|
|
5
|
+
Background,
|
|
6
|
+
Controls,
|
|
7
|
+
useNodesState,
|
|
8
|
+
useEdgesState,
|
|
9
|
+
useReactFlow,
|
|
10
|
+
ConnectionLineType,
|
|
11
|
+
type Node,
|
|
12
|
+
type Edge,
|
|
13
|
+
} from '@xyflow/react';
|
|
14
|
+
import {
|
|
15
|
+
Event as EventNode,
|
|
16
|
+
Command as CommandNode,
|
|
17
|
+
Query as QueryNode,
|
|
18
|
+
Service as ServiceNode,
|
|
19
|
+
Field as FieldNode,
|
|
20
|
+
} from '@eventcatalog/visualiser';
|
|
21
|
+
import { X, ChevronDown, ChevronRight, Search, AlertTriangle } from 'lucide-react';
|
|
22
|
+
import { BoltIcon, ChatBubbleLeftIcon, MagnifyingGlassIcon, ServerIcon } from '@heroicons/react/24/solid';
|
|
23
|
+
import { getNodesAndEdges, type FieldOccurrence } from '@utils/node-graphs/field-node-graph';
|
|
24
|
+
import * as ContextMenu from '@radix-ui/react-context-menu';
|
|
25
|
+
import '@xyflow/react/dist/style.css';
|
|
26
|
+
|
|
27
|
+
interface FieldNodeGraphProps {
|
|
28
|
+
fieldPath: string;
|
|
29
|
+
fieldType: string;
|
|
30
|
+
fieldDescription?: string;
|
|
31
|
+
fieldRequired?: boolean;
|
|
32
|
+
fieldConflicts?: { type: string; count: number }[];
|
|
33
|
+
occurrences: FieldOccurrence[];
|
|
34
|
+
onClose: () => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface ContextMenuItem {
|
|
38
|
+
label: string;
|
|
39
|
+
href: string;
|
|
40
|
+
external?: boolean;
|
|
41
|
+
download?: string;
|
|
42
|
+
separator?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function wrapWithContextMenu(Component: React.ComponentType<any>) {
|
|
46
|
+
const Wrapped = memo((props: any) => {
|
|
47
|
+
const items: ContextMenuItem[] | undefined = props.data?.contextMenu;
|
|
48
|
+
if (!items?.length) return <Component {...props} />;
|
|
49
|
+
return (
|
|
50
|
+
<ContextMenu.Root>
|
|
51
|
+
<ContextMenu.Trigger asChild>
|
|
52
|
+
<div>
|
|
53
|
+
<Component {...props} />
|
|
54
|
+
</div>
|
|
55
|
+
</ContextMenu.Trigger>
|
|
56
|
+
<ContextMenu.Portal>
|
|
57
|
+
<ContextMenu.Content
|
|
58
|
+
className="min-w-[220px] bg-[rgb(var(--ec-card-bg,255_255_255))] rounded-md p-1 shadow-md border border-[rgb(var(--ec-page-border))] z-50"
|
|
59
|
+
onClick={(e) => e.stopPropagation()}
|
|
60
|
+
>
|
|
61
|
+
{items.map((item, index) => (
|
|
62
|
+
<div key={index}>
|
|
63
|
+
{item.separator && index > 0 && <ContextMenu.Separator className="h-[1px] bg-[rgb(var(--ec-page-border))] m-1" />}
|
|
64
|
+
<ContextMenu.Item
|
|
65
|
+
asChild
|
|
66
|
+
className="text-sm px-2 py-1.5 outline-none cursor-pointer hover:bg-[rgb(var(--ec-content-hover))] rounded-sm flex items-center text-[rgb(var(--ec-page-text))]"
|
|
67
|
+
>
|
|
68
|
+
<a
|
|
69
|
+
href={item.href}
|
|
70
|
+
{...(item.download ? { download: item.download } : {})}
|
|
71
|
+
{...(item.external ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
|
|
72
|
+
>
|
|
73
|
+
{item.label}
|
|
74
|
+
</a>
|
|
75
|
+
</ContextMenu.Item>
|
|
76
|
+
</div>
|
|
77
|
+
))}
|
|
78
|
+
</ContextMenu.Content>
|
|
79
|
+
</ContextMenu.Portal>
|
|
80
|
+
</ContextMenu.Root>
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
Wrapped.displayName = `WithContextMenu(${Component.displayName || Component.name || 'Component'})`;
|
|
84
|
+
return Wrapped;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const nodeTypes = {
|
|
88
|
+
service: wrapWithContextMenu(ServiceNode),
|
|
89
|
+
event: wrapWithContextMenu(EventNode),
|
|
90
|
+
command: wrapWithContextMenu(CommandNode),
|
|
91
|
+
query: wrapWithContextMenu(QueryNode),
|
|
92
|
+
field: FieldNode,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const getIconForMessageType = (type: string) => {
|
|
96
|
+
switch (type) {
|
|
97
|
+
case 'event':
|
|
98
|
+
return { Icon: BoltIcon, bg: 'bg-orange-500' };
|
|
99
|
+
case 'command':
|
|
100
|
+
return { Icon: ChatBubbleLeftIcon, bg: 'bg-blue-500' };
|
|
101
|
+
case 'query':
|
|
102
|
+
return { Icon: MagnifyingGlassIcon, bg: 'bg-green-500' };
|
|
103
|
+
default:
|
|
104
|
+
return { Icon: ChatBubbleLeftIcon, bg: 'bg-gray-500' };
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const COLLAPSED_LIMIT = 5;
|
|
109
|
+
|
|
110
|
+
interface ResourceItem {
|
|
111
|
+
key: string;
|
|
112
|
+
label: string;
|
|
113
|
+
version: string;
|
|
114
|
+
Icon: React.ComponentType<{ className?: string }>;
|
|
115
|
+
bg: string;
|
|
116
|
+
onClick: () => void;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function CollapsibleResourceList({
|
|
120
|
+
title,
|
|
121
|
+
count,
|
|
122
|
+
items,
|
|
123
|
+
defaultExpanded = true,
|
|
124
|
+
}: {
|
|
125
|
+
title: string;
|
|
126
|
+
count: number;
|
|
127
|
+
items: ResourceItem[];
|
|
128
|
+
defaultExpanded?: boolean;
|
|
129
|
+
}) {
|
|
130
|
+
const [expanded, setExpanded] = useState(defaultExpanded);
|
|
131
|
+
const [showAll, setShowAll] = useState(false);
|
|
132
|
+
const [search, setSearch] = useState('');
|
|
133
|
+
|
|
134
|
+
const filtered = search ? items.filter((item) => item.label.toLowerCase().includes(search.toLowerCase())) : items;
|
|
135
|
+
const displayed = showAll ? filtered : filtered.slice(0, COLLAPSED_LIMIT);
|
|
136
|
+
const hasMore = filtered.length > COLLAPSED_LIMIT;
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<div>
|
|
140
|
+
<button className="flex items-center justify-between w-full mb-2 group" onClick={() => setExpanded(!expanded)}>
|
|
141
|
+
<h4 className="text-[11px] font-medium text-[rgb(var(--ec-page-text-muted))] uppercase tracking-wider flex items-center gap-1">
|
|
142
|
+
{expanded ? <ChevronDown className="w-3 h-3" /> : <ChevronRight className="w-3 h-3" />}
|
|
143
|
+
{title} ({count})
|
|
144
|
+
</h4>
|
|
145
|
+
</button>
|
|
146
|
+
{expanded && (
|
|
147
|
+
<div className="space-y-1.5">
|
|
148
|
+
{items.length > COLLAPSED_LIMIT && (
|
|
149
|
+
<div className="relative mb-1">
|
|
150
|
+
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3 h-3 text-[rgb(var(--ec-icon-color))]" />
|
|
151
|
+
<input
|
|
152
|
+
type="text"
|
|
153
|
+
placeholder={`Filter ${title.toLowerCase()}...`}
|
|
154
|
+
value={search}
|
|
155
|
+
onChange={(e) => {
|
|
156
|
+
setSearch(e.target.value);
|
|
157
|
+
setShowAll(true);
|
|
158
|
+
}}
|
|
159
|
+
className="w-full pl-7 pr-2 py-1 text-[11px] rounded border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-input-bg,var(--ec-page-bg)))] text-[rgb(var(--ec-page-text))] placeholder:text-[rgb(var(--ec-page-text-muted))] focus:outline-none focus:border-[rgb(var(--ec-accent))]"
|
|
160
|
+
onClick={(e) => e.stopPropagation()}
|
|
161
|
+
/>
|
|
162
|
+
</div>
|
|
163
|
+
)}
|
|
164
|
+
{displayed.map((item) => (
|
|
165
|
+
<button
|
|
166
|
+
key={item.key}
|
|
167
|
+
className="flex items-center gap-2 w-full rounded-lg border border-[rgb(var(--ec-page-border)/0.5)] bg-[rgb(var(--ec-card-bg,var(--ec-page-bg)))] hover:border-[rgb(var(--ec-accent))] transition-colors px-2.5 py-2 text-left"
|
|
168
|
+
onClick={(e) => {
|
|
169
|
+
e.stopPropagation();
|
|
170
|
+
item.onClick();
|
|
171
|
+
}}
|
|
172
|
+
>
|
|
173
|
+
<span className={`flex items-center justify-center w-5 h-5 ${item.bg} rounded flex-shrink-0`}>
|
|
174
|
+
<item.Icon className="h-2.5 w-2.5 text-white" />
|
|
175
|
+
</span>
|
|
176
|
+
<div className="min-w-0">
|
|
177
|
+
<div className="text-xs font-medium text-[rgb(var(--ec-page-text))] truncate">{item.label}</div>
|
|
178
|
+
<div className="text-[10px] text-[rgb(var(--ec-page-text-muted))]">v{item.version}</div>
|
|
179
|
+
</div>
|
|
180
|
+
</button>
|
|
181
|
+
))}
|
|
182
|
+
{hasMore && !search && (
|
|
183
|
+
<button
|
|
184
|
+
className="text-[11px] text-[rgb(var(--ec-accent))] hover:underline w-full text-left pl-1"
|
|
185
|
+
onClick={(e) => {
|
|
186
|
+
e.stopPropagation();
|
|
187
|
+
setShowAll(!showAll);
|
|
188
|
+
}}
|
|
189
|
+
>
|
|
190
|
+
{showAll ? 'Show less' : `Show all ${filtered.length}`}
|
|
191
|
+
</button>
|
|
192
|
+
)}
|
|
193
|
+
{search && filtered.length === 0 && (
|
|
194
|
+
<div className="text-[11px] text-[rgb(var(--ec-page-text-muted))] pl-1">No matches</div>
|
|
195
|
+
)}
|
|
196
|
+
</div>
|
|
197
|
+
)}
|
|
198
|
+
</div>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function FieldNodeGraphInner({
|
|
203
|
+
fieldPath,
|
|
204
|
+
fieldType,
|
|
205
|
+
fieldDescription,
|
|
206
|
+
fieldRequired,
|
|
207
|
+
fieldConflicts,
|
|
208
|
+
occurrences,
|
|
209
|
+
onClose,
|
|
210
|
+
}: FieldNodeGraphProps) {
|
|
211
|
+
const { nodes: initialNodes, edges: initialEdges } = useMemo(
|
|
212
|
+
() => getNodesAndEdges({ fieldPath, fieldType, occurrences }),
|
|
213
|
+
[fieldPath, fieldType, occurrences]
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
const [nodes, , onNodesChange] = useNodesState(initialNodes);
|
|
217
|
+
const [edges, , onEdgesChange] = useEdgesState(initialEdges);
|
|
218
|
+
const edgesRef = useRef(initialEdges);
|
|
219
|
+
edgesRef.current = edges;
|
|
220
|
+
|
|
221
|
+
const { fitView } = useReactFlow();
|
|
222
|
+
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
223
|
+
|
|
224
|
+
// Load visualiser styles (animations, hover effects, theme variables)
|
|
225
|
+
useEffect(() => {
|
|
226
|
+
import('@eventcatalog/visualiser/styles-core.css');
|
|
227
|
+
}, []);
|
|
228
|
+
|
|
229
|
+
useEffect(() => {
|
|
230
|
+
const timer = setTimeout(() => fitView({ padding: 0.2 }), 50);
|
|
231
|
+
return () => clearTimeout(timer);
|
|
232
|
+
}, [fitView]);
|
|
233
|
+
|
|
234
|
+
// Unique messages, producers, consumers for the details panel
|
|
235
|
+
const uniqueMessages = useMemo(() => {
|
|
236
|
+
const seen = new Set<string>();
|
|
237
|
+
return occurrences.filter((occ) => {
|
|
238
|
+
const key = `${occ.messageId}-${occ.messageVersion}`;
|
|
239
|
+
if (seen.has(key)) return false;
|
|
240
|
+
seen.add(key);
|
|
241
|
+
return true;
|
|
242
|
+
});
|
|
243
|
+
}, [occurrences]);
|
|
244
|
+
|
|
245
|
+
const uniqueProducers = useMemo(() => {
|
|
246
|
+
const seen = new Set<string>();
|
|
247
|
+
const result: { id: string; version: string; name?: string }[] = [];
|
|
248
|
+
for (const occ of occurrences) {
|
|
249
|
+
for (const p of occ.producers) {
|
|
250
|
+
const key = `${p.id}-${p.version}`;
|
|
251
|
+
if (!seen.has(key)) {
|
|
252
|
+
seen.add(key);
|
|
253
|
+
result.push(p);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return result;
|
|
258
|
+
}, [occurrences]);
|
|
259
|
+
|
|
260
|
+
const uniqueConsumers = useMemo(() => {
|
|
261
|
+
const seen = new Set<string>();
|
|
262
|
+
const result: { id: string; version: string; name?: string }[] = [];
|
|
263
|
+
for (const occ of occurrences) {
|
|
264
|
+
for (const c of occ.consumers) {
|
|
265
|
+
const key = `${c.id}-${c.version}`;
|
|
266
|
+
if (!seen.has(key)) {
|
|
267
|
+
seen.add(key);
|
|
268
|
+
result.push(c);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return result;
|
|
273
|
+
}, [occurrences]);
|
|
274
|
+
|
|
275
|
+
// ── Hover highlighting (matches main NodeGraph behaviour) ──
|
|
276
|
+
|
|
277
|
+
const hoveredEdgeNodesRef = useRef<Element[]>([]);
|
|
278
|
+
const hoveredNodeEdgesRef = useRef<Element[]>([]);
|
|
279
|
+
const hoveredNodePeersRef = useRef<Element[]>([]);
|
|
280
|
+
|
|
281
|
+
const handleEdgeMouseEnter = useCallback((_: React.MouseEvent, edge: Edge) => {
|
|
282
|
+
const wrapper = wrapperRef.current;
|
|
283
|
+
if (!wrapper) return;
|
|
284
|
+
const nodeEls = wrapper.querySelectorAll(`[data-id="${edge.source}"], [data-id="${edge.target}"]`);
|
|
285
|
+
nodeEls.forEach((el) => el.classList.add('ec-edge-hover-node'));
|
|
286
|
+
hoveredEdgeNodesRef.current = Array.from(nodeEls);
|
|
287
|
+
}, []);
|
|
288
|
+
|
|
289
|
+
const handleEdgeMouseLeave = useCallback(() => {
|
|
290
|
+
hoveredEdgeNodesRef.current.forEach((el) => el.classList.remove('ec-edge-hover-node'));
|
|
291
|
+
hoveredEdgeNodesRef.current = [];
|
|
292
|
+
}, []);
|
|
293
|
+
|
|
294
|
+
const handleNodeMouseEnter = useCallback((_: React.MouseEvent, node: Node) => {
|
|
295
|
+
const wrapper = wrapperRef.current;
|
|
296
|
+
if (!wrapper) return;
|
|
297
|
+
|
|
298
|
+
const peerIds = new Set<string>();
|
|
299
|
+
const edgeEls: Element[] = [];
|
|
300
|
+
|
|
301
|
+
for (const edge of edgesRef.current) {
|
|
302
|
+
if (edge.source !== node.id && edge.target !== node.id) continue;
|
|
303
|
+
const el = wrapper.querySelector(`.react-flow__edge[data-id="${edge.id}"]`);
|
|
304
|
+
if (el) {
|
|
305
|
+
el.classList.add('ec-node-hover-edge');
|
|
306
|
+
edgeEls.push(el);
|
|
307
|
+
}
|
|
308
|
+
if (edge.source !== node.id) peerIds.add(edge.source);
|
|
309
|
+
if (edge.target !== node.id) peerIds.add(edge.target);
|
|
310
|
+
}
|
|
311
|
+
hoveredNodeEdgesRef.current = edgeEls;
|
|
312
|
+
|
|
313
|
+
peerIds.add(node.id);
|
|
314
|
+
const selector = Array.from(peerIds)
|
|
315
|
+
.map((id) => `[data-id="${id}"]`)
|
|
316
|
+
.join(', ');
|
|
317
|
+
if (selector) {
|
|
318
|
+
const peerEls = wrapper.querySelectorAll(selector);
|
|
319
|
+
peerEls.forEach((el) => el.classList.add('ec-edge-hover-node'));
|
|
320
|
+
hoveredNodePeersRef.current = Array.from(peerEls);
|
|
321
|
+
}
|
|
322
|
+
}, []);
|
|
323
|
+
|
|
324
|
+
const handleNodeMouseLeave = useCallback(() => {
|
|
325
|
+
hoveredNodeEdgesRef.current.forEach((el) => el.classList.remove('ec-node-hover-edge'));
|
|
326
|
+
hoveredNodeEdgesRef.current = [];
|
|
327
|
+
hoveredNodePeersRef.current.forEach((el) => el.classList.remove('ec-edge-hover-node'));
|
|
328
|
+
hoveredNodePeersRef.current = [];
|
|
329
|
+
}, []);
|
|
330
|
+
|
|
331
|
+
return (
|
|
332
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={onClose}>
|
|
333
|
+
<div
|
|
334
|
+
className="w-[90vw] h-[80vh] max-w-[1400px] rounded-xl border shadow-2xl flex flex-col overflow-hidden"
|
|
335
|
+
style={{
|
|
336
|
+
backgroundColor: 'rgb(var(--ec-page-bg))',
|
|
337
|
+
borderColor: 'rgb(var(--ec-page-border))',
|
|
338
|
+
}}
|
|
339
|
+
onClick={(e) => e.stopPropagation()}
|
|
340
|
+
>
|
|
341
|
+
{/* Header */}
|
|
342
|
+
<div
|
|
343
|
+
className="flex items-center justify-between px-5 py-3 border-b flex-shrink-0"
|
|
344
|
+
style={{ borderColor: 'rgb(var(--ec-page-border))' }}
|
|
345
|
+
>
|
|
346
|
+
<div className="flex items-center gap-2">
|
|
347
|
+
<h3 className="text-sm font-semibold" style={{ color: 'rgb(var(--ec-page-text))' }}>
|
|
348
|
+
Field Traceability
|
|
349
|
+
</h3>
|
|
350
|
+
<code
|
|
351
|
+
className="px-2 py-0.5 rounded text-xs font-mono"
|
|
352
|
+
style={{
|
|
353
|
+
backgroundColor: 'rgb(var(--ec-accent) / 0.1)',
|
|
354
|
+
color: 'rgb(var(--ec-accent))',
|
|
355
|
+
}}
|
|
356
|
+
>
|
|
357
|
+
{fieldPath}
|
|
358
|
+
</code>
|
|
359
|
+
</div>
|
|
360
|
+
<button
|
|
361
|
+
onClick={onClose}
|
|
362
|
+
className="p-1.5 rounded-lg transition-colors hover:bg-[rgb(var(--ec-content-hover))] flex-shrink-0"
|
|
363
|
+
style={{ color: 'rgb(var(--ec-icon-color))' }}
|
|
364
|
+
title="Close"
|
|
365
|
+
>
|
|
366
|
+
<X className="w-4 h-4" />
|
|
367
|
+
</button>
|
|
368
|
+
</div>
|
|
369
|
+
|
|
370
|
+
{/* Body: Graph + Details panel */}
|
|
371
|
+
<div className="flex-1 min-h-0 flex">
|
|
372
|
+
{/* Node Graph */}
|
|
373
|
+
<div className="flex-1 min-w-0 eventcatalog-visualizer" ref={wrapperRef}>
|
|
374
|
+
<ReactFlow
|
|
375
|
+
nodes={nodes}
|
|
376
|
+
edges={edges}
|
|
377
|
+
onNodesChange={onNodesChange}
|
|
378
|
+
onEdgesChange={onEdgesChange}
|
|
379
|
+
nodeTypes={nodeTypes}
|
|
380
|
+
onEdgeMouseEnter={handleEdgeMouseEnter}
|
|
381
|
+
onEdgeMouseLeave={handleEdgeMouseLeave}
|
|
382
|
+
onNodeMouseEnter={handleNodeMouseEnter}
|
|
383
|
+
onNodeMouseLeave={handleNodeMouseLeave}
|
|
384
|
+
defaultEdgeOptions={{ type: 'smoothstep' }}
|
|
385
|
+
connectionLineType={ConnectionLineType.SmoothStep}
|
|
386
|
+
fitView
|
|
387
|
+
proOptions={{ hideAttribution: true }}
|
|
388
|
+
nodesDraggable={true}
|
|
389
|
+
nodesConnectable={false}
|
|
390
|
+
elementsSelectable={false}
|
|
391
|
+
minZoom={0.07}
|
|
392
|
+
maxZoom={1.5}
|
|
393
|
+
>
|
|
394
|
+
<Background color="var(--ec-bg-dots)" gap={16} />
|
|
395
|
+
<Controls position="bottom-left" />
|
|
396
|
+
</ReactFlow>
|
|
397
|
+
</div>
|
|
398
|
+
|
|
399
|
+
{/* Right details panel */}
|
|
400
|
+
<div className="w-[280px] flex-shrink-0 border-l overflow-y-auto" style={{ borderColor: 'rgb(var(--ec-page-border))' }}>
|
|
401
|
+
<div className="p-4 space-y-4">
|
|
402
|
+
{/* Field info */}
|
|
403
|
+
<div>
|
|
404
|
+
<h4 className="text-[11px] font-medium text-[rgb(var(--ec-page-text-muted))] uppercase tracking-wider mb-2">
|
|
405
|
+
Field
|
|
406
|
+
</h4>
|
|
407
|
+
<div className="space-y-1.5">
|
|
408
|
+
<div className="text-sm font-mono font-semibold text-[rgb(var(--ec-page-text))]">{fieldPath}</div>
|
|
409
|
+
<div className="text-xs text-[rgb(var(--ec-page-text-muted))]">{fieldType}</div>
|
|
410
|
+
{fieldRequired && (
|
|
411
|
+
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-semibold bg-red-500/10 text-red-500 border border-red-500/20">
|
|
412
|
+
Required
|
|
413
|
+
</span>
|
|
414
|
+
)}
|
|
415
|
+
{fieldDescription && (
|
|
416
|
+
<p className="text-xs text-[rgb(var(--ec-page-text-muted))] leading-relaxed mt-1">{fieldDescription}</p>
|
|
417
|
+
)}
|
|
418
|
+
</div>
|
|
419
|
+
</div>
|
|
420
|
+
|
|
421
|
+
{/* Conflicts */}
|
|
422
|
+
{fieldConflicts && fieldConflicts.length > 1 && (
|
|
423
|
+
<div>
|
|
424
|
+
<h4 className="text-[11px] font-medium text-amber-500 uppercase tracking-wider mb-2 flex items-center gap-1">
|
|
425
|
+
<AlertTriangle className="w-3 h-3" />
|
|
426
|
+
Type Conflict
|
|
427
|
+
</h4>
|
|
428
|
+
<div className="rounded-lg border border-amber-500/20 bg-amber-500/5 p-2.5">
|
|
429
|
+
<p className="text-xs text-amber-600 mb-2">
|
|
430
|
+
This field has inconsistent types across messages (
|
|
431
|
+
{fieldConflicts.map((c) => `${c.type} in ${c.count}`).join(', ')})
|
|
432
|
+
</p>
|
|
433
|
+
<div className="space-y-1">
|
|
434
|
+
{fieldConflicts.map((c) => (
|
|
435
|
+
<div key={c.type} className="flex items-center justify-between text-xs">
|
|
436
|
+
<code className="font-mono text-[rgb(var(--ec-page-text))]">{c.type}</code>
|
|
437
|
+
<span className="text-[rgb(var(--ec-page-text-muted))]">
|
|
438
|
+
{c.count} {c.count === 1 ? 'schema' : 'schemas'}
|
|
439
|
+
</span>
|
|
440
|
+
</div>
|
|
441
|
+
))}
|
|
442
|
+
</div>
|
|
443
|
+
</div>
|
|
444
|
+
</div>
|
|
445
|
+
)}
|
|
446
|
+
|
|
447
|
+
{/* Messages */}
|
|
448
|
+
<CollapsibleResourceList
|
|
449
|
+
title="Messages"
|
|
450
|
+
count={uniqueMessages.length}
|
|
451
|
+
defaultExpanded={true}
|
|
452
|
+
items={uniqueMessages.map((occ) => {
|
|
453
|
+
const { Icon, bg } = getIconForMessageType(occ.messageType);
|
|
454
|
+
return {
|
|
455
|
+
key: `${occ.messageId}-${occ.messageVersion}`,
|
|
456
|
+
label: occ.messageName || occ.messageId,
|
|
457
|
+
version: occ.messageVersion,
|
|
458
|
+
Icon,
|
|
459
|
+
bg,
|
|
460
|
+
onClick: () => {
|
|
461
|
+
const nodeId = `msg-${occ.messageId}-${occ.messageVersion}`;
|
|
462
|
+
fitView({ nodes: [{ id: nodeId }], duration: 300, padding: 0.5 });
|
|
463
|
+
},
|
|
464
|
+
};
|
|
465
|
+
})}
|
|
466
|
+
/>
|
|
467
|
+
|
|
468
|
+
{/* Producers */}
|
|
469
|
+
{uniqueProducers.length > 0 && (
|
|
470
|
+
<CollapsibleResourceList
|
|
471
|
+
title="Producers"
|
|
472
|
+
count={uniqueProducers.length}
|
|
473
|
+
defaultExpanded={false}
|
|
474
|
+
items={uniqueProducers.map((p) => ({
|
|
475
|
+
key: `${p.id}-${p.version}`,
|
|
476
|
+
label: p.name || p.id,
|
|
477
|
+
version: p.version,
|
|
478
|
+
Icon: ServerIcon,
|
|
479
|
+
bg: 'bg-pink-500',
|
|
480
|
+
onClick: () => {
|
|
481
|
+
const nodeId = `svc-producer-${p.id}-${p.version}`;
|
|
482
|
+
fitView({ nodes: [{ id: nodeId }], duration: 300, padding: 0.5 });
|
|
483
|
+
},
|
|
484
|
+
}))}
|
|
485
|
+
/>
|
|
486
|
+
)}
|
|
487
|
+
|
|
488
|
+
{/* Consumers */}
|
|
489
|
+
{uniqueConsumers.length > 0 && (
|
|
490
|
+
<CollapsibleResourceList
|
|
491
|
+
title="Consumers"
|
|
492
|
+
count={uniqueConsumers.length}
|
|
493
|
+
defaultExpanded={false}
|
|
494
|
+
items={uniqueConsumers.map((c) => ({
|
|
495
|
+
key: `${c.id}-${c.version}`,
|
|
496
|
+
label: c.name || c.id,
|
|
497
|
+
version: c.version,
|
|
498
|
+
Icon: ServerIcon,
|
|
499
|
+
bg: 'bg-pink-500',
|
|
500
|
+
onClick: () => {
|
|
501
|
+
const nodeId = `svc-consumer-${c.id}-${c.version}`;
|
|
502
|
+
fitView({ nodes: [{ id: nodeId }], duration: 300, padding: 0.5 });
|
|
503
|
+
},
|
|
504
|
+
}))}
|
|
505
|
+
/>
|
|
506
|
+
)}
|
|
507
|
+
</div>
|
|
508
|
+
</div>
|
|
509
|
+
</div>
|
|
510
|
+
</div>
|
|
511
|
+
</div>
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
export default function FieldNodeGraph(props: FieldNodeGraphProps) {
|
|
516
|
+
return (
|
|
517
|
+
<ReactFlowProvider>
|
|
518
|
+
<FieldNodeGraphInner {...props} />
|
|
519
|
+
</ReactFlowProvider>
|
|
520
|
+
);
|
|
521
|
+
}
|