@eventcatalog/core 2.44.3 → 2.44.4
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-SHFEPDUF.js → chunk-5JM4QIHQ.js} +1 -1
- package/dist/{chunk-CGPQJVQ3.js → chunk-DAC5BKLG.js} +1 -1
- package/dist/{chunk-EJOD6TNE.js → chunk-DFFXN3Z4.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/src/components/MDX/NodeGraph/NodeGraph.tsx +113 -0
- package/eventcatalog/src/components/MDX/NodeGraph/StepWalkthrough.tsx +296 -0
- package/eventcatalog/src/components/SideNav/ListViewSideBar/components/MessageList.tsx +27 -2
- package/eventcatalog/src/components/SideNav/ListViewSideBar/index.tsx +64 -14
- package/package.json +1 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
log_build_default
|
|
3
|
-
} from "../chunk-
|
|
4
|
-
import "../chunk-
|
|
5
|
-
import "../chunk-
|
|
3
|
+
} from "../chunk-DAC5BKLG.js";
|
|
4
|
+
import "../chunk-5JM4QIHQ.js";
|
|
5
|
+
import "../chunk-DFFXN3Z4.js";
|
|
6
6
|
import "../chunk-E7TXTI7G.js";
|
|
7
7
|
export {
|
|
8
8
|
log_build_default as default
|
package/dist/constants.cjs
CHANGED
package/dist/constants.js
CHANGED
package/dist/eventcatalog.cjs
CHANGED
package/dist/eventcatalog.js
CHANGED
|
@@ -6,8 +6,8 @@ import {
|
|
|
6
6
|
} from "./chunk-DCLTVJDP.js";
|
|
7
7
|
import {
|
|
8
8
|
log_build_default
|
|
9
|
-
} from "./chunk-
|
|
10
|
-
import "./chunk-
|
|
9
|
+
} from "./chunk-DAC5BKLG.js";
|
|
10
|
+
import "./chunk-5JM4QIHQ.js";
|
|
11
11
|
import {
|
|
12
12
|
catalogToAstro,
|
|
13
13
|
checkAndConvertMdToMdx
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
import "./chunk-EXAALOQA.js";
|
|
16
16
|
import {
|
|
17
17
|
VERSION
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-DFFXN3Z4.js";
|
|
19
19
|
import {
|
|
20
20
|
isAuthEnabled,
|
|
21
21
|
isBackstagePluginEnabled,
|
|
@@ -40,6 +40,7 @@ import ChannelNode from './Nodes/Channel';
|
|
|
40
40
|
import { CogIcon } from '@heroicons/react/20/solid';
|
|
41
41
|
import { useEventCatalogVisualiser } from 'src/hooks/eventcatalog-visualizer';
|
|
42
42
|
import VisualiserSearch, { type VisualiserSearchRef } from './VisualiserSearch';
|
|
43
|
+
import StepWalkthrough from './StepWalkthrough';
|
|
43
44
|
interface Props {
|
|
44
45
|
nodes: any;
|
|
45
46
|
edges: any;
|
|
@@ -97,6 +98,7 @@ const NodeGraphBuilder = ({
|
|
|
97
98
|
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
|
98
99
|
const [isAnimated, setIsAnimated] = useState(false);
|
|
99
100
|
const [animateMessages, setAnimateMessages] = useState(false);
|
|
101
|
+
const [activeStepIndex, setActiveStepIndex] = useState<number | null>(null);
|
|
100
102
|
const { hideChannels, toggleChannelsVisibility } = useEventCatalogVisualiser({ nodes, edges, setNodes, setEdges });
|
|
101
103
|
const { fitView, getNodes } = useReactFlow();
|
|
102
104
|
const searchRef = useRef<VisualiserSearchRef>(null);
|
|
@@ -343,6 +345,106 @@ const NodeGraphBuilder = ({
|
|
|
343
345
|
|
|
344
346
|
const legend = getNodesByCollectionWithColors(nodes);
|
|
345
347
|
|
|
348
|
+
const handleStepChange = useCallback(
|
|
349
|
+
(nodeId: string | null, highlightPaths?: string[], shouldZoomOut?: boolean) => {
|
|
350
|
+
if (nodeId === null) {
|
|
351
|
+
// Reset all nodes and edges
|
|
352
|
+
resetNodesAndEdges();
|
|
353
|
+
setActiveStepIndex(null);
|
|
354
|
+
|
|
355
|
+
// If shouldZoomOut is true, fit the entire view
|
|
356
|
+
if (shouldZoomOut) {
|
|
357
|
+
setTimeout(() => {
|
|
358
|
+
fitView({ duration: 800, padding: 0.1 });
|
|
359
|
+
}, 100);
|
|
360
|
+
}
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const activeNode = nodes.find((node: Node) => node.id === nodeId);
|
|
365
|
+
if (!activeNode) return;
|
|
366
|
+
|
|
367
|
+
// Create set of highlighted nodes and edges
|
|
368
|
+
const highlightedNodeIds = new Set<string>();
|
|
369
|
+
const highlightedEdgeIds = new Set<string>();
|
|
370
|
+
|
|
371
|
+
// Add current node
|
|
372
|
+
highlightedNodeIds.add(activeNode.id);
|
|
373
|
+
|
|
374
|
+
// Add incoming edges and their source nodes
|
|
375
|
+
edges.forEach((edge: Edge) => {
|
|
376
|
+
if (edge.target === activeNode.id) {
|
|
377
|
+
highlightedEdgeIds.add(edge.id);
|
|
378
|
+
highlightedNodeIds.add(edge.source);
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// Add outgoing edges
|
|
383
|
+
if (highlightPaths) {
|
|
384
|
+
// Highlight all possible paths when at a fork
|
|
385
|
+
highlightPaths.forEach((pathId) => {
|
|
386
|
+
const [source, target] = pathId.split('-');
|
|
387
|
+
edges.forEach((edge: Edge) => {
|
|
388
|
+
if (edge.source === source && edge.target === target) {
|
|
389
|
+
highlightedEdgeIds.add(edge.id);
|
|
390
|
+
highlightedNodeIds.add(edge.target);
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
} else {
|
|
395
|
+
// Highlight all outgoing edges normally
|
|
396
|
+
edges.forEach((edge: Edge) => {
|
|
397
|
+
if (edge.source === activeNode.id) {
|
|
398
|
+
highlightedEdgeIds.add(edge.id);
|
|
399
|
+
highlightedNodeIds.add(edge.target);
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Update nodes
|
|
405
|
+
const updatedNodes = nodes.map((node: Node) => {
|
|
406
|
+
if (highlightedNodeIds.has(node.id)) {
|
|
407
|
+
return { ...node, style: { ...node.style, opacity: 1 } };
|
|
408
|
+
}
|
|
409
|
+
return { ...node, style: { ...node.style, opacity: 0.2 } };
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// Update edges
|
|
413
|
+
const updatedEdges = edges.map((edge: Edge) => {
|
|
414
|
+
if (highlightedEdgeIds.has(edge.id)) {
|
|
415
|
+
return {
|
|
416
|
+
...edge,
|
|
417
|
+
data: { ...edge.data, opacity: 1, animated: true },
|
|
418
|
+
style: { ...edge.style, opacity: 1, strokeWidth: 3 },
|
|
419
|
+
labelStyle: { ...edge.labelStyle, opacity: 1 },
|
|
420
|
+
animated: true,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
return {
|
|
424
|
+
...edge,
|
|
425
|
+
data: { ...edge.data, opacity: 0.2, animated: false },
|
|
426
|
+
style: { ...edge.style, opacity: 0.2, strokeWidth: 2 },
|
|
427
|
+
labelStyle: { ...edge.labelStyle, opacity: 0.2 },
|
|
428
|
+
animated: false,
|
|
429
|
+
};
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
setNodes(updatedNodes);
|
|
433
|
+
setEdges(updatedEdges);
|
|
434
|
+
|
|
435
|
+
// Fit view to active node
|
|
436
|
+
fitView({
|
|
437
|
+
padding: 0.4,
|
|
438
|
+
duration: 800,
|
|
439
|
+
nodes: [activeNode],
|
|
440
|
+
});
|
|
441
|
+
},
|
|
442
|
+
[nodes, edges, setNodes, setEdges, resetNodesAndEdges, fitView]
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
// Check if this is a flow visualization by checking if edges use flow-edge type
|
|
446
|
+
const isFlowVisualization = edges.some((edge: Edge) => edge.type === 'flow-edge');
|
|
447
|
+
|
|
346
448
|
return (
|
|
347
449
|
<ReactFlow
|
|
348
450
|
nodeTypes={nodeTypes}
|
|
@@ -471,6 +573,17 @@ const NodeGraphBuilder = ({
|
|
|
471
573
|
)}
|
|
472
574
|
{includeBackground && <Background color="#bbb" gap={16} />}
|
|
473
575
|
{includeBackground && <Controls />}
|
|
576
|
+
{isFlowVisualization && (
|
|
577
|
+
<Panel position="bottom-left">
|
|
578
|
+
<StepWalkthrough
|
|
579
|
+
nodes={nodes}
|
|
580
|
+
edges={edges}
|
|
581
|
+
isFlowVisualization={isFlowVisualization}
|
|
582
|
+
onStepChange={handleStepChange}
|
|
583
|
+
mode={mode}
|
|
584
|
+
/>
|
|
585
|
+
</Panel>
|
|
586
|
+
)}
|
|
474
587
|
{includeKey && (
|
|
475
588
|
<Panel position="bottom-right">
|
|
476
589
|
<div className=" bg-white font-light px-4 text-[12px] shadow-md py-1 rounded-md">
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import React, { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/24/outline';
|
|
3
|
+
import type { Node, Edge } from '@xyflow/react';
|
|
4
|
+
|
|
5
|
+
interface NodeData {
|
|
6
|
+
step?: {
|
|
7
|
+
title?: string;
|
|
8
|
+
summary?: string;
|
|
9
|
+
};
|
|
10
|
+
service?: {
|
|
11
|
+
data?: {
|
|
12
|
+
name?: string;
|
|
13
|
+
summary?: string;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
message?: {
|
|
17
|
+
data?: {
|
|
18
|
+
name?: string;
|
|
19
|
+
summary?: string;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
flow?: {
|
|
23
|
+
data?: {
|
|
24
|
+
name?: string;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
custom?: {
|
|
28
|
+
title?: string;
|
|
29
|
+
label?: string;
|
|
30
|
+
summary?: string;
|
|
31
|
+
};
|
|
32
|
+
actor?: {
|
|
33
|
+
label?: string;
|
|
34
|
+
};
|
|
35
|
+
externalSystem?: {
|
|
36
|
+
label?: string;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface CustomNode {
|
|
41
|
+
id: string;
|
|
42
|
+
data: NodeData;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface StepWalkthroughProps {
|
|
46
|
+
nodes: CustomNode[];
|
|
47
|
+
edges: Edge[];
|
|
48
|
+
isFlowVisualization: boolean;
|
|
49
|
+
onStepChange: (nodeId: string | null, highlightPaths?: string[], shouldZoomOut?: boolean) => void;
|
|
50
|
+
mode?: 'full' | 'simple';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface PathOption {
|
|
54
|
+
targetId: string;
|
|
55
|
+
label?: string;
|
|
56
|
+
targetNode: CustomNode;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export default function StepWalkthrough({
|
|
60
|
+
nodes,
|
|
61
|
+
edges,
|
|
62
|
+
isFlowVisualization,
|
|
63
|
+
onStepChange,
|
|
64
|
+
mode = 'full',
|
|
65
|
+
}: StepWalkthroughProps) {
|
|
66
|
+
const [currentNodeId, setCurrentNodeId] = useState<string | null>(null);
|
|
67
|
+
const [pathHistory, setPathHistory] = useState<string[]>([]);
|
|
68
|
+
const [currentStepIndex, setCurrentStepIndex] = useState<number>(-1); // -1 means not started
|
|
69
|
+
const [availablePaths, setAvailablePaths] = useState<PathOption[]>([]);
|
|
70
|
+
const [selectedPathIndex, setSelectedPathIndex] = useState<number>(0);
|
|
71
|
+
const [startNodeId, setStartNodeId] = useState<string | null>(null);
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (isFlowVisualization && nodes.length > 0) {
|
|
75
|
+
// Find the starting node (node with no incoming edges)
|
|
76
|
+
const incomingEdgeMap = new Map<string, number>();
|
|
77
|
+
nodes.forEach((node: CustomNode) => incomingEdgeMap.set(node.id, 0));
|
|
78
|
+
|
|
79
|
+
edges.forEach((edge: Edge) => {
|
|
80
|
+
if (incomingEdgeMap.has(edge.target)) {
|
|
81
|
+
incomingEdgeMap.set(edge.target, (incomingEdgeMap.get(edge.target) || 0) + 1);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const startNodes = nodes.filter((node: CustomNode) => incomingEdgeMap.get(node.id) === 0);
|
|
86
|
+
if (startNodes.length > 0 && !startNodeId) {
|
|
87
|
+
const firstStartNode = startNodes[0];
|
|
88
|
+
setStartNodeId(firstStartNode.id);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}, [nodes, edges, isFlowVisualization, startNodeId]);
|
|
92
|
+
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (currentNodeId) {
|
|
95
|
+
// Find available paths from current node
|
|
96
|
+
const outgoingEdges = edges.filter((edge: Edge) => edge.source === currentNodeId);
|
|
97
|
+
const paths: PathOption[] = outgoingEdges.map((edge: Edge) => {
|
|
98
|
+
const targetNode = nodes.find((n: CustomNode) => n.id === edge.target);
|
|
99
|
+
return {
|
|
100
|
+
targetId: edge.target,
|
|
101
|
+
label: edge.label as string | undefined,
|
|
102
|
+
targetNode: targetNode!,
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
setAvailablePaths(paths);
|
|
106
|
+
setSelectedPathIndex(0);
|
|
107
|
+
} else {
|
|
108
|
+
setAvailablePaths([]);
|
|
109
|
+
}
|
|
110
|
+
}, [currentNodeId, nodes, edges]);
|
|
111
|
+
|
|
112
|
+
const handleNextStep = useCallback(() => {
|
|
113
|
+
if (currentStepIndex === -1) {
|
|
114
|
+
// Start the walkthrough
|
|
115
|
+
if (startNodeId) {
|
|
116
|
+
setPathHistory([startNodeId]);
|
|
117
|
+
setCurrentNodeId(startNodeId);
|
|
118
|
+
setCurrentStepIndex(0);
|
|
119
|
+
onStepChange(startNodeId);
|
|
120
|
+
}
|
|
121
|
+
} else if (availablePaths.length > 0) {
|
|
122
|
+
// Move to the selected path
|
|
123
|
+
const selectedPath = availablePaths[selectedPathIndex];
|
|
124
|
+
const newHistory = [...pathHistory, selectedPath.targetId];
|
|
125
|
+
setPathHistory(newHistory);
|
|
126
|
+
setCurrentNodeId(selectedPath.targetId);
|
|
127
|
+
setCurrentStepIndex((prev) => prev + 1);
|
|
128
|
+
|
|
129
|
+
// Highlight the selected path
|
|
130
|
+
const allPaths = availablePaths.map((p) => `${currentNodeId}-${p.targetId}`);
|
|
131
|
+
onStepChange(selectedPath.targetId, allPaths);
|
|
132
|
+
}
|
|
133
|
+
}, [currentStepIndex, startNodeId, availablePaths, selectedPathIndex, currentNodeId, onStepChange]);
|
|
134
|
+
|
|
135
|
+
const handlePreviousStep = useCallback(() => {
|
|
136
|
+
if (currentStepIndex > 0) {
|
|
137
|
+
// Go back to previous step
|
|
138
|
+
const newIndex = currentStepIndex - 1;
|
|
139
|
+
const prevNodeId = pathHistory[newIndex];
|
|
140
|
+
setCurrentNodeId(prevNodeId);
|
|
141
|
+
setCurrentStepIndex(newIndex);
|
|
142
|
+
onStepChange(prevNodeId);
|
|
143
|
+
} else if (currentStepIndex === 0) {
|
|
144
|
+
// Go back to the start (no selection)
|
|
145
|
+
setCurrentNodeId(null);
|
|
146
|
+
setCurrentStepIndex(-1);
|
|
147
|
+
onStepChange(null);
|
|
148
|
+
}
|
|
149
|
+
}, [currentStepIndex, pathHistory, onStepChange]);
|
|
150
|
+
|
|
151
|
+
const handlePathSelection = useCallback((index: number) => {
|
|
152
|
+
setSelectedPathIndex(index);
|
|
153
|
+
}, []);
|
|
154
|
+
|
|
155
|
+
const handleFinish = useCallback(() => {
|
|
156
|
+
setCurrentNodeId(null);
|
|
157
|
+
setCurrentStepIndex(-1);
|
|
158
|
+
setPathHistory([]);
|
|
159
|
+
onStepChange(null, [], true); // Pass true to indicate full reset with zoom out
|
|
160
|
+
}, [onStepChange]);
|
|
161
|
+
|
|
162
|
+
if (!isFlowVisualization || nodes.length === 0 || mode !== 'full') {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const getCurrentStepInfo = () => {
|
|
167
|
+
if (currentStepIndex === -1) {
|
|
168
|
+
return { title: 'Walk through business flow', description: 'Step through the flow to understand the business process' };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const currentNode = nodes.find((n: CustomNode) => n.id === currentNodeId);
|
|
172
|
+
if (!currentNode) return { title: 'Unknown step', description: '' };
|
|
173
|
+
|
|
174
|
+
let stepNumber = currentStepIndex + 1;
|
|
175
|
+
let title = `Step ${stepNumber}`;
|
|
176
|
+
let description = '';
|
|
177
|
+
|
|
178
|
+
// Get node information based on type - check step data first, then type-specific data
|
|
179
|
+
if (currentNode.data.step?.title) {
|
|
180
|
+
title += `: ${currentNode.data.step.title}`;
|
|
181
|
+
} else if (currentNode.data.service?.data?.name) {
|
|
182
|
+
title += `: ${currentNode.data.service.data.name}`;
|
|
183
|
+
} else if (currentNode.data.message?.data?.name) {
|
|
184
|
+
title += `: ${currentNode.data.message.data.name}`;
|
|
185
|
+
} else if (currentNode.data.flow?.data?.name) {
|
|
186
|
+
title += `: ${currentNode.data.flow.data.name}`;
|
|
187
|
+
} else if (currentNode.data.custom?.title) {
|
|
188
|
+
title += `: ${currentNode.data.custom.title}`;
|
|
189
|
+
} else if (currentNode.data.custom?.label) {
|
|
190
|
+
title += `: ${currentNode.data.custom.label}`;
|
|
191
|
+
} else if (currentNode.data.actor?.label) {
|
|
192
|
+
title += `: ${currentNode.data.actor.label}`;
|
|
193
|
+
} else if (currentNode.data.externalSystem?.label) {
|
|
194
|
+
title += `: ${currentNode.data.externalSystem.label}`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Get description - check step data first, then type-specific data
|
|
198
|
+
if (currentNode.data.step?.summary) {
|
|
199
|
+
description = currentNode.data.step.summary;
|
|
200
|
+
} else if (currentNode.data.service?.data?.summary) {
|
|
201
|
+
description = currentNode.data.service.data.summary;
|
|
202
|
+
} else if (currentNode.data.message?.data?.summary) {
|
|
203
|
+
description = currentNode.data.message.data.summary;
|
|
204
|
+
} else if (currentNode.data.custom?.summary) {
|
|
205
|
+
description = currentNode.data.custom.summary;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return { title, description };
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const { title, description } = getCurrentStepInfo();
|
|
212
|
+
|
|
213
|
+
return (
|
|
214
|
+
<div className="ml-8 bg-white rounded-lg shadow-sm p-[8px] z-30 border border-gray-200 w-[350px]">
|
|
215
|
+
<div className="mb-3">
|
|
216
|
+
<h3 className="text-sm font-semibold text-gray-900">{title}</h3>
|
|
217
|
+
{description && <p className="text-xs text-gray-600 mt-1">{description}</p>}
|
|
218
|
+
</div>
|
|
219
|
+
|
|
220
|
+
{/* Show path options when there are multiple paths */}
|
|
221
|
+
{currentNodeId && availablePaths.length > 1 && (
|
|
222
|
+
<div className="mb-3">
|
|
223
|
+
<label className="block text-xs font-medium text-gray-700 mb-2">Choose next path:</label>
|
|
224
|
+
<select
|
|
225
|
+
value={selectedPathIndex}
|
|
226
|
+
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => handlePathSelection(parseInt(e.target.value))}
|
|
227
|
+
className="w-full px-3 py-2 text-xs border border-gray-300 rounded-md bg-white focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
|
228
|
+
>
|
|
229
|
+
{availablePaths.map((path, index) => {
|
|
230
|
+
// @ts-ignore
|
|
231
|
+
const nodeLabel =
|
|
232
|
+
path.targetNode.data.step?.title ||
|
|
233
|
+
(path.targetNode.data as any).service?.data?.name ||
|
|
234
|
+
(path.targetNode.data as any).message?.data?.name ||
|
|
235
|
+
(path.targetNode.data as any).flow?.data?.name ||
|
|
236
|
+
(path.targetNode.data as any).custom?.title ||
|
|
237
|
+
(path.targetNode.data as any).custom?.label ||
|
|
238
|
+
(path.targetNode.data as any).actor?.label ||
|
|
239
|
+
(path.targetNode.data as any).externalSystem?.label ||
|
|
240
|
+
'Unknown';
|
|
241
|
+
|
|
242
|
+
return (
|
|
243
|
+
<option key={path.targetId} value={index}>
|
|
244
|
+
{path.label ? `${path.label}: ${nodeLabel}` : nodeLabel}
|
|
245
|
+
</option>
|
|
246
|
+
);
|
|
247
|
+
})}
|
|
248
|
+
</select>
|
|
249
|
+
</div>
|
|
250
|
+
)}
|
|
251
|
+
|
|
252
|
+
<div className="flex items-center justify-between">
|
|
253
|
+
{currentStepIndex === -1 ? (
|
|
254
|
+
// Initial state - show only Start button on the right
|
|
255
|
+
<>
|
|
256
|
+
<div className="flex-1"></div>
|
|
257
|
+
<button
|
|
258
|
+
onClick={handleNextStep}
|
|
259
|
+
className="flex items-center justify-center px-6 py-2 text-xs font-medium bg-purple-600 text-white rounded-md hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 transition-colors"
|
|
260
|
+
>
|
|
261
|
+
Start
|
|
262
|
+
</button>
|
|
263
|
+
</>
|
|
264
|
+
) : (
|
|
265
|
+
// In walkthrough - show Previous on left, Next on right (only if paths available)
|
|
266
|
+
<>
|
|
267
|
+
<button
|
|
268
|
+
onClick={handlePreviousStep}
|
|
269
|
+
className="flex items-center justify-center px-4 py-2 text-xs font-medium bg-purple-600 text-white rounded-md hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 transition-colors"
|
|
270
|
+
>
|
|
271
|
+
<ChevronLeftIcon className="w-4 h-4 mr-1" />
|
|
272
|
+
Previous
|
|
273
|
+
</button>
|
|
274
|
+
|
|
275
|
+
{availablePaths.length > 0 ? (
|
|
276
|
+
<button
|
|
277
|
+
onClick={handleNextStep}
|
|
278
|
+
className="flex items-center justify-center px-4 py-2 text-xs font-medium bg-purple-600 text-white rounded-md hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 transition-colors"
|
|
279
|
+
>
|
|
280
|
+
Next
|
|
281
|
+
<ChevronRightIcon className="w-4 h-4 ml-1" />
|
|
282
|
+
</button>
|
|
283
|
+
) : (
|
|
284
|
+
<button
|
|
285
|
+
onClick={handleFinish}
|
|
286
|
+
className="flex items-center justify-center px-4 py-2 text-xs font-medium bg-green-600 text-white rounded-md hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 transition-colors"
|
|
287
|
+
>
|
|
288
|
+
Finish
|
|
289
|
+
</button>
|
|
290
|
+
)}
|
|
291
|
+
</>
|
|
292
|
+
)}
|
|
293
|
+
</div>
|
|
294
|
+
</div>
|
|
295
|
+
);
|
|
296
|
+
}
|
|
@@ -1,10 +1,33 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { getMessageColorByCollection, getMessageCollectionName } from '../index';
|
|
3
|
+
|
|
3
4
|
interface MessageListProps {
|
|
4
5
|
messages: any[];
|
|
5
6
|
decodedCurrentPath: string;
|
|
7
|
+
searchTerm?: string;
|
|
6
8
|
}
|
|
7
9
|
|
|
10
|
+
const HighlightedText = React.memo(({ text, searchTerm }: { text: string; searchTerm?: string }) => {
|
|
11
|
+
if (!searchTerm) return <>{text}</>;
|
|
12
|
+
|
|
13
|
+
const regex = new RegExp(`(${searchTerm})`, 'gi');
|
|
14
|
+
const parts = text.split(regex);
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<>
|
|
18
|
+
{parts.map((part, index) =>
|
|
19
|
+
regex.test(part) ? (
|
|
20
|
+
<span key={index} className="bg-yellow-200 text-gray-900 font-semibold">
|
|
21
|
+
{part}
|
|
22
|
+
</span>
|
|
23
|
+
) : (
|
|
24
|
+
<span key={index}>{part}</span>
|
|
25
|
+
)
|
|
26
|
+
)}
|
|
27
|
+
</>
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
|
|
8
31
|
const getMessageColorByLabelOrCollection = (collection: string, badge?: string) => {
|
|
9
32
|
if (!badge) {
|
|
10
33
|
return getMessageColorByCollection(collection);
|
|
@@ -25,7 +48,7 @@ const getMessageColorByLabelOrCollection = (collection: string, badge?: string)
|
|
|
25
48
|
return getMessageColorByCollection(collection);
|
|
26
49
|
};
|
|
27
50
|
|
|
28
|
-
const MessageList: React.FC<MessageListProps> = ({ messages, decodedCurrentPath }) => (
|
|
51
|
+
const MessageList: React.FC<MessageListProps> = ({ messages, decodedCurrentPath, searchTerm }) => (
|
|
29
52
|
<ul className="space-y-0.5 border-l border-gray-200/80 ml-[9px] pl-4">
|
|
30
53
|
{messages.map((message: any) => (
|
|
31
54
|
<li key={message.id} data-active={decodedCurrentPath === message.href}>
|
|
@@ -35,7 +58,9 @@ const MessageList: React.FC<MessageListProps> = ({ messages, decodedCurrentPath
|
|
|
35
58
|
decodedCurrentPath.includes(message.href) ? 'bg-purple-100 ' : 'hover:bg-purple-100'
|
|
36
59
|
}`}
|
|
37
60
|
>
|
|
38
|
-
<span className="truncate">
|
|
61
|
+
<span className="truncate">
|
|
62
|
+
<HighlightedText text={message.data?.sidebar?.label || message.data.name} searchTerm={searchTerm} />
|
|
63
|
+
</span>
|
|
39
64
|
<span
|
|
40
65
|
className={`ml-2 text-[10px] flex items-center gap-1 font-medium px-2 uppercase py-0.5 rounded ${getMessageColorByLabelOrCollection(message.collection, message.data?.sidebar?.badge)}`}
|
|
41
66
|
>
|
|
@@ -8,6 +8,27 @@ import type { MessageItem, ServiceItem, ListViewSideBarProps } from './types';
|
|
|
8
8
|
const STORAGE_KEY = 'EventCatalog:catalogSidebarCollapsedGroups';
|
|
9
9
|
const DEBOUNCE_DELAY = 300; // 300ms debounce delay
|
|
10
10
|
|
|
11
|
+
const HighlightedText = React.memo(({ text, searchTerm }: { text: string; searchTerm: string }) => {
|
|
12
|
+
if (!searchTerm) return <>{text}</>;
|
|
13
|
+
|
|
14
|
+
const regex = new RegExp(`(${searchTerm})`, 'gi');
|
|
15
|
+
const parts = text.split(regex);
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<>
|
|
19
|
+
{parts.map((part, index) =>
|
|
20
|
+
regex.test(part) ? (
|
|
21
|
+
<span key={index} className="bg-yellow-200 text-gray-900 font-semibold">
|
|
22
|
+
{part}
|
|
23
|
+
</span>
|
|
24
|
+
) : (
|
|
25
|
+
<span key={index}>{part}</span>
|
|
26
|
+
)
|
|
27
|
+
)}
|
|
28
|
+
</>
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
|
|
11
32
|
export const getMessageColorByCollection = (collection: string) => {
|
|
12
33
|
if (collection === 'commands') return 'bg-blue-50 text-blue-600';
|
|
13
34
|
if (collection === 'queries') return 'bg-green-50 text-green-600';
|
|
@@ -46,12 +67,14 @@ const ServiceItem = React.memo(
|
|
|
46
67
|
collapsedGroups,
|
|
47
68
|
toggleGroupCollapse,
|
|
48
69
|
isVisualizer,
|
|
70
|
+
searchTerm,
|
|
49
71
|
}: {
|
|
50
72
|
item: ServiceItem;
|
|
51
73
|
decodedCurrentPath: string;
|
|
52
74
|
collapsedGroups: { [key: string]: boolean };
|
|
53
75
|
toggleGroupCollapse: (group: string) => void;
|
|
54
76
|
isVisualizer: boolean;
|
|
77
|
+
searchTerm: string;
|
|
55
78
|
}) => {
|
|
56
79
|
const asyncAPISpecifications = item.specifications?.filter((spec) => spec.type === 'asyncapi');
|
|
57
80
|
const openAPISpecifications = item.specifications?.filter((spec) => spec.type === 'openapi');
|
|
@@ -68,7 +91,9 @@ const ServiceItem = React.memo(
|
|
|
68
91
|
}}
|
|
69
92
|
className="flex justify-between items-center pl-2 w-full text-xs"
|
|
70
93
|
>
|
|
71
|
-
<span className="truncate text-xs font-bold">
|
|
94
|
+
<span className="truncate text-xs font-bold">
|
|
95
|
+
<HighlightedText text={item.label} searchTerm={searchTerm} />
|
|
96
|
+
</span>
|
|
72
97
|
<span className="text-purple-600 ml-2 text-[10px] font-medium bg-purple-50 px-2 py-0.5 rounded">SERVICE</span>
|
|
73
98
|
</button>
|
|
74
99
|
}
|
|
@@ -131,7 +156,7 @@ const ServiceItem = React.memo(
|
|
|
131
156
|
</button>
|
|
132
157
|
}
|
|
133
158
|
>
|
|
134
|
-
<MessageList messages={item.receives} decodedCurrentPath={decodedCurrentPath} />
|
|
159
|
+
<MessageList messages={item.receives} decodedCurrentPath={decodedCurrentPath} searchTerm={searchTerm} />
|
|
135
160
|
</CollapsibleGroup>
|
|
136
161
|
|
|
137
162
|
<CollapsibleGroup
|
|
@@ -149,7 +174,7 @@ const ServiceItem = React.memo(
|
|
|
149
174
|
</button>
|
|
150
175
|
}
|
|
151
176
|
>
|
|
152
|
-
<MessageList messages={item.sends} decodedCurrentPath={decodedCurrentPath} />
|
|
177
|
+
<MessageList messages={item.sends} decodedCurrentPath={decodedCurrentPath} searchTerm={searchTerm} />
|
|
153
178
|
</CollapsibleGroup>
|
|
154
179
|
{!isVisualizer && item.entities.length > 0 && (
|
|
155
180
|
<CollapsibleGroup
|
|
@@ -167,7 +192,7 @@ const ServiceItem = React.memo(
|
|
|
167
192
|
</button>
|
|
168
193
|
}
|
|
169
194
|
>
|
|
170
|
-
<MessageList messages={item.entities} decodedCurrentPath={decodedCurrentPath} />
|
|
195
|
+
<MessageList messages={item.entities} decodedCurrentPath={decodedCurrentPath} searchTerm={searchTerm} />
|
|
171
196
|
</CollapsibleGroup>
|
|
172
197
|
)}
|
|
173
198
|
</div>
|
|
@@ -206,12 +231,17 @@ const ListViewSideBar: React.FC<ListViewSideBarProps> = ({ resources, currentPat
|
|
|
206
231
|
const filteredData = useMemo(() => {
|
|
207
232
|
if (!debouncedSearchTerm) return data;
|
|
208
233
|
|
|
209
|
-
const filterItem = (item: { label: string }) => {
|
|
210
|
-
return
|
|
234
|
+
const filterItem = (item: { label: string; id?: string }) => {
|
|
235
|
+
return (
|
|
236
|
+
item.label.toLowerCase().includes(debouncedSearchTerm) || (item.id && item.id.toLowerCase().includes(debouncedSearchTerm))
|
|
237
|
+
);
|
|
211
238
|
};
|
|
212
239
|
|
|
213
240
|
const filterMessages = (messages: MessageItem[]) => {
|
|
214
|
-
return messages.filter(
|
|
241
|
+
return messages.filter(
|
|
242
|
+
(message) =>
|
|
243
|
+
message.data.name.toLowerCase().includes(debouncedSearchTerm) || message.id.toLowerCase().includes(debouncedSearchTerm)
|
|
244
|
+
);
|
|
215
245
|
};
|
|
216
246
|
|
|
217
247
|
return {
|
|
@@ -224,13 +254,22 @@ const ListViewSideBar: React.FC<ListViewSideBarProps> = ({ resources, currentPat
|
|
|
224
254
|
receives: filterMessages(service.receives),
|
|
225
255
|
isVisible:
|
|
226
256
|
filterItem(service) ||
|
|
227
|
-
service.sends.some(
|
|
228
|
-
|
|
257
|
+
service.sends.some(
|
|
258
|
+
(msg: MessageItem) =>
|
|
259
|
+
msg.data.name.toLowerCase().includes(debouncedSearchTerm) || msg.id.toLowerCase().includes(debouncedSearchTerm)
|
|
260
|
+
) ||
|
|
261
|
+
service.receives.some(
|
|
262
|
+
(msg: MessageItem) =>
|
|
263
|
+
msg.data.name.toLowerCase().includes(debouncedSearchTerm) || msg.id.toLowerCase().includes(debouncedSearchTerm)
|
|
264
|
+
),
|
|
229
265
|
}))
|
|
230
266
|
.filter((service: ServiceItem & { isVisible: boolean }) => service.isVisible) || [],
|
|
231
267
|
flows: data.flows?.filter(filterItem) || [],
|
|
232
268
|
messagesNotInService:
|
|
233
|
-
data.messagesNotInService?.filter(
|
|
269
|
+
data.messagesNotInService?.filter(
|
|
270
|
+
(msg: MessageItem) =>
|
|
271
|
+
msg.label.toLowerCase().includes(debouncedSearchTerm) || msg.id.toLowerCase().includes(debouncedSearchTerm)
|
|
272
|
+
) || [],
|
|
234
273
|
};
|
|
235
274
|
}, [data, debouncedSearchTerm]);
|
|
236
275
|
|
|
@@ -333,7 +372,9 @@ const ListViewSideBar: React.FC<ListViewSideBarProps> = ({ resources, currentPat
|
|
|
333
372
|
decodedCurrentPath === item.href
|
|
334
373
|
}`}
|
|
335
374
|
>
|
|
336
|
-
<span className="truncate">
|
|
375
|
+
<span className="truncate">
|
|
376
|
+
<HighlightedText text={item.label} searchTerm={debouncedSearchTerm} />
|
|
377
|
+
</span>
|
|
337
378
|
<span className="text-yellow-600 ml-2 text-[10px] font-medium bg-yellow-50 px-2 py-0.5 rounded">
|
|
338
379
|
{isDomainSubDomain(item) ? 'SUBDOMAIN' : 'DOMAIN'}
|
|
339
380
|
</span>
|
|
@@ -395,7 +436,11 @@ const ListViewSideBar: React.FC<ListViewSideBarProps> = ({ resources, currentPat
|
|
|
395
436
|
</button>
|
|
396
437
|
}
|
|
397
438
|
>
|
|
398
|
-
<MessageList
|
|
439
|
+
<MessageList
|
|
440
|
+
messages={item.entities}
|
|
441
|
+
decodedCurrentPath={decodedCurrentPath}
|
|
442
|
+
searchTerm={debouncedSearchTerm}
|
|
443
|
+
/>
|
|
399
444
|
</CollapsibleGroup>
|
|
400
445
|
)}
|
|
401
446
|
</div>
|
|
@@ -417,6 +462,7 @@ const ListViewSideBar: React.FC<ListViewSideBarProps> = ({ resources, currentPat
|
|
|
417
462
|
collapsedGroups={collapsedGroups}
|
|
418
463
|
toggleGroupCollapse={toggleGroupCollapse}
|
|
419
464
|
isVisualizer={isVisualizer}
|
|
465
|
+
searchTerm={debouncedSearchTerm}
|
|
420
466
|
/>
|
|
421
467
|
))}
|
|
422
468
|
</ul>
|
|
@@ -434,7 +480,9 @@ const ListViewSideBar: React.FC<ListViewSideBarProps> = ({ resources, currentPat
|
|
|
434
480
|
decodedCurrentPath === item.href ? 'bg-purple-100 text-purple-900' : 'hover:bg-purple-100'
|
|
435
481
|
}`}
|
|
436
482
|
>
|
|
437
|
-
<span className="truncate">
|
|
483
|
+
<span className="truncate">
|
|
484
|
+
<HighlightedText text={item.label} searchTerm={debouncedSearchTerm} />
|
|
485
|
+
</span>
|
|
438
486
|
<span
|
|
439
487
|
className={`ml-2 text-[10px] font-medium px-2 uppercase py-0.5 rounded ${getMessageColorByCollection(item.collection)}`}
|
|
440
488
|
>
|
|
@@ -458,7 +506,9 @@ const ListViewSideBar: React.FC<ListViewSideBarProps> = ({ resources, currentPat
|
|
|
458
506
|
decodedCurrentPath === item.href ? 'bg-cyan-100 text-cyan-900' : 'hover:bg-purple-100'
|
|
459
507
|
}`}
|
|
460
508
|
>
|
|
461
|
-
<span className="truncate">
|
|
509
|
+
<span className="truncate">
|
|
510
|
+
<HighlightedText text={item.label} searchTerm={debouncedSearchTerm} />
|
|
511
|
+
</span>
|
|
462
512
|
<span className="text-cyan-600 ml-2 text-[10px] font-medium bg-cyan-50 px-2 py-0.5 rounded">FLOW</span>
|
|
463
513
|
</a>
|
|
464
514
|
</li>
|