@eventcatalog/core 3.12.8 → 3.13.0-beta.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-DINAMVEI.js → chunk-7RCUF3VG.js} +1 -1
- package/dist/{chunk-GOLMKUV3.js → chunk-AY2OEUWV.js} +1 -1
- package/dist/{chunk-6MBAYHHT.js → chunk-NXATPLVB.js} +1 -1
- package/dist/{chunk-JUWMXGCI.js → chunk-V3GX7FC3.js} +1 -1
- package/dist/{chunk-ROHEB5DM.js → chunk-VXTATPGX.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/Design/Design.astro +2 -2
- package/eventcatalog/src/components/MDX/EntityMap/EntityMap.astro +2 -2
- package/eventcatalog/src/components/MDX/File.tsx +7 -7
- package/eventcatalog/src/components/MDX/Flow/Flow.astro +2 -2
- package/eventcatalog/src/components/MDX/NodeGraph/AstroNodeGraph.tsx +104 -0
- package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.astro +2 -2
- package/eventcatalog/src/components/MDX/NodeGraph/README.md +85 -0
- package/eventcatalog/src/components/MDX/Schema.astro +3 -6
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +1 -6
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/spec/[filename].astro +2 -2
- package/eventcatalog/src/pages/visualiser/designs/[id]/index.astro +2 -2
- package/eventcatalog/src/utils/collections/channels.ts +1 -26
- package/eventcatalog/src/utils/collections/commands.ts +1 -26
- package/eventcatalog/src/utils/collections/containers.ts +1 -32
- package/eventcatalog/src/utils/collections/data-products.ts +1 -19
- package/eventcatalog/src/utils/collections/diagrams.ts +0 -7
- package/eventcatalog/src/utils/collections/domains.ts +0 -18
- package/eventcatalog/src/utils/collections/entities.ts +1 -27
- package/eventcatalog/src/utils/collections/events.ts +1 -22
- package/eventcatalog/src/utils/collections/flows.ts +0 -8
- package/eventcatalog/src/utils/collections/queries.ts +1 -22
- package/eventcatalog/src/utils/collections/schemas.ts +9 -4
- package/eventcatalog/src/utils/collections/services.ts +0 -20
- package/eventcatalog/src/utils/collections/teams.ts +0 -6
- package/eventcatalog/src/utils/collections/users.ts +0 -6
- package/eventcatalog/src/utils/collections/util.ts +10 -1
- package/eventcatalog/src/utils/node-graphs/container-node-graph.ts +66 -17
- package/eventcatalog/src/utils/node-graphs/data-products-node-graph.ts +14 -5
- package/eventcatalog/src/utils/node-graphs/domains-node-graph.ts +1 -1
- package/eventcatalog/src/utils/node-graphs/message-node-graph.ts +133 -18
- package/eventcatalog/src/utils/node-graphs/services-node-graph.ts +36 -14
- package/eventcatalog/src/utils/node-graphs/utils/utils.ts +115 -4
- package/package.json +4 -4
- package/eventcatalog/src/components/MDX/NodeGraph/DownloadButton.tsx +0 -62
- package/eventcatalog/src/components/MDX/NodeGraph/Edges/AnimatedMessageEdge.tsx +0 -110
- package/eventcatalog/src/components/MDX/NodeGraph/Edges/FlowEdge.tsx +0 -96
- package/eventcatalog/src/components/MDX/NodeGraph/Edges/MultilineEdgeLabel.tsx +0 -52
- package/eventcatalog/src/components/MDX/NodeGraph/FocusMode/FocusModeContent.tsx +0 -294
- package/eventcatalog/src/components/MDX/NodeGraph/FocusMode/FocusModeNodeActions.tsx +0 -92
- package/eventcatalog/src/components/MDX/NodeGraph/FocusMode/FocusModePlaceholder.tsx +0 -26
- package/eventcatalog/src/components/MDX/NodeGraph/FocusMode/utils.ts +0 -163
- package/eventcatalog/src/components/MDX/NodeGraph/FocusModeModal.tsx +0 -99
- package/eventcatalog/src/components/MDX/NodeGraph/MermaidView.tsx +0 -242
- package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.tsx +0 -1181
- package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Actor.tsx +0 -46
- package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Channel.tsx +0 -55
- package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Command.tsx +0 -27
- package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Custom.tsx +0 -159
- package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Data.tsx +0 -63
- package/eventcatalog/src/components/MDX/NodeGraph/Nodes/DataProduct.tsx +0 -132
- package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Domain.tsx +0 -155
- package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Entity.tsx +0 -154
- package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Event.tsx +0 -29
- package/eventcatalog/src/components/MDX/NodeGraph/Nodes/ExternalSystem.tsx +0 -79
- package/eventcatalog/src/components/MDX/NodeGraph/Nodes/ExternalSystem2.tsx +0 -24
- package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Flow.tsx +0 -107
- package/eventcatalog/src/components/MDX/NodeGraph/Nodes/MessageContextMenu.tsx +0 -63
- package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Query.tsx +0 -28
- package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Service.tsx +0 -127
- package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Step.tsx +0 -64
- package/eventcatalog/src/components/MDX/NodeGraph/Nodes/User.tsx +0 -76
- package/eventcatalog/src/components/MDX/NodeGraph/Nodes/View.tsx +0 -24
- package/eventcatalog/src/components/MDX/NodeGraph/StepWalkthrough.tsx +0 -296
- package/eventcatalog/src/components/MDX/NodeGraph/StudioModal.tsx +0 -129
- package/eventcatalog/src/components/MDX/NodeGraph/VisualiserSearch.tsx +0 -258
- package/eventcatalog/src/components/MDX/NodeGraph/VisualizerDropdownContent.tsx +0 -313
|
@@ -1,1181 +0,0 @@
|
|
|
1
|
-
import { useEffect, useMemo, useState, useCallback, useRef } from 'react';
|
|
2
|
-
import { createPortal } from 'react-dom';
|
|
3
|
-
import {
|
|
4
|
-
ReactFlow,
|
|
5
|
-
Background,
|
|
6
|
-
ConnectionLineType,
|
|
7
|
-
Controls,
|
|
8
|
-
Panel,
|
|
9
|
-
MiniMap,
|
|
10
|
-
ReactFlowProvider,
|
|
11
|
-
useNodesState,
|
|
12
|
-
useEdgesState,
|
|
13
|
-
type Edge,
|
|
14
|
-
type Node,
|
|
15
|
-
type NodeChange,
|
|
16
|
-
useReactFlow,
|
|
17
|
-
getNodesBounds,
|
|
18
|
-
getViewportForBounds,
|
|
19
|
-
type NodeTypes,
|
|
20
|
-
} from '@xyflow/react';
|
|
21
|
-
import '@xyflow/react/dist/style.css';
|
|
22
|
-
import { ExternalLink, HistoryIcon, CheckIcon, ClipboardIcon, MoreVertical } from 'lucide-react';
|
|
23
|
-
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
|
24
|
-
import { toPng } from 'html-to-image';
|
|
25
|
-
import { DocumentArrowDownIcon, PresentationChartLineIcon } from '@heroicons/react/24/outline';
|
|
26
|
-
// Nodes and edges
|
|
27
|
-
import ServiceNode from './Nodes/Service';
|
|
28
|
-
import FlowNode from './Nodes/Flow';
|
|
29
|
-
import EventNode from './Nodes/Event';
|
|
30
|
-
import EntityNode from './Nodes/Entity';
|
|
31
|
-
import QueryNode from './Nodes/Query';
|
|
32
|
-
import UserNode from './Nodes/User';
|
|
33
|
-
import StepNode from './Nodes/Step';
|
|
34
|
-
import CommandNode from './Nodes/Command';
|
|
35
|
-
import ExternalSystemNode from './Nodes/ExternalSystem';
|
|
36
|
-
import DomainNode from './Nodes/Domain';
|
|
37
|
-
import AnimatedMessageEdge from './Edges/AnimatedMessageEdge';
|
|
38
|
-
import MultilineEdgeLabel from './Edges/MultilineEdgeLabel';
|
|
39
|
-
import FlowEdge from './Edges/FlowEdge';
|
|
40
|
-
import CustomNode from './Nodes/Custom';
|
|
41
|
-
import DataNode from './Nodes/Data';
|
|
42
|
-
import ViewNode from './Nodes/View';
|
|
43
|
-
import ActorNode from './Nodes/Actor';
|
|
44
|
-
import ExternalSystemNode2 from './Nodes/ExternalSystem2';
|
|
45
|
-
import DataProductNode from './Nodes/DataProduct';
|
|
46
|
-
import { Note as NoteNode } from '@eventcatalog/visualizer';
|
|
47
|
-
|
|
48
|
-
import type { CollectionEntry } from 'astro:content';
|
|
49
|
-
import { navigate } from 'astro:transitions/client';
|
|
50
|
-
import type { CollectionTypes } from '@types';
|
|
51
|
-
import { buildUrl } from '@utils/url-builder';
|
|
52
|
-
import ChannelNode from './Nodes/Channel';
|
|
53
|
-
import { useEventCatalogVisualiser } from 'src/hooks/eventcatalog-visualizer';
|
|
54
|
-
import VisualiserSearch, { type VisualiserSearchRef } from './VisualiserSearch';
|
|
55
|
-
import StepWalkthrough from './StepWalkthrough';
|
|
56
|
-
import StudioModal from './StudioModal';
|
|
57
|
-
import FocusModeModal from './FocusModeModal';
|
|
58
|
-
import MermaidView from './MermaidView';
|
|
59
|
-
import VisualizerDropdownContent from './VisualizerDropdownContent';
|
|
60
|
-
import { convertToMermaid } from '@utils/node-graphs/export-mermaid';
|
|
61
|
-
import { copyToClipboard } from '@utils/clipboard';
|
|
62
|
-
|
|
63
|
-
// Minimum pixel change to detect layout modifications (avoids floating point comparison issues)
|
|
64
|
-
const POSITION_CHANGE_THRESHOLD = 1;
|
|
65
|
-
|
|
66
|
-
interface Props {
|
|
67
|
-
nodes: any;
|
|
68
|
-
edges: any;
|
|
69
|
-
title?: string;
|
|
70
|
-
subtitle?: string;
|
|
71
|
-
includeBackground?: boolean;
|
|
72
|
-
includeControls?: boolean;
|
|
73
|
-
linkTo: 'docs' | 'visualiser';
|
|
74
|
-
includeKey?: boolean;
|
|
75
|
-
linksToVisualiser?: boolean;
|
|
76
|
-
links?: { label: string; url: string }[];
|
|
77
|
-
mode?: 'full' | 'simple';
|
|
78
|
-
showFlowWalkthrough?: boolean;
|
|
79
|
-
showSearch?: boolean;
|
|
80
|
-
zoomOnScroll?: boolean;
|
|
81
|
-
designId?: string;
|
|
82
|
-
isStudioModalOpen?: boolean;
|
|
83
|
-
setIsStudioModalOpen?: (isOpen: boolean) => void;
|
|
84
|
-
isChatEnabled?: boolean;
|
|
85
|
-
maxTextSize?: number;
|
|
86
|
-
isDevMode?: boolean;
|
|
87
|
-
resourceKey?: string;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const getVisualiserUrlForCollection = (collectionItem: CollectionEntry<CollectionTypes>) => {
|
|
91
|
-
return buildUrl(`/visualiser/${collectionItem.collection}/${collectionItem.data.id}/${collectionItem.data.version}`);
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
const NodeGraphBuilder = ({
|
|
95
|
-
nodes: initialNodes,
|
|
96
|
-
edges: initialEdges,
|
|
97
|
-
title,
|
|
98
|
-
includeBackground = true,
|
|
99
|
-
linkTo = 'docs',
|
|
100
|
-
includeKey = true,
|
|
101
|
-
linksToVisualiser = false,
|
|
102
|
-
links = [],
|
|
103
|
-
mode = 'full',
|
|
104
|
-
showFlowWalkthrough = true,
|
|
105
|
-
showSearch = true,
|
|
106
|
-
zoomOnScroll = false,
|
|
107
|
-
isStudioModalOpen,
|
|
108
|
-
setIsStudioModalOpen = () => {},
|
|
109
|
-
isChatEnabled = false,
|
|
110
|
-
maxTextSize,
|
|
111
|
-
isDevMode = false,
|
|
112
|
-
resourceKey,
|
|
113
|
-
}: Props) => {
|
|
114
|
-
const nodeTypes = useMemo(
|
|
115
|
-
() =>
|
|
116
|
-
({
|
|
117
|
-
service: ServiceNode,
|
|
118
|
-
services: ServiceNode,
|
|
119
|
-
flow: FlowNode,
|
|
120
|
-
flows: FlowNode,
|
|
121
|
-
event: EventNode,
|
|
122
|
-
events: EventNode,
|
|
123
|
-
channel: ChannelNode,
|
|
124
|
-
channels: ChannelNode,
|
|
125
|
-
query: QueryNode,
|
|
126
|
-
queries: QueryNode,
|
|
127
|
-
command: CommandNode,
|
|
128
|
-
commands: CommandNode,
|
|
129
|
-
domain: DomainNode,
|
|
130
|
-
domains: DomainNode,
|
|
131
|
-
step: StepNode,
|
|
132
|
-
user: UserNode,
|
|
133
|
-
custom: CustomNode,
|
|
134
|
-
externalSystem: ExternalSystemNode,
|
|
135
|
-
'external-system': ExternalSystemNode2,
|
|
136
|
-
entity: EntityNode,
|
|
137
|
-
entities: EntityNode,
|
|
138
|
-
data: DataNode,
|
|
139
|
-
view: ViewNode,
|
|
140
|
-
actor: ActorNode,
|
|
141
|
-
'data-product': DataProductNode,
|
|
142
|
-
'data-products': DataProductNode,
|
|
143
|
-
note: (props: any) => <NoteNode {...props} readOnly={true} />,
|
|
144
|
-
}) as unknown as NodeTypes,
|
|
145
|
-
[]
|
|
146
|
-
);
|
|
147
|
-
const edgeTypes = useMemo(
|
|
148
|
-
() => ({
|
|
149
|
-
animated: AnimatedMessageEdge,
|
|
150
|
-
multiline: MultilineEdgeLabel,
|
|
151
|
-
'flow-edge': FlowEdge,
|
|
152
|
-
}),
|
|
153
|
-
[]
|
|
154
|
-
);
|
|
155
|
-
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
|
156
|
-
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
|
157
|
-
const [animateMessages, setAnimateMessages] = useState(false);
|
|
158
|
-
const [activeStepIndex, setActiveStepIndex] = useState<number | null>(null);
|
|
159
|
-
const [isFullscreen, setIsFullscreen] = useState(false);
|
|
160
|
-
const [mermaidCode, setMermaidCode] = useState('');
|
|
161
|
-
const [isShareModalOpen, setIsShareModalOpen] = useState(false);
|
|
162
|
-
const [shareUrlCopySuccess, setShareUrlCopySuccess] = useState(false);
|
|
163
|
-
const [isMermaidView, setIsMermaidView] = useState(false);
|
|
164
|
-
const [showMinimap, setShowMinimap] = useState(false);
|
|
165
|
-
const [hasLayoutChanges, setHasLayoutChanges] = useState(false);
|
|
166
|
-
const [isSavingLayout, setIsSavingLayout] = useState(false);
|
|
167
|
-
const initialPositionsRef = useRef<Record<string, { x: number; y: number }>>({});
|
|
168
|
-
// const [isStudioModalOpen, setIsStudioModalOpen] = useState(false);
|
|
169
|
-
const [focusModeOpen, setFocusModeOpen] = useState(false);
|
|
170
|
-
const [focusedNodeId, setFocusedNodeId] = useState<string | null>(null);
|
|
171
|
-
|
|
172
|
-
// Check if there are channels to determine if we need the visualizer functionality
|
|
173
|
-
const hasChannels = useMemo(() => initialNodes.some((node: any) => node.type === 'channels'), [initialNodes]);
|
|
174
|
-
const { hideChannels, toggleChannelsVisibility } = useEventCatalogVisualiser({
|
|
175
|
-
nodes,
|
|
176
|
-
edges,
|
|
177
|
-
setNodes,
|
|
178
|
-
setEdges,
|
|
179
|
-
skipProcessing: !hasChannels, // Pass flag to skip processing when no channels
|
|
180
|
-
});
|
|
181
|
-
const { fitView, getNodes, toObject } = useReactFlow();
|
|
182
|
-
const searchRef = useRef<VisualiserSearchRef>(null);
|
|
183
|
-
const reactFlowWrapperRef = useRef<HTMLDivElement>(null);
|
|
184
|
-
const scrollableContainerRef = useRef<HTMLElement | null>(null);
|
|
185
|
-
|
|
186
|
-
// Store initial node positions for change detection (dev mode only)
|
|
187
|
-
useEffect(() => {
|
|
188
|
-
if (isDevMode && initialNodes.length > 0) {
|
|
189
|
-
const positions: Record<string, { x: number; y: number }> = {};
|
|
190
|
-
initialNodes.forEach((node: Node) => {
|
|
191
|
-
positions[node.id] = { x: node.position.x, y: node.position.y };
|
|
192
|
-
});
|
|
193
|
-
initialPositionsRef.current = positions;
|
|
194
|
-
}
|
|
195
|
-
}, [isDevMode, initialNodes]);
|
|
196
|
-
|
|
197
|
-
// Detect layout changes by comparing current positions to initial positions
|
|
198
|
-
const checkForLayoutChanges = useCallback(() => {
|
|
199
|
-
if (!isDevMode) return;
|
|
200
|
-
const initial = initialPositionsRef.current;
|
|
201
|
-
if (Object.keys(initial).length === 0) return;
|
|
202
|
-
|
|
203
|
-
const hasChanges = nodes.some((node) => {
|
|
204
|
-
const initialPos = initial[node.id];
|
|
205
|
-
return (
|
|
206
|
-
initialPos &&
|
|
207
|
-
(Math.abs(node.position.x - initialPos.x) > POSITION_CHANGE_THRESHOLD ||
|
|
208
|
-
Math.abs(node.position.y - initialPos.y) > POSITION_CHANGE_THRESHOLD)
|
|
209
|
-
);
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
setHasLayoutChanges(hasChanges);
|
|
213
|
-
}, [isDevMode, nodes]);
|
|
214
|
-
|
|
215
|
-
// Wrap onNodesChange to detect layout changes after node drag
|
|
216
|
-
const handleNodesChange = useCallback(
|
|
217
|
-
(changes: NodeChange[]) => {
|
|
218
|
-
onNodesChange(changes);
|
|
219
|
-
// Check for position changes after drag ends
|
|
220
|
-
const hasDragEnd = changes.some((change) => change.type === 'position' && !change.dragging);
|
|
221
|
-
if (hasDragEnd) {
|
|
222
|
-
// Use setTimeout to ensure state is updated
|
|
223
|
-
setTimeout(checkForLayoutChanges, 0);
|
|
224
|
-
}
|
|
225
|
-
},
|
|
226
|
-
[onNodesChange, checkForLayoutChanges]
|
|
227
|
-
);
|
|
228
|
-
|
|
229
|
-
const resetNodesAndEdges = useCallback(() => {
|
|
230
|
-
setNodes((nds) =>
|
|
231
|
-
nds.map((node) => {
|
|
232
|
-
node.style = { ...node.style, opacity: 1 };
|
|
233
|
-
return { ...node, animated: animateMessages };
|
|
234
|
-
})
|
|
235
|
-
);
|
|
236
|
-
setEdges((eds) =>
|
|
237
|
-
eds.map((edge) => {
|
|
238
|
-
edge.style = { ...edge.style, opacity: 1 };
|
|
239
|
-
edge.labelStyle = { ...edge.labelStyle, opacity: 1 };
|
|
240
|
-
return { ...edge, data: { ...edge.data, opacity: 1, animated: animateMessages }, animated: animateMessages };
|
|
241
|
-
})
|
|
242
|
-
);
|
|
243
|
-
}, [setNodes, setEdges, animateMessages]);
|
|
244
|
-
|
|
245
|
-
const handleNodeClick = useCallback(
|
|
246
|
-
(_: any, node: Node) => {
|
|
247
|
-
if (linksToVisualiser) {
|
|
248
|
-
if (node.type === 'events' || node.type === 'commands') {
|
|
249
|
-
navigate(getVisualiserUrlForCollection(node.data.message as CollectionEntry<CollectionTypes>));
|
|
250
|
-
}
|
|
251
|
-
if (node.type === 'services') {
|
|
252
|
-
navigate(getVisualiserUrlForCollection(node.data.service as CollectionEntry<'services'>));
|
|
253
|
-
}
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// Disable focus mode for flow and entity visualizations
|
|
258
|
-
const isFlow = edges.some((edge: Edge) => edge.type === 'flow-edge');
|
|
259
|
-
const isEntityVisualizer = nodes.some((n: Node) => n.type === 'entities');
|
|
260
|
-
if (isFlow || isEntityVisualizer) return;
|
|
261
|
-
|
|
262
|
-
// Open focus mode modal
|
|
263
|
-
setFocusedNodeId(node.id);
|
|
264
|
-
setFocusModeOpen(true);
|
|
265
|
-
},
|
|
266
|
-
[linksToVisualiser, edges, nodes]
|
|
267
|
-
);
|
|
268
|
-
|
|
269
|
-
const toggleAnimateMessages = () => {
|
|
270
|
-
setAnimateMessages(!animateMessages);
|
|
271
|
-
localStorage.setItem('EventCatalog:animateMessages', JSON.stringify(!animateMessages));
|
|
272
|
-
};
|
|
273
|
-
|
|
274
|
-
// Handle fit to view
|
|
275
|
-
const handleFitView = useCallback(() => {
|
|
276
|
-
fitView({ duration: 400, padding: 0.2 });
|
|
277
|
-
}, [fitView]);
|
|
278
|
-
|
|
279
|
-
// animate messages, between views
|
|
280
|
-
// URL parameter takes priority over localStorage
|
|
281
|
-
useEffect(() => {
|
|
282
|
-
const urlParams = new URLSearchParams(window.location.search);
|
|
283
|
-
const animateParam = urlParams.get('animate');
|
|
284
|
-
|
|
285
|
-
if (animateParam === 'true') {
|
|
286
|
-
setAnimateMessages(true);
|
|
287
|
-
} else if (animateParam === 'false') {
|
|
288
|
-
setAnimateMessages(false);
|
|
289
|
-
} else {
|
|
290
|
-
// Fall back to localStorage if no URL parameter
|
|
291
|
-
const storedAnimateMessages = localStorage.getItem('EventCatalog:animateMessages');
|
|
292
|
-
if (storedAnimateMessages !== null) {
|
|
293
|
-
setAnimateMessages(storedAnimateMessages === 'true');
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
}, []);
|
|
297
|
-
|
|
298
|
-
useEffect(() => {
|
|
299
|
-
setEdges((eds) =>
|
|
300
|
-
eds.map((edge) => ({
|
|
301
|
-
...edge,
|
|
302
|
-
animated: animateMessages,
|
|
303
|
-
type: edge.type === 'flow-edge' || edge.type === 'multiline' ? edge.type : animateMessages ? 'animated' : 'default',
|
|
304
|
-
data: { ...edge.data, animateMessages, animated: animateMessages },
|
|
305
|
-
}))
|
|
306
|
-
);
|
|
307
|
-
}, [animateMessages]);
|
|
308
|
-
|
|
309
|
-
useEffect(() => {
|
|
310
|
-
setTimeout(() => {
|
|
311
|
-
fitView({ duration: 800 });
|
|
312
|
-
}, 150);
|
|
313
|
-
}, []);
|
|
314
|
-
|
|
315
|
-
// Generate mermaid code from nodes and edges
|
|
316
|
-
useEffect(() => {
|
|
317
|
-
try {
|
|
318
|
-
const code = convertToMermaid(nodes, edges, { includeStyles: true, direction: 'LR' });
|
|
319
|
-
setMermaidCode(code);
|
|
320
|
-
} catch (error) {
|
|
321
|
-
console.error('Error generating mermaid code:', error);
|
|
322
|
-
setMermaidCode('');
|
|
323
|
-
}
|
|
324
|
-
}, [nodes, edges]);
|
|
325
|
-
|
|
326
|
-
// Handle scroll wheel events to forward to page when no modifier keys are pressed
|
|
327
|
-
// Only when zoomOnScroll is disabled
|
|
328
|
-
// This is a fix for when we embed node graphs into pages, and users are scrolling the documentation pages
|
|
329
|
-
// We dont want REACT FLOW to swallow the scroll events, so we forward them to the parent page
|
|
330
|
-
useEffect(() => {
|
|
331
|
-
// Skip scroll handling if zoomOnScroll is enabled
|
|
332
|
-
if (zoomOnScroll) return;
|
|
333
|
-
|
|
334
|
-
// Cache the scrollable container on mount (expensive operation done once)
|
|
335
|
-
const findScrollableContainer = (): HTMLElement | null => {
|
|
336
|
-
// Try specific known selectors first (fast)
|
|
337
|
-
const selectors = [
|
|
338
|
-
'.docs-layout .overflow-y-auto',
|
|
339
|
-
'.overflow-y-auto',
|
|
340
|
-
'[style*="overflow-y:auto"]',
|
|
341
|
-
'[style*="overflow-y: auto"]',
|
|
342
|
-
];
|
|
343
|
-
|
|
344
|
-
for (const selector of selectors) {
|
|
345
|
-
const element = document.querySelector(selector) as HTMLElement;
|
|
346
|
-
if (element) return element;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
return null;
|
|
350
|
-
};
|
|
351
|
-
|
|
352
|
-
// Find and cache the scrollable container once
|
|
353
|
-
if (!scrollableContainerRef.current) {
|
|
354
|
-
scrollableContainerRef.current = findScrollableContainer();
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
const handleWheel = (event: WheelEvent) => {
|
|
358
|
-
// Only forward scroll if no modifier keys are pressed
|
|
359
|
-
if (!event.ctrlKey && !event.shiftKey && !event.metaKey) {
|
|
360
|
-
event.preventDefault();
|
|
361
|
-
|
|
362
|
-
const scrollableContainer = scrollableContainerRef.current;
|
|
363
|
-
|
|
364
|
-
if (scrollableContainer) {
|
|
365
|
-
scrollableContainer.scrollBy({
|
|
366
|
-
top: event.deltaY,
|
|
367
|
-
left: event.deltaX,
|
|
368
|
-
behavior: 'instant',
|
|
369
|
-
});
|
|
370
|
-
} else {
|
|
371
|
-
// Fallback to window scroll
|
|
372
|
-
window.scrollBy({
|
|
373
|
-
top: event.deltaY,
|
|
374
|
-
left: event.deltaX,
|
|
375
|
-
behavior: 'instant',
|
|
376
|
-
});
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
};
|
|
380
|
-
|
|
381
|
-
const wrapper = reactFlowWrapperRef.current;
|
|
382
|
-
if (wrapper) {
|
|
383
|
-
wrapper.addEventListener('wheel', handleWheel, { passive: false });
|
|
384
|
-
return () => {
|
|
385
|
-
wrapper.removeEventListener('wheel', handleWheel);
|
|
386
|
-
};
|
|
387
|
-
}
|
|
388
|
-
}, [zoomOnScroll]);
|
|
389
|
-
|
|
390
|
-
const handlePaneClick = useCallback(() => {
|
|
391
|
-
searchRef.current?.hideSuggestions();
|
|
392
|
-
resetNodesAndEdges();
|
|
393
|
-
fitView({ duration: 800 });
|
|
394
|
-
}, [resetNodesAndEdges, fitView]);
|
|
395
|
-
|
|
396
|
-
const handleNodeSelect = useCallback(
|
|
397
|
-
(node: Node) => {
|
|
398
|
-
handleNodeClick(null, node);
|
|
399
|
-
},
|
|
400
|
-
[handleNodeClick]
|
|
401
|
-
);
|
|
402
|
-
|
|
403
|
-
const handleSearchClear = useCallback(() => {
|
|
404
|
-
resetNodesAndEdges();
|
|
405
|
-
fitView({ duration: 800 });
|
|
406
|
-
}, [resetNodesAndEdges, fitView]);
|
|
407
|
-
|
|
408
|
-
const downloadImage = useCallback((dataUrl: string, filename?: string) => {
|
|
409
|
-
const a = document.createElement('a');
|
|
410
|
-
a.setAttribute('download', `${filename || 'eventcatalog'}.png`);
|
|
411
|
-
a.setAttribute('href', dataUrl);
|
|
412
|
-
a.click();
|
|
413
|
-
}, []);
|
|
414
|
-
|
|
415
|
-
const openStudioModal = () => {
|
|
416
|
-
setIsStudioModalOpen(true);
|
|
417
|
-
};
|
|
418
|
-
|
|
419
|
-
const openChat = useCallback(() => {
|
|
420
|
-
window.dispatchEvent(new CustomEvent('eventcatalog:open-chat'));
|
|
421
|
-
}, []);
|
|
422
|
-
|
|
423
|
-
// Layout persistence handlers (dev mode only)
|
|
424
|
-
const handleSaveLayout = useCallback(async (): Promise<boolean> => {
|
|
425
|
-
if (!resourceKey) return false;
|
|
426
|
-
|
|
427
|
-
const positions: Record<string, { x: number; y: number }> = {};
|
|
428
|
-
nodes.forEach((node) => {
|
|
429
|
-
positions[node.id] = {
|
|
430
|
-
x: node.position.x,
|
|
431
|
-
y: node.position.y,
|
|
432
|
-
};
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
try {
|
|
436
|
-
const response = await fetch('/api/dev/visualizer-layout/save', {
|
|
437
|
-
method: 'POST',
|
|
438
|
-
headers: { 'Content-Type': 'application/json' },
|
|
439
|
-
body: JSON.stringify({ resourceKey, positions }),
|
|
440
|
-
});
|
|
441
|
-
const result = await response.json();
|
|
442
|
-
return result.success === true;
|
|
443
|
-
} catch {
|
|
444
|
-
return false;
|
|
445
|
-
}
|
|
446
|
-
}, [nodes, resourceKey]);
|
|
447
|
-
|
|
448
|
-
const handleResetLayout = useCallback(async (): Promise<boolean> => {
|
|
449
|
-
if (!resourceKey) return false;
|
|
450
|
-
|
|
451
|
-
try {
|
|
452
|
-
const response = await fetch('/api/dev/visualizer-layout/reset', {
|
|
453
|
-
method: 'POST',
|
|
454
|
-
headers: { 'Content-Type': 'application/json' },
|
|
455
|
-
body: JSON.stringify({ resourceKey }),
|
|
456
|
-
});
|
|
457
|
-
const result = await response.json();
|
|
458
|
-
return result.success === true;
|
|
459
|
-
} catch {
|
|
460
|
-
return false;
|
|
461
|
-
}
|
|
462
|
-
}, [resourceKey]);
|
|
463
|
-
|
|
464
|
-
// Quick save handler for the change detection UI
|
|
465
|
-
const handleQuickSaveLayout = useCallback(async () => {
|
|
466
|
-
setIsSavingLayout(true);
|
|
467
|
-
const success = await handleSaveLayout();
|
|
468
|
-
setIsSavingLayout(false);
|
|
469
|
-
if (success) {
|
|
470
|
-
// Update initial positions to current positions after save
|
|
471
|
-
const positions: Record<string, { x: number; y: number }> = {};
|
|
472
|
-
nodes.forEach((node) => {
|
|
473
|
-
positions[node.id] = { x: node.position.x, y: node.position.y };
|
|
474
|
-
});
|
|
475
|
-
initialPositionsRef.current = positions;
|
|
476
|
-
setHasLayoutChanges(false);
|
|
477
|
-
}
|
|
478
|
-
}, [handleSaveLayout, nodes]);
|
|
479
|
-
|
|
480
|
-
const handleCopyArchitectureCode = useCallback(async () => {
|
|
481
|
-
await copyToClipboard(mermaidCode);
|
|
482
|
-
}, [mermaidCode]);
|
|
483
|
-
|
|
484
|
-
const handleCopyShareUrl = useCallback(async () => {
|
|
485
|
-
const url = typeof window !== 'undefined' ? window.location.href : '';
|
|
486
|
-
await copyToClipboard(url);
|
|
487
|
-
setShareUrlCopySuccess(true);
|
|
488
|
-
setTimeout(() => setShareUrlCopySuccess(false), 2000);
|
|
489
|
-
}, []);
|
|
490
|
-
|
|
491
|
-
const toggleFullScreen = useCallback(() => {
|
|
492
|
-
if (!document.fullscreenElement) {
|
|
493
|
-
reactFlowWrapperRef.current?.requestFullscreen().catch((err) => {
|
|
494
|
-
console.error(`Error attempting to enable full-screen mode: ${err.message} (${err.name})`);
|
|
495
|
-
});
|
|
496
|
-
} else {
|
|
497
|
-
document.exitFullscreen();
|
|
498
|
-
}
|
|
499
|
-
}, []);
|
|
500
|
-
|
|
501
|
-
useEffect(() => {
|
|
502
|
-
const handleFullscreenChange = () => {
|
|
503
|
-
setIsFullscreen(!!document.fullscreenElement);
|
|
504
|
-
setTimeout(() => {
|
|
505
|
-
fitView({ duration: 800 });
|
|
506
|
-
}, 100);
|
|
507
|
-
};
|
|
508
|
-
|
|
509
|
-
document.addEventListener('fullscreenchange', handleFullscreenChange);
|
|
510
|
-
return () => {
|
|
511
|
-
document.removeEventListener('fullscreenchange', handleFullscreenChange);
|
|
512
|
-
};
|
|
513
|
-
}, [fitView]);
|
|
514
|
-
|
|
515
|
-
const handleExportVisual = useCallback(() => {
|
|
516
|
-
const imageWidth = 1024;
|
|
517
|
-
const imageHeight = 768;
|
|
518
|
-
const nodesBounds = getNodesBounds(getNodes());
|
|
519
|
-
const width = imageWidth > nodesBounds.width ? imageWidth : nodesBounds.width;
|
|
520
|
-
const height = imageHeight > nodesBounds.height ? imageHeight : nodesBounds.height;
|
|
521
|
-
const viewport = getViewportForBounds(nodesBounds, width, height, 0.5, 2, 0);
|
|
522
|
-
|
|
523
|
-
// Hide controls during export
|
|
524
|
-
const controls = document.querySelector('.react-flow__controls') as HTMLElement;
|
|
525
|
-
if (controls) controls.style.display = 'none';
|
|
526
|
-
|
|
527
|
-
toPng(document.querySelector('.react-flow__viewport') as HTMLElement, {
|
|
528
|
-
backgroundColor: '#f1f1f1',
|
|
529
|
-
width,
|
|
530
|
-
height,
|
|
531
|
-
style: {
|
|
532
|
-
width: width.toString(),
|
|
533
|
-
height: height.toString(),
|
|
534
|
-
transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`,
|
|
535
|
-
},
|
|
536
|
-
}).then((dataUrl: string) => {
|
|
537
|
-
downloadImage(dataUrl, title);
|
|
538
|
-
// Restore controls
|
|
539
|
-
if (controls) controls.style.display = 'block';
|
|
540
|
-
});
|
|
541
|
-
}, [getNodes, downloadImage, title]);
|
|
542
|
-
|
|
543
|
-
const handleLegendClick = useCallback(
|
|
544
|
-
(collectionType: string, groupId?: string) => {
|
|
545
|
-
const updatedNodes = nodes.map((node: Node<any>) => {
|
|
546
|
-
// Check if the groupId is set first
|
|
547
|
-
if (groupId && node.data.group && node.data.group?.id === groupId) {
|
|
548
|
-
return { ...node, style: { ...node.style, opacity: 1 } };
|
|
549
|
-
} else {
|
|
550
|
-
if (node.type === collectionType) {
|
|
551
|
-
return { ...node, style: { ...node.style, opacity: 1 } };
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
return { ...node, style: { ...node.style, opacity: 0.1 } };
|
|
555
|
-
});
|
|
556
|
-
|
|
557
|
-
const updatedEdges = edges.map((edge) => {
|
|
558
|
-
return {
|
|
559
|
-
...edge,
|
|
560
|
-
data: { ...edge.data, opacity: 0.1 },
|
|
561
|
-
style: { ...edge.style, opacity: 0.1 },
|
|
562
|
-
labelStyle: { ...edge.labelStyle, opacity: 0.1 },
|
|
563
|
-
animated: animateMessages,
|
|
564
|
-
};
|
|
565
|
-
});
|
|
566
|
-
|
|
567
|
-
setNodes(updatedNodes);
|
|
568
|
-
setEdges(updatedEdges);
|
|
569
|
-
|
|
570
|
-
fitView({
|
|
571
|
-
padding: 0.2,
|
|
572
|
-
duration: 800,
|
|
573
|
-
nodes: updatedNodes.filter((node) => node.type === collectionType),
|
|
574
|
-
});
|
|
575
|
-
},
|
|
576
|
-
[nodes, edges, setNodes, setEdges, fitView]
|
|
577
|
-
);
|
|
578
|
-
|
|
579
|
-
const getNodesByCollectionWithColors = useCallback((nodes: Node<any>[]) => {
|
|
580
|
-
const colorClasses = {
|
|
581
|
-
events: 'bg-orange-600',
|
|
582
|
-
services: 'bg-pink-600',
|
|
583
|
-
flows: 'bg-teal-600',
|
|
584
|
-
commands: 'bg-blue-600',
|
|
585
|
-
queries: 'bg-green-600',
|
|
586
|
-
channels: 'bg-gray-600',
|
|
587
|
-
externalSystem: 'bg-pink-600',
|
|
588
|
-
actor: 'bg-yellow-500',
|
|
589
|
-
step: 'bg-gray-700',
|
|
590
|
-
data: 'bg-blue-600',
|
|
591
|
-
'data-products': 'bg-indigo-600',
|
|
592
|
-
};
|
|
593
|
-
|
|
594
|
-
let legendForDomains: { [key: string]: { count: number; colorClass: string; groupId: string } } = {};
|
|
595
|
-
|
|
596
|
-
// Find any groups
|
|
597
|
-
const domainGroups = [
|
|
598
|
-
...new Set(
|
|
599
|
-
nodes.filter((node) => node.data.group && node.data.group?.type === 'Domain').map((node) => node.data.group?.id)
|
|
600
|
-
),
|
|
601
|
-
];
|
|
602
|
-
|
|
603
|
-
domainGroups.forEach((groupId) => {
|
|
604
|
-
const group = nodes.filter((node) => node.data.group && node.data.group?.id === groupId);
|
|
605
|
-
legendForDomains[`${groupId} (Domain)`] = { count: group.length, colorClass: 'bg-yellow-600', groupId };
|
|
606
|
-
});
|
|
607
|
-
|
|
608
|
-
const legendForNodes = nodes.reduce(
|
|
609
|
-
(acc: { [key: string]: { count: number; colorClass: string; groupId?: string } }, node) => {
|
|
610
|
-
const collection = node.type;
|
|
611
|
-
if (collection) {
|
|
612
|
-
if (acc[collection]) {
|
|
613
|
-
acc[collection].count += 1;
|
|
614
|
-
} else {
|
|
615
|
-
acc[collection] = { count: 1, colorClass: colorClasses[collection as keyof typeof colorClasses] || 'bg-black' };
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
return acc;
|
|
619
|
-
},
|
|
620
|
-
{}
|
|
621
|
-
);
|
|
622
|
-
|
|
623
|
-
return { ...legendForDomains, ...legendForNodes };
|
|
624
|
-
}, []);
|
|
625
|
-
|
|
626
|
-
const legend = getNodesByCollectionWithColors(nodes);
|
|
627
|
-
|
|
628
|
-
const handleStepChange = useCallback(
|
|
629
|
-
(nodeId: string | null, highlightPaths?: string[], shouldZoomOut?: boolean) => {
|
|
630
|
-
if (nodeId === null) {
|
|
631
|
-
// Reset all nodes and edges
|
|
632
|
-
resetNodesAndEdges();
|
|
633
|
-
setActiveStepIndex(null);
|
|
634
|
-
|
|
635
|
-
// If shouldZoomOut is true, fit the entire view
|
|
636
|
-
if (shouldZoomOut) {
|
|
637
|
-
setTimeout(() => {
|
|
638
|
-
fitView({ duration: 800, padding: 0.1 });
|
|
639
|
-
}, 100);
|
|
640
|
-
}
|
|
641
|
-
return;
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
const activeNode = nodes.find((node: Node) => node.id === nodeId);
|
|
645
|
-
if (!activeNode) return;
|
|
646
|
-
|
|
647
|
-
// Create set of highlighted nodes and edges
|
|
648
|
-
const highlightedNodeIds = new Set<string>();
|
|
649
|
-
const highlightedEdgeIds = new Set<string>();
|
|
650
|
-
|
|
651
|
-
// Add current node
|
|
652
|
-
highlightedNodeIds.add(activeNode.id);
|
|
653
|
-
|
|
654
|
-
// Add incoming edges and their source nodes
|
|
655
|
-
edges.forEach((edge: Edge) => {
|
|
656
|
-
if (edge.target === activeNode.id) {
|
|
657
|
-
highlightedEdgeIds.add(edge.id);
|
|
658
|
-
highlightedNodeIds.add(edge.source);
|
|
659
|
-
}
|
|
660
|
-
});
|
|
661
|
-
|
|
662
|
-
// Add outgoing edges
|
|
663
|
-
if (highlightPaths) {
|
|
664
|
-
// Highlight all possible paths when at a fork
|
|
665
|
-
highlightPaths.forEach((pathId) => {
|
|
666
|
-
const [source, target] = pathId.split('-');
|
|
667
|
-
edges.forEach((edge: Edge) => {
|
|
668
|
-
if (edge.source === source && edge.target === target) {
|
|
669
|
-
highlightedEdgeIds.add(edge.id);
|
|
670
|
-
highlightedNodeIds.add(edge.target);
|
|
671
|
-
}
|
|
672
|
-
});
|
|
673
|
-
});
|
|
674
|
-
} else {
|
|
675
|
-
// Highlight all outgoing edges normally
|
|
676
|
-
edges.forEach((edge: Edge) => {
|
|
677
|
-
if (edge.source === activeNode.id) {
|
|
678
|
-
highlightedEdgeIds.add(edge.id);
|
|
679
|
-
highlightedNodeIds.add(edge.target);
|
|
680
|
-
}
|
|
681
|
-
});
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
// Update nodes
|
|
685
|
-
const updatedNodes = nodes.map((node: Node) => {
|
|
686
|
-
if (highlightedNodeIds.has(node.id)) {
|
|
687
|
-
return { ...node, style: { ...node.style, opacity: 1 } };
|
|
688
|
-
}
|
|
689
|
-
return { ...node, style: { ...node.style, opacity: 0.2 } };
|
|
690
|
-
});
|
|
691
|
-
|
|
692
|
-
// Update edges
|
|
693
|
-
const updatedEdges = edges.map((edge: Edge) => {
|
|
694
|
-
if (highlightedEdgeIds.has(edge.id)) {
|
|
695
|
-
return {
|
|
696
|
-
...edge,
|
|
697
|
-
data: { ...edge.data, opacity: 1, animated: true },
|
|
698
|
-
style: { ...edge.style, opacity: 1, strokeWidth: 3 },
|
|
699
|
-
labelStyle: { ...edge.labelStyle, opacity: 1 },
|
|
700
|
-
animated: true,
|
|
701
|
-
};
|
|
702
|
-
}
|
|
703
|
-
return {
|
|
704
|
-
...edge,
|
|
705
|
-
data: { ...edge.data, opacity: 0.2, animated: false },
|
|
706
|
-
style: { ...edge.style, opacity: 0.2, strokeWidth: 2 },
|
|
707
|
-
labelStyle: { ...edge.labelStyle, opacity: 0.2 },
|
|
708
|
-
animated: false,
|
|
709
|
-
};
|
|
710
|
-
});
|
|
711
|
-
|
|
712
|
-
setNodes(updatedNodes);
|
|
713
|
-
setEdges(updatedEdges);
|
|
714
|
-
|
|
715
|
-
// Fit view to active node
|
|
716
|
-
fitView({
|
|
717
|
-
padding: 0.4,
|
|
718
|
-
duration: 800,
|
|
719
|
-
nodes: [activeNode],
|
|
720
|
-
});
|
|
721
|
-
},
|
|
722
|
-
[nodes, edges, setNodes, setEdges, resetNodesAndEdges, fitView]
|
|
723
|
-
);
|
|
724
|
-
|
|
725
|
-
// Check if this is a flow visualization by checking if edges use flow-edge type
|
|
726
|
-
const isFlowVisualization = edges.some((edge: Edge) => edge.type === 'flow-edge');
|
|
727
|
-
|
|
728
|
-
return (
|
|
729
|
-
<div ref={reactFlowWrapperRef} className="w-full h-full bg-gray-50 flex flex-col">
|
|
730
|
-
{isMermaidView ? (
|
|
731
|
-
<>
|
|
732
|
-
{/* Menu Bar for Mermaid View */}
|
|
733
|
-
<div className="w-full pr-6 flex space-x-2 justify-between items-center bg-[rgb(var(--ec-page-bg))] border-b border-[rgb(var(--ec-page-border))] p-4">
|
|
734
|
-
<div className="flex space-x-2 ml-4">
|
|
735
|
-
{/* Settings Dropdown Menu */}
|
|
736
|
-
<DropdownMenu.Root>
|
|
737
|
-
<DropdownMenu.Trigger asChild>
|
|
738
|
-
<button
|
|
739
|
-
className="py-2.5 px-4 bg-[rgb(var(--ec-page-bg))] hover:bg-[rgb(var(--ec-accent-subtle)/0.4)] border border-[rgb(var(--ec-page-border))] rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[rgb(var(--ec-accent))] flex items-center gap-3 transition-all duration-200 hover:border-[rgb(var(--ec-accent)/0.3)] group whitespace-nowrap"
|
|
740
|
-
aria-label="Open menu"
|
|
741
|
-
>
|
|
742
|
-
{title && (
|
|
743
|
-
<span className="text-base font-medium text-[rgb(var(--ec-page-text))] leading-tight">{title}</span>
|
|
744
|
-
)}
|
|
745
|
-
<MoreVertical className="h-5 w-5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0 group-hover:text-[rgb(var(--ec-accent))] transition-colors duration-150" />
|
|
746
|
-
</button>
|
|
747
|
-
</DropdownMenu.Trigger>
|
|
748
|
-
<DropdownMenu.Portal>
|
|
749
|
-
<DropdownMenu.Content
|
|
750
|
-
className="min-w-56 bg-[rgb(var(--ec-page-bg))] border border-[rgb(var(--ec-page-border))] rounded-lg shadow-xl z-50 py-1.5 animate-in fade-in zoom-in-95 duration-200"
|
|
751
|
-
sideOffset={0}
|
|
752
|
-
align="end"
|
|
753
|
-
alignOffset={-180}
|
|
754
|
-
>
|
|
755
|
-
<DropdownMenu.Arrow className="fill-[rgb(var(--ec-page-bg))] stroke-[rgb(var(--ec-page-border))] stroke-1" />
|
|
756
|
-
<VisualizerDropdownContent
|
|
757
|
-
isMermaidView={isMermaidView}
|
|
758
|
-
setIsMermaidView={setIsMermaidView}
|
|
759
|
-
animateMessages={animateMessages}
|
|
760
|
-
toggleAnimateMessages={toggleAnimateMessages}
|
|
761
|
-
hideChannels={hideChannels}
|
|
762
|
-
toggleChannelsVisibility={toggleChannelsVisibility}
|
|
763
|
-
hasChannels={hasChannels}
|
|
764
|
-
showMinimap={showMinimap}
|
|
765
|
-
setShowMinimap={setShowMinimap}
|
|
766
|
-
handleFitView={handleFitView}
|
|
767
|
-
searchRef={searchRef}
|
|
768
|
-
isChatEnabled={isChatEnabled}
|
|
769
|
-
openChat={openChat}
|
|
770
|
-
handleCopyArchitectureCode={handleCopyArchitectureCode}
|
|
771
|
-
handleExportVisual={handleExportVisual}
|
|
772
|
-
setIsShareModalOpen={setIsShareModalOpen}
|
|
773
|
-
toggleFullScreen={toggleFullScreen}
|
|
774
|
-
openStudioModal={openStudioModal}
|
|
775
|
-
isDevMode={isDevMode}
|
|
776
|
-
onSaveLayout={handleSaveLayout}
|
|
777
|
-
onResetLayout={handleResetLayout}
|
|
778
|
-
/>
|
|
779
|
-
</DropdownMenu.Content>
|
|
780
|
-
</DropdownMenu.Portal>
|
|
781
|
-
</DropdownMenu.Root>
|
|
782
|
-
</div>
|
|
783
|
-
{mode === 'full' && showSearch && (
|
|
784
|
-
<div className="flex justify-end items-center gap-2">
|
|
785
|
-
{!isMermaidView && (
|
|
786
|
-
<div className="w-96">
|
|
787
|
-
<VisualiserSearch ref={searchRef} nodes={nodes} onNodeSelect={handleNodeSelect} onClear={handleSearchClear} />
|
|
788
|
-
</div>
|
|
789
|
-
)}
|
|
790
|
-
</div>
|
|
791
|
-
)}
|
|
792
|
-
</div>
|
|
793
|
-
{/* Mermaid View */}
|
|
794
|
-
<div className="flex-1 overflow-hidden">
|
|
795
|
-
<MermaidView nodes={nodes} edges={edges} maxTextSize={maxTextSize} />
|
|
796
|
-
</div>
|
|
797
|
-
</>
|
|
798
|
-
) : (
|
|
799
|
-
<ReactFlow
|
|
800
|
-
nodeTypes={nodeTypes}
|
|
801
|
-
edgeTypes={edgeTypes}
|
|
802
|
-
minZoom={0.07}
|
|
803
|
-
nodes={nodes}
|
|
804
|
-
edges={edges}
|
|
805
|
-
fitView
|
|
806
|
-
onNodesChange={handleNodesChange}
|
|
807
|
-
onEdgesChange={onEdgesChange}
|
|
808
|
-
connectionLineType={ConnectionLineType.SmoothStep}
|
|
809
|
-
nodeOrigin={[0.1, 0.1]}
|
|
810
|
-
onNodeClick={handleNodeClick}
|
|
811
|
-
onPaneClick={handlePaneClick}
|
|
812
|
-
zoomOnScroll={zoomOnScroll}
|
|
813
|
-
className="relative"
|
|
814
|
-
>
|
|
815
|
-
<Panel position="top-center" className="w-full pr-6 ">
|
|
816
|
-
<div className="flex space-x-2 justify-between items-center">
|
|
817
|
-
<div className="flex space-x-2 ml-4">
|
|
818
|
-
{/* Settings Dropdown Menu */}
|
|
819
|
-
<DropdownMenu.Root>
|
|
820
|
-
<DropdownMenu.Trigger asChild>
|
|
821
|
-
<button
|
|
822
|
-
className="py-2.5 px-4 bg-[rgb(var(--ec-page-bg))] hover:bg-[rgb(var(--ec-accent-subtle)/0.4)] border border-[rgb(var(--ec-page-border))] rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[rgb(var(--ec-accent))] flex items-center gap-3 transition-all duration-200 hover:border-[rgb(var(--ec-accent)/0.3)] group whitespace-nowrap"
|
|
823
|
-
aria-label="Open menu"
|
|
824
|
-
>
|
|
825
|
-
{title && (
|
|
826
|
-
<span className="text-base font-medium text-[rgb(var(--ec-page-text))] leading-tight">{title}</span>
|
|
827
|
-
)}
|
|
828
|
-
<MoreVertical className="h-5 w-5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0 group-hover:text-[rgb(var(--ec-accent))] transition-colors duration-150" />
|
|
829
|
-
</button>
|
|
830
|
-
</DropdownMenu.Trigger>
|
|
831
|
-
<DropdownMenu.Portal>
|
|
832
|
-
<DropdownMenu.Content
|
|
833
|
-
className="min-w-56 bg-[rgb(var(--ec-page-bg))] border border-[rgb(var(--ec-page-border))] rounded-lg shadow-xl z-50 py-1.5 animate-in fade-in zoom-in-95 duration-200"
|
|
834
|
-
sideOffset={0}
|
|
835
|
-
align="end"
|
|
836
|
-
alignOffset={-180}
|
|
837
|
-
>
|
|
838
|
-
<DropdownMenu.Arrow className="fill-[rgb(var(--ec-page-bg))] stroke-[rgb(var(--ec-page-border))] stroke-1" />
|
|
839
|
-
<VisualizerDropdownContent
|
|
840
|
-
isMermaidView={isMermaidView}
|
|
841
|
-
setIsMermaidView={setIsMermaidView}
|
|
842
|
-
animateMessages={animateMessages}
|
|
843
|
-
toggleAnimateMessages={toggleAnimateMessages}
|
|
844
|
-
hideChannels={hideChannels}
|
|
845
|
-
toggleChannelsVisibility={toggleChannelsVisibility}
|
|
846
|
-
hasChannels={hasChannels}
|
|
847
|
-
showMinimap={showMinimap}
|
|
848
|
-
setShowMinimap={setShowMinimap}
|
|
849
|
-
handleFitView={handleFitView}
|
|
850
|
-
searchRef={searchRef}
|
|
851
|
-
isChatEnabled={isChatEnabled}
|
|
852
|
-
openChat={openChat}
|
|
853
|
-
handleCopyArchitectureCode={handleCopyArchitectureCode}
|
|
854
|
-
handleExportVisual={handleExportVisual}
|
|
855
|
-
setIsShareModalOpen={setIsShareModalOpen}
|
|
856
|
-
toggleFullScreen={toggleFullScreen}
|
|
857
|
-
openStudioModal={openStudioModal}
|
|
858
|
-
isDevMode={isDevMode}
|
|
859
|
-
onSaveLayout={handleSaveLayout}
|
|
860
|
-
onResetLayout={handleResetLayout}
|
|
861
|
-
/>
|
|
862
|
-
</DropdownMenu.Content>
|
|
863
|
-
</DropdownMenu.Portal>
|
|
864
|
-
</DropdownMenu.Root>
|
|
865
|
-
</div>
|
|
866
|
-
{mode === 'full' && showSearch && (
|
|
867
|
-
<div className="flex justify-end items-center gap-2">
|
|
868
|
-
{!isMermaidView && (
|
|
869
|
-
<div className="w-96">
|
|
870
|
-
<VisualiserSearch
|
|
871
|
-
ref={searchRef}
|
|
872
|
-
nodes={nodes}
|
|
873
|
-
onNodeSelect={handleNodeSelect}
|
|
874
|
-
onClear={handleSearchClear}
|
|
875
|
-
/>
|
|
876
|
-
</div>
|
|
877
|
-
)}
|
|
878
|
-
</div>
|
|
879
|
-
)}
|
|
880
|
-
</div>
|
|
881
|
-
{links.length > 0 && (
|
|
882
|
-
<div className="flex justify-end mt-3">
|
|
883
|
-
<div className="relative flex items-center -mt-1">
|
|
884
|
-
<span className="absolute left-2 pointer-events-none flex items-center h-full">
|
|
885
|
-
<HistoryIcon className="h-4 w-4 text-gray-600" />
|
|
886
|
-
</span>
|
|
887
|
-
<select
|
|
888
|
-
value={links.find((link) => window.location.href.includes(link.url))?.url || links[0].url}
|
|
889
|
-
onChange={(e) => navigate(e.target.value)}
|
|
890
|
-
className="appearance-none pl-7 pr-6 py-0 text-[14px] bg-white rounded-md border border-gray-200 hover:bg-gray-100/50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[rgb(var(--ec-accent))]"
|
|
891
|
-
style={{ minWidth: 120, height: '26px' }}
|
|
892
|
-
>
|
|
893
|
-
{links.map((link) => (
|
|
894
|
-
<option key={link.url} value={link.url}>
|
|
895
|
-
{link.label}
|
|
896
|
-
</option>
|
|
897
|
-
))}
|
|
898
|
-
</select>
|
|
899
|
-
<span className="absolute right-2 pointer-events-none">
|
|
900
|
-
<svg className="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
|
901
|
-
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
|
|
902
|
-
</svg>
|
|
903
|
-
</span>
|
|
904
|
-
</div>
|
|
905
|
-
</div>
|
|
906
|
-
)}
|
|
907
|
-
</Panel>
|
|
908
|
-
|
|
909
|
-
{includeBackground && <Background color="#bbb" gap={16} />}
|
|
910
|
-
{includeBackground && <Controls />}
|
|
911
|
-
{showMinimap && (
|
|
912
|
-
<MiniMap
|
|
913
|
-
nodeStrokeWidth={3}
|
|
914
|
-
zoomable
|
|
915
|
-
pannable
|
|
916
|
-
style={{
|
|
917
|
-
backgroundColor: 'rgb(var(--ec-page-bg))',
|
|
918
|
-
border: '1px solid rgb(var(--ec-page-border))',
|
|
919
|
-
borderRadius: '8px',
|
|
920
|
-
}}
|
|
921
|
-
/>
|
|
922
|
-
)}
|
|
923
|
-
{isFlowVisualization && showFlowWalkthrough && (
|
|
924
|
-
<Panel position="bottom-left">
|
|
925
|
-
<StepWalkthrough
|
|
926
|
-
nodes={nodes}
|
|
927
|
-
edges={edges}
|
|
928
|
-
isFlowVisualization={isFlowVisualization}
|
|
929
|
-
onStepChange={handleStepChange}
|
|
930
|
-
mode={mode}
|
|
931
|
-
/>
|
|
932
|
-
</Panel>
|
|
933
|
-
)}
|
|
934
|
-
{/* Dev Mode: Layout change indicator */}
|
|
935
|
-
{isDevMode && hasLayoutChanges && (
|
|
936
|
-
<Panel
|
|
937
|
-
position="bottom-left"
|
|
938
|
-
style={
|
|
939
|
-
isFlowVisualization && showFlowWalkthrough
|
|
940
|
-
? { marginBottom: '20px', marginLeft: '410px' }
|
|
941
|
-
: { marginLeft: '60px' }
|
|
942
|
-
}
|
|
943
|
-
>
|
|
944
|
-
<div className="bg-[rgb(var(--ec-card-bg))] border border-[rgb(var(--ec-page-border))] rounded-lg shadow-md px-3 py-2 flex items-center gap-3">
|
|
945
|
-
<span className="text-xs text-[rgb(var(--ec-page-text-muted))]">Layout changed</span>
|
|
946
|
-
<button
|
|
947
|
-
onClick={handleQuickSaveLayout}
|
|
948
|
-
disabled={isSavingLayout}
|
|
949
|
-
className="text-xs font-medium text-[rgb(var(--ec-accent-text))] bg-[rgb(var(--ec-accent-subtle))] hover:bg-[rgb(var(--ec-accent-subtle)/0.7)] px-2 py-1 rounded transition-colors disabled:opacity-50"
|
|
950
|
-
>
|
|
951
|
-
{isSavingLayout ? 'Saving...' : 'Save'}
|
|
952
|
-
</button>
|
|
953
|
-
</div>
|
|
954
|
-
</Panel>
|
|
955
|
-
)}
|
|
956
|
-
{includeKey && (
|
|
957
|
-
<Panel position="bottom-right" style={showMinimap ? { marginRight: '230px' } : undefined}>
|
|
958
|
-
<div className=" bg-white font-light px-4 text-[12px] shadow-md py-1 rounded-md">
|
|
959
|
-
<ul className="m-0 p-0 ">
|
|
960
|
-
{Object.entries(legend).map(([key, { count, colorClass, groupId }]) => (
|
|
961
|
-
<li
|
|
962
|
-
key={key}
|
|
963
|
-
className="flex space-x-2 items-center text-[10px] cursor-pointer hover:text-[rgb(var(--ec-accent))] hover:underline"
|
|
964
|
-
onClick={() => handleLegendClick(key, groupId)}
|
|
965
|
-
>
|
|
966
|
-
<span className={`w-2 h-2 block ${colorClass}`} />
|
|
967
|
-
<span className="block capitalize">
|
|
968
|
-
{key} ({count})
|
|
969
|
-
</span>
|
|
970
|
-
</li>
|
|
971
|
-
))}
|
|
972
|
-
</ul>
|
|
973
|
-
</div>
|
|
974
|
-
</Panel>
|
|
975
|
-
)}
|
|
976
|
-
</ReactFlow>
|
|
977
|
-
)}
|
|
978
|
-
<StudioModal isOpen={isStudioModalOpen || false} onClose={() => setIsStudioModalOpen(false)} />
|
|
979
|
-
<FocusModeModal
|
|
980
|
-
isOpen={focusModeOpen}
|
|
981
|
-
onClose={() => setFocusModeOpen(false)}
|
|
982
|
-
initialNodeId={focusedNodeId}
|
|
983
|
-
nodes={nodes}
|
|
984
|
-
edges={edges}
|
|
985
|
-
nodeTypes={nodeTypes}
|
|
986
|
-
edgeTypes={edgeTypes}
|
|
987
|
-
/>
|
|
988
|
-
|
|
989
|
-
{/* Share Link Modal */}
|
|
990
|
-
{isShareModalOpen && (
|
|
991
|
-
<>
|
|
992
|
-
<div
|
|
993
|
-
className="fixed inset-0 bg-black/20 z-40"
|
|
994
|
-
onClick={() => setIsShareModalOpen(false)}
|
|
995
|
-
style={{ animation: 'fadeIn 150ms ease-out' }}
|
|
996
|
-
/>
|
|
997
|
-
<div
|
|
998
|
-
className="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-[rgb(var(--ec-page-bg))] rounded-lg shadow-xl z-50 w-full max-w-md p-6 border border-[rgb(var(--ec-page-border))]"
|
|
999
|
-
style={{ animation: 'slideInCenter 250ms ease-out' }}
|
|
1000
|
-
>
|
|
1001
|
-
<style>{`
|
|
1002
|
-
@keyframes fadeIn {
|
|
1003
|
-
from { opacity: 0; }
|
|
1004
|
-
to { opacity: 1; }
|
|
1005
|
-
}
|
|
1006
|
-
@keyframes slideInCenter {
|
|
1007
|
-
from { opacity: 0; transform: translate(-50%, -48%); }
|
|
1008
|
-
to { opacity: 1; transform: translate(-50%, -50%); }
|
|
1009
|
-
}
|
|
1010
|
-
`}</style>
|
|
1011
|
-
|
|
1012
|
-
<div className="flex justify-between items-start mb-4">
|
|
1013
|
-
<h3 className="text-lg font-semibold text-[rgb(var(--ec-page-text))]">Share Link</h3>
|
|
1014
|
-
<button
|
|
1015
|
-
onClick={() => setIsShareModalOpen(false)}
|
|
1016
|
-
className="text-[rgb(var(--ec-page-text-muted))] hover:text-[rgb(var(--ec-page-text))] transition-colors"
|
|
1017
|
-
aria-label="Close modal"
|
|
1018
|
-
>
|
|
1019
|
-
<ExternalLink className="w-5 h-5 rotate-180" />
|
|
1020
|
-
</button>
|
|
1021
|
-
</div>
|
|
1022
|
-
|
|
1023
|
-
<p className="text-sm text-[rgb(var(--ec-page-text-muted))] mb-4">
|
|
1024
|
-
Share this link with your team to let them view this visualization.
|
|
1025
|
-
</p>
|
|
1026
|
-
|
|
1027
|
-
<div className="flex gap-2">
|
|
1028
|
-
<input
|
|
1029
|
-
type="text"
|
|
1030
|
-
readOnly
|
|
1031
|
-
value={typeof window !== 'undefined' ? window.location.href : ''}
|
|
1032
|
-
className="flex-1 px-3 py-2.5 bg-[rgb(var(--ec-input-bg))] border border-[rgb(var(--ec-input-border))] rounded-md text-[rgb(var(--ec-input-text))] text-sm focus:outline-none focus:ring-2 focus:ring-[rgb(var(--ec-accent))]"
|
|
1033
|
-
/>
|
|
1034
|
-
<button
|
|
1035
|
-
onClick={handleCopyShareUrl}
|
|
1036
|
-
className={`px-4 py-2.5 rounded-md font-medium transition-all duration-200 flex items-center gap-2 ${
|
|
1037
|
-
shareUrlCopySuccess ? 'bg-green-500 text-white' : 'bg-[rgb(var(--ec-accent))] text-white hover:opacity-90'
|
|
1038
|
-
}`}
|
|
1039
|
-
aria-label={shareUrlCopySuccess ? 'Copied!' : 'Copy link'}
|
|
1040
|
-
>
|
|
1041
|
-
{shareUrlCopySuccess ? <CheckIcon className="w-4 h-4" /> : <ClipboardIcon className="w-4 h-4" />}
|
|
1042
|
-
<span>{shareUrlCopySuccess ? 'Copied!' : 'Copy'}</span>
|
|
1043
|
-
</button>
|
|
1044
|
-
</div>
|
|
1045
|
-
</div>
|
|
1046
|
-
</>
|
|
1047
|
-
)}
|
|
1048
|
-
</div>
|
|
1049
|
-
);
|
|
1050
|
-
};
|
|
1051
|
-
|
|
1052
|
-
interface NodeGraphProps {
|
|
1053
|
-
id: string;
|
|
1054
|
-
title?: string;
|
|
1055
|
-
href?: string;
|
|
1056
|
-
hrefLabel?: string;
|
|
1057
|
-
nodes: Node[];
|
|
1058
|
-
edges: Edge[];
|
|
1059
|
-
linkTo: 'docs' | 'visualiser';
|
|
1060
|
-
includeKey?: boolean;
|
|
1061
|
-
footerLabel?: string;
|
|
1062
|
-
linksToVisualiser?: boolean;
|
|
1063
|
-
links?: { label: string; url: string }[];
|
|
1064
|
-
mode?: 'full' | 'simple';
|
|
1065
|
-
portalId?: string;
|
|
1066
|
-
showFlowWalkthrough?: boolean;
|
|
1067
|
-
showSearch?: boolean;
|
|
1068
|
-
zoomOnScroll?: boolean;
|
|
1069
|
-
designId?: string;
|
|
1070
|
-
isChatEnabled?: boolean;
|
|
1071
|
-
maxTextSize?: number;
|
|
1072
|
-
isDevMode?: boolean;
|
|
1073
|
-
resourceKey?: string;
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
const NodeGraph = ({
|
|
1077
|
-
id,
|
|
1078
|
-
nodes,
|
|
1079
|
-
edges,
|
|
1080
|
-
title,
|
|
1081
|
-
href,
|
|
1082
|
-
linkTo = 'docs',
|
|
1083
|
-
hrefLabel = 'Open in visualizer',
|
|
1084
|
-
includeKey = true,
|
|
1085
|
-
footerLabel,
|
|
1086
|
-
linksToVisualiser = false,
|
|
1087
|
-
links = [],
|
|
1088
|
-
mode = 'full',
|
|
1089
|
-
portalId,
|
|
1090
|
-
showFlowWalkthrough = true,
|
|
1091
|
-
showSearch = true,
|
|
1092
|
-
zoomOnScroll = false,
|
|
1093
|
-
designId,
|
|
1094
|
-
isChatEnabled = false,
|
|
1095
|
-
maxTextSize,
|
|
1096
|
-
isDevMode = false,
|
|
1097
|
-
resourceKey,
|
|
1098
|
-
}: NodeGraphProps) => {
|
|
1099
|
-
const [elem, setElem] = useState(null);
|
|
1100
|
-
const [showFooter, setShowFooter] = useState(true);
|
|
1101
|
-
const [isStudioModalOpen, setIsStudioModalOpen] = useState(false);
|
|
1102
|
-
|
|
1103
|
-
const openStudioModal = useCallback(() => {
|
|
1104
|
-
setIsStudioModalOpen(true);
|
|
1105
|
-
}, []);
|
|
1106
|
-
|
|
1107
|
-
const containerToRenderInto = portalId || `${id}-portal`;
|
|
1108
|
-
|
|
1109
|
-
useEffect(() => {
|
|
1110
|
-
// @ts-ignore
|
|
1111
|
-
setElem(document.getElementById(containerToRenderInto));
|
|
1112
|
-
}, []);
|
|
1113
|
-
|
|
1114
|
-
useEffect(() => {
|
|
1115
|
-
const urlParams = new URLSearchParams(window.location.search);
|
|
1116
|
-
const embed = urlParams.get('embed');
|
|
1117
|
-
if (embed === 'true') {
|
|
1118
|
-
setShowFooter(false);
|
|
1119
|
-
}
|
|
1120
|
-
}, []);
|
|
1121
|
-
|
|
1122
|
-
if (!elem) return null;
|
|
1123
|
-
|
|
1124
|
-
return (
|
|
1125
|
-
<div>
|
|
1126
|
-
{createPortal(
|
|
1127
|
-
<ReactFlowProvider>
|
|
1128
|
-
<NodeGraphBuilder
|
|
1129
|
-
edges={edges}
|
|
1130
|
-
nodes={nodes}
|
|
1131
|
-
title={title}
|
|
1132
|
-
linkTo={linkTo}
|
|
1133
|
-
includeKey={includeKey}
|
|
1134
|
-
linksToVisualiser={linksToVisualiser}
|
|
1135
|
-
links={links}
|
|
1136
|
-
mode={mode}
|
|
1137
|
-
showFlowWalkthrough={showFlowWalkthrough}
|
|
1138
|
-
showSearch={showSearch}
|
|
1139
|
-
zoomOnScroll={zoomOnScroll}
|
|
1140
|
-
designId={designId || id}
|
|
1141
|
-
isStudioModalOpen={isStudioModalOpen}
|
|
1142
|
-
setIsStudioModalOpen={setIsStudioModalOpen}
|
|
1143
|
-
isChatEnabled={isChatEnabled}
|
|
1144
|
-
maxTextSize={maxTextSize}
|
|
1145
|
-
isDevMode={isDevMode}
|
|
1146
|
-
resourceKey={resourceKey}
|
|
1147
|
-
/>
|
|
1148
|
-
|
|
1149
|
-
{showFooter && (
|
|
1150
|
-
<div className="flex justify-between" id="visualiser-footer">
|
|
1151
|
-
{footerLabel && (
|
|
1152
|
-
<div className="py-2 w-full text-left ">
|
|
1153
|
-
<span className=" text-sm no-underline py-2 text-gray-500">{footerLabel}</span>
|
|
1154
|
-
</div>
|
|
1155
|
-
)}
|
|
1156
|
-
|
|
1157
|
-
{href && (
|
|
1158
|
-
<div className="py-2 w-full text-right flex justify-between">
|
|
1159
|
-
{/* <span className="text-sm text-gray-500 italic">Right click a node to access documentation</span> */}
|
|
1160
|
-
<button
|
|
1161
|
-
onClick={openStudioModal}
|
|
1162
|
-
className=" text-sm underline text-gray-800 hover:text-primary flex items-center space-x-1"
|
|
1163
|
-
>
|
|
1164
|
-
<span>Open in EventCatalog Studio</span>
|
|
1165
|
-
<ExternalLink className="w-3 h-3" />
|
|
1166
|
-
</button>
|
|
1167
|
-
<a className=" text-sm underline text-gray-800 hover:text-primary" href={href}>
|
|
1168
|
-
{hrefLabel} →
|
|
1169
|
-
</a>
|
|
1170
|
-
</div>
|
|
1171
|
-
)}
|
|
1172
|
-
</div>
|
|
1173
|
-
)}
|
|
1174
|
-
</ReactFlowProvider>,
|
|
1175
|
-
elem
|
|
1176
|
-
)}
|
|
1177
|
-
</div>
|
|
1178
|
-
);
|
|
1179
|
-
};
|
|
1180
|
-
|
|
1181
|
-
export default NodeGraph;
|