@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.
Files changed (83) hide show
  1. package/dist/analytics/analytics.cjs +1 -1
  2. package/dist/analytics/analytics.js +2 -2
  3. package/dist/analytics/log-build.cjs +1 -1
  4. package/dist/analytics/log-build.js +3 -3
  5. package/dist/{chunk-DINAMVEI.js → chunk-7RCUF3VG.js} +1 -1
  6. package/dist/{chunk-GOLMKUV3.js → chunk-AY2OEUWV.js} +1 -1
  7. package/dist/{chunk-6MBAYHHT.js → chunk-NXATPLVB.js} +1 -1
  8. package/dist/{chunk-JUWMXGCI.js → chunk-V3GX7FC3.js} +1 -1
  9. package/dist/{chunk-ROHEB5DM.js → chunk-VXTATPGX.js} +1 -1
  10. package/dist/constants.cjs +1 -1
  11. package/dist/constants.js +1 -1
  12. package/dist/eventcatalog.cjs +1 -1
  13. package/dist/eventcatalog.js +5 -5
  14. package/dist/generate.cjs +1 -1
  15. package/dist/generate.js +3 -3
  16. package/dist/utils/cli-logger.cjs +1 -1
  17. package/dist/utils/cli-logger.js +2 -2
  18. package/eventcatalog/src/components/MDX/Design/Design.astro +2 -2
  19. package/eventcatalog/src/components/MDX/EntityMap/EntityMap.astro +2 -2
  20. package/eventcatalog/src/components/MDX/File.tsx +7 -7
  21. package/eventcatalog/src/components/MDX/Flow/Flow.astro +2 -2
  22. package/eventcatalog/src/components/MDX/NodeGraph/AstroNodeGraph.tsx +104 -0
  23. package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.astro +2 -2
  24. package/eventcatalog/src/components/MDX/NodeGraph/README.md +85 -0
  25. package/eventcatalog/src/components/MDX/Schema.astro +3 -6
  26. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +1 -6
  27. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/spec/[filename].astro +2 -2
  28. package/eventcatalog/src/pages/visualiser/designs/[id]/index.astro +2 -2
  29. package/eventcatalog/src/utils/collections/channels.ts +1 -26
  30. package/eventcatalog/src/utils/collections/commands.ts +1 -26
  31. package/eventcatalog/src/utils/collections/containers.ts +1 -32
  32. package/eventcatalog/src/utils/collections/data-products.ts +1 -19
  33. package/eventcatalog/src/utils/collections/diagrams.ts +0 -7
  34. package/eventcatalog/src/utils/collections/domains.ts +0 -18
  35. package/eventcatalog/src/utils/collections/entities.ts +1 -27
  36. package/eventcatalog/src/utils/collections/events.ts +1 -22
  37. package/eventcatalog/src/utils/collections/flows.ts +0 -8
  38. package/eventcatalog/src/utils/collections/queries.ts +1 -22
  39. package/eventcatalog/src/utils/collections/schemas.ts +9 -4
  40. package/eventcatalog/src/utils/collections/services.ts +0 -20
  41. package/eventcatalog/src/utils/collections/teams.ts +0 -6
  42. package/eventcatalog/src/utils/collections/users.ts +0 -6
  43. package/eventcatalog/src/utils/collections/util.ts +10 -1
  44. package/eventcatalog/src/utils/node-graphs/container-node-graph.ts +66 -17
  45. package/eventcatalog/src/utils/node-graphs/data-products-node-graph.ts +14 -5
  46. package/eventcatalog/src/utils/node-graphs/domains-node-graph.ts +1 -1
  47. package/eventcatalog/src/utils/node-graphs/message-node-graph.ts +133 -18
  48. package/eventcatalog/src/utils/node-graphs/services-node-graph.ts +36 -14
  49. package/eventcatalog/src/utils/node-graphs/utils/utils.ts +115 -4
  50. package/package.json +4 -4
  51. package/eventcatalog/src/components/MDX/NodeGraph/DownloadButton.tsx +0 -62
  52. package/eventcatalog/src/components/MDX/NodeGraph/Edges/AnimatedMessageEdge.tsx +0 -110
  53. package/eventcatalog/src/components/MDX/NodeGraph/Edges/FlowEdge.tsx +0 -96
  54. package/eventcatalog/src/components/MDX/NodeGraph/Edges/MultilineEdgeLabel.tsx +0 -52
  55. package/eventcatalog/src/components/MDX/NodeGraph/FocusMode/FocusModeContent.tsx +0 -294
  56. package/eventcatalog/src/components/MDX/NodeGraph/FocusMode/FocusModeNodeActions.tsx +0 -92
  57. package/eventcatalog/src/components/MDX/NodeGraph/FocusMode/FocusModePlaceholder.tsx +0 -26
  58. package/eventcatalog/src/components/MDX/NodeGraph/FocusMode/utils.ts +0 -163
  59. package/eventcatalog/src/components/MDX/NodeGraph/FocusModeModal.tsx +0 -99
  60. package/eventcatalog/src/components/MDX/NodeGraph/MermaidView.tsx +0 -242
  61. package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.tsx +0 -1181
  62. package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Actor.tsx +0 -46
  63. package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Channel.tsx +0 -55
  64. package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Command.tsx +0 -27
  65. package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Custom.tsx +0 -159
  66. package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Data.tsx +0 -63
  67. package/eventcatalog/src/components/MDX/NodeGraph/Nodes/DataProduct.tsx +0 -132
  68. package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Domain.tsx +0 -155
  69. package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Entity.tsx +0 -154
  70. package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Event.tsx +0 -29
  71. package/eventcatalog/src/components/MDX/NodeGraph/Nodes/ExternalSystem.tsx +0 -79
  72. package/eventcatalog/src/components/MDX/NodeGraph/Nodes/ExternalSystem2.tsx +0 -24
  73. package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Flow.tsx +0 -107
  74. package/eventcatalog/src/components/MDX/NodeGraph/Nodes/MessageContextMenu.tsx +0 -63
  75. package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Query.tsx +0 -28
  76. package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Service.tsx +0 -127
  77. package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Step.tsx +0 -64
  78. package/eventcatalog/src/components/MDX/NodeGraph/Nodes/User.tsx +0 -76
  79. package/eventcatalog/src/components/MDX/NodeGraph/Nodes/View.tsx +0 -24
  80. package/eventcatalog/src/components/MDX/NodeGraph/StepWalkthrough.tsx +0 -296
  81. package/eventcatalog/src/components/MDX/NodeGraph/StudioModal.tsx +0 -129
  82. package/eventcatalog/src/components/MDX/NodeGraph/VisualiserSearch.tsx +0 -258
  83. 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} &rarr;
1169
- </a>
1170
- </div>
1171
- )}
1172
- </div>
1173
- )}
1174
- </ReactFlowProvider>,
1175
- elem
1176
- )}
1177
- </div>
1178
- );
1179
- };
1180
-
1181
- export default NodeGraph;