@eventcatalog/core 3.10.2 → 3.12.0

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 (41) 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-K7EPHS7S.js → chunk-G7GG3HEB.js} +1 -1
  6. package/dist/{chunk-DGE5ITBR.js → chunk-K44BXVHU.js} +1 -1
  7. package/dist/{chunk-IJKRHWWO.js → chunk-LXOS3MXQ.js} +1 -1
  8. package/dist/{chunk-QWSUFHCT.js → chunk-VUBZ6A7B.js} +1 -1
  9. package/dist/{chunk-AL67CV2N.js → chunk-WVKLG26T.js} +1 -1
  10. package/dist/constants.cjs +1 -1
  11. package/dist/constants.js +1 -1
  12. package/dist/eventcatalog.cjs +2 -1
  13. package/dist/eventcatalog.js +6 -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/integrations/eventcatalog-features.ts +13 -0
  19. package/eventcatalog/src/components/MDX/Design/Design.astro +10 -2
  20. package/eventcatalog/src/components/MDX/EntityMap/EntityMap.astro +10 -2
  21. package/eventcatalog/src/components/MDX/Flow/Flow.astro +10 -2
  22. package/eventcatalog/src/components/MDX/NodeGraph/Edges/AnimatedMessageEdge.tsx +13 -0
  23. package/eventcatalog/src/components/MDX/NodeGraph/FocusMode/FocusModeContent.tsx +294 -0
  24. package/eventcatalog/src/components/MDX/NodeGraph/FocusMode/FocusModeNodeActions.tsx +92 -0
  25. package/eventcatalog/src/components/MDX/NodeGraph/FocusMode/FocusModePlaceholder.tsx +26 -0
  26. package/eventcatalog/src/components/MDX/NodeGraph/FocusMode/utils.ts +163 -0
  27. package/eventcatalog/src/components/MDX/NodeGraph/FocusModeModal.tsx +99 -0
  28. package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.astro +10 -2
  29. package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.tsx +166 -43
  30. package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Entity.tsx +4 -1
  31. package/eventcatalog/src/components/MDX/NodeGraph/Nodes/MessageContextMenu.tsx +4 -1
  32. package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Service.tsx +4 -1
  33. package/eventcatalog/src/components/MDX/NodeGraph/VisualizerDropdownContent.tsx +91 -2
  34. package/eventcatalog/src/enterprise/visualizer-layout/reset.ts +45 -0
  35. package/eventcatalog/src/enterprise/visualizer-layout/save.ts +57 -0
  36. package/eventcatalog/src/layouts/Footer.astro +4 -1
  37. package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +3 -1
  38. package/eventcatalog/src/utils/feature.ts +2 -0
  39. package/eventcatalog/src/utils/node-graphs/layout-persistence.ts +81 -0
  40. package/eventcatalog/tailwind.config.mjs +10 -0
  41. package/package.json +1 -1
@@ -0,0 +1,92 @@
1
+ import React from 'react';
2
+ import { NodeToolbar, Position, useViewport, type Node } from '@xyflow/react';
3
+ import { ArrowRightLeft, FileText } from 'lucide-react';
4
+ import { getNodeDocUrl } from './utils';
5
+ import { buildUrl } from '@utils/url-builder';
6
+
7
+ interface FocusModeNodeActionsProps {
8
+ node: Node;
9
+ isCenter: boolean;
10
+ onSwitch: (nodeId: string, direction: 'left' | 'right') => void;
11
+ }
12
+
13
+ const FocusModeNodeActions: React.FC<FocusModeNodeActionsProps> = ({ node, isCenter, onSwitch }) => {
14
+ const { zoom } = useViewport();
15
+
16
+ // Don't show actions for placeholder nodes
17
+ if (node.type === 'placeholder') return null;
18
+
19
+ const docUrl = getNodeDocUrl(node);
20
+ const direction = (node.position?.x ?? 0) < 0 ? 'left' : 'right';
21
+
22
+ // Scale sizes based on zoom (inverse relationship - smaller when zoomed out)
23
+ const baseButtonSize = 32;
24
+ const baseIconSize = 16;
25
+ const scaleFactor = Math.max(0.6, Math.min(1, zoom));
26
+ const buttonSize = Math.round(baseButtonSize * scaleFactor);
27
+ const iconSize = Math.round(baseIconSize * scaleFactor);
28
+
29
+ const handleSwitch = (e: React.MouseEvent) => {
30
+ e.stopPropagation();
31
+ onSwitch(node.id, direction);
32
+ };
33
+
34
+ const handleDocClick = (e: React.MouseEvent) => {
35
+ e.stopPropagation();
36
+ if (docUrl) {
37
+ window.location.href = buildUrl(docUrl);
38
+ }
39
+ };
40
+
41
+ // Center node only shows docs icon (if available)
42
+ if (isCenter) {
43
+ if (!docUrl) return null;
44
+ return (
45
+ <NodeToolbar nodeId={node.id} position={Position.Bottom} isVisible={true} offset={-16}>
46
+ <div
47
+ className="flex items-center gap-1 bg-[rgb(var(--ec-card-bg,var(--ec-page-bg)))] border border-[rgb(var(--ec-page-border))] rounded-lg shadow-md"
48
+ style={{ padding: Math.round(4 * scaleFactor) }}
49
+ >
50
+ <button
51
+ onClick={handleDocClick}
52
+ className="flex items-center justify-center rounded-md text-[rgb(var(--ec-icon-color))] hover:text-[rgb(var(--ec-accent))] hover:bg-[rgb(var(--ec-accent-subtle))] transition-colors"
53
+ style={{ width: buttonSize, height: buttonSize }}
54
+ title="View documentation"
55
+ >
56
+ <FileText style={{ width: iconSize, height: iconSize }} />
57
+ </button>
58
+ </div>
59
+ </NodeToolbar>
60
+ );
61
+ }
62
+
63
+ return (
64
+ <NodeToolbar nodeId={node.id} position={Position.Bottom} isVisible={true} offset={-16}>
65
+ <div
66
+ className="flex items-center gap-1 bg-[rgb(var(--ec-card-bg,var(--ec-page-bg)))] border border-[rgb(var(--ec-page-border))] rounded-lg shadow-md"
67
+ style={{ padding: Math.round(4 * scaleFactor) }}
68
+ >
69
+ {docUrl && (
70
+ <button
71
+ onClick={handleDocClick}
72
+ className="flex items-center justify-center rounded-md text-[rgb(var(--ec-icon-color))] hover:text-[rgb(var(--ec-accent))] hover:bg-[rgb(var(--ec-accent-subtle))] transition-colors"
73
+ style={{ width: buttonSize, height: buttonSize }}
74
+ title="View documentation"
75
+ >
76
+ <FileText style={{ width: iconSize, height: iconSize }} />
77
+ </button>
78
+ )}
79
+ <button
80
+ onClick={handleSwitch}
81
+ className="flex items-center justify-center rounded-md text-[rgb(var(--ec-icon-color))] hover:text-[rgb(var(--ec-accent))] hover:bg-[rgb(var(--ec-accent-subtle))] transition-colors"
82
+ style={{ width: buttonSize, height: buttonSize }}
83
+ title="Focus on this node"
84
+ >
85
+ <ArrowRightLeft style={{ width: iconSize, height: iconSize }} />
86
+ </button>
87
+ </div>
88
+ </NodeToolbar>
89
+ );
90
+ };
91
+
92
+ export default FocusModeNodeActions;
@@ -0,0 +1,26 @@
1
+ import React from 'react';
2
+ import { Handle, Position } from '@xyflow/react';
3
+
4
+ interface FocusModePlaceholderProps {
5
+ data: {
6
+ label: string;
7
+ side: 'left' | 'right';
8
+ };
9
+ }
10
+
11
+ const FocusModePlaceholder: React.FC<FocusModePlaceholderProps> = ({ data }) => {
12
+ const { label, side } = data;
13
+
14
+ return (
15
+ <div
16
+ className="px-4 py-4 rounded-lg border-2 border-dashed border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-page-bg)/0.5)] max-w-[280px] flex items-center justify-center"
17
+ style={{ opacity: 0.6, minHeight: '130px' }}
18
+ >
19
+ {side === 'right' && <Handle type="target" position={Position.Left} style={{ visibility: 'hidden' }} />}
20
+ <div className="text-center text-sm text-[rgb(var(--ec-page-text-muted))] italic">{label}</div>
21
+ {side === 'left' && <Handle type="source" position={Position.Right} style={{ visibility: 'hidden' }} />}
22
+ </div>
23
+ );
24
+ };
25
+
26
+ export default FocusModePlaceholder;
@@ -0,0 +1,163 @@
1
+ import type { Node, Edge } from '@xyflow/react';
2
+
3
+ export interface NodeDisplayInfo {
4
+ id: string;
5
+ name: string;
6
+ type: string;
7
+ version?: string;
8
+ description?: string;
9
+ }
10
+
11
+ export interface ConnectedNodes {
12
+ leftNodes: Node[];
13
+ rightNodes: Node[];
14
+ }
15
+
16
+ export const NODE_COLOR_CLASSES: Record<string, string> = {
17
+ events: 'bg-orange-600',
18
+ services: 'bg-pink-600',
19
+ flows: 'bg-teal-600',
20
+ commands: 'bg-blue-600',
21
+ queries: 'bg-green-600',
22
+ channels: 'bg-gray-600',
23
+ externalSystem: 'bg-pink-600',
24
+ actor: 'bg-yellow-500',
25
+ step: 'bg-gray-700',
26
+ data: 'bg-blue-600',
27
+ 'data-products': 'bg-indigo-600',
28
+ domains: 'bg-yellow-600',
29
+ entities: 'bg-purple-600',
30
+ };
31
+
32
+ export const NODE_TYPE_LABELS: Record<string, string> = {
33
+ events: 'Event',
34
+ services: 'Service',
35
+ flows: 'Flow',
36
+ commands: 'Command',
37
+ queries: 'Query',
38
+ channels: 'Channel',
39
+ externalSystem: 'External System',
40
+ actor: 'Actor',
41
+ step: 'Step',
42
+ data: 'Data',
43
+ 'data-products': 'Data Product',
44
+ domains: 'Domain',
45
+ entities: 'Entity',
46
+ };
47
+
48
+ /**
49
+ * Get connected nodes for a given center node
50
+ * Left nodes: incoming edges (they send TO this node)
51
+ * Right nodes: outgoing edges (this node sends TO them)
52
+ */
53
+ export function getConnectedNodes(centerNodeId: string, nodes: Node[], edges: Edge[]): ConnectedNodes {
54
+ const leftIds = new Set<string>();
55
+ const rightIds = new Set<string>();
56
+
57
+ edges.forEach((edge) => {
58
+ if (edge.target === centerNodeId) {
59
+ leftIds.add(edge.source);
60
+ }
61
+ if (edge.source === centerNodeId) {
62
+ rightIds.add(edge.target);
63
+ }
64
+ });
65
+
66
+ return {
67
+ leftNodes: nodes.filter((n) => leftIds.has(n.id)),
68
+ rightNodes: nodes.filter((n) => rightIds.has(n.id)),
69
+ };
70
+ }
71
+
72
+ // Entity keys that follow the standard data structure pattern
73
+ const ENTITY_KEYS = ['service', 'message', 'flow', 'channel', 'domain', 'entity', 'dataProduct'] as const;
74
+
75
+ /**
76
+ * Extract display information from a ReactFlow node
77
+ */
78
+ export function getNodeDisplayInfo(node: Node): NodeDisplayInfo {
79
+ const nodeType = node.type || 'unknown';
80
+ const data = node.data as any;
81
+
82
+ // Find the entity in data using standard keys
83
+ const entityKey = ENTITY_KEYS.find((key) => data[key]);
84
+ const entity = entityKey ? data[entityKey] : null;
85
+
86
+ const name = entity?.data?.name || entity?.id || data.label || data.name || node.id;
87
+ const version = entity?.data?.version || entity?.version || data.version || '';
88
+ const description = entity?.data?.summary || entity?.data?.description || data.summary || data.description || '';
89
+
90
+ return {
91
+ id: node.id,
92
+ name,
93
+ type: nodeType,
94
+ version,
95
+ description: description ? truncateDescription(description, 100) : undefined,
96
+ };
97
+ }
98
+
99
+ /**
100
+ * Truncate description to a max length
101
+ */
102
+ function truncateDescription(text: string, maxLength: number): string {
103
+ if (text.length <= maxLength) return text;
104
+ return text.slice(0, maxLength).trim() + '...';
105
+ }
106
+
107
+ /**
108
+ * Get the color class for a node type
109
+ */
110
+ export function getNodeColorClass(nodeType: string): string {
111
+ return NODE_COLOR_CLASSES[nodeType] || 'bg-gray-500';
112
+ }
113
+
114
+ /**
115
+ * Get the display label for a node type
116
+ */
117
+ export function getNodeTypeLabel(nodeType: string): string {
118
+ return NODE_TYPE_LABELS[nodeType] || nodeType;
119
+ }
120
+
121
+ // Mapping from entity key to doc path
122
+ const DOC_PATH_MAP: Record<string, string> = {
123
+ service: 'services',
124
+ flow: 'flows',
125
+ channel: 'channels',
126
+ domain: 'domains',
127
+ entity: 'entities',
128
+ dataProduct: 'data-products',
129
+ };
130
+
131
+ /**
132
+ * Get the documentation URL for a node
133
+ */
134
+ export function getNodeDocUrl(node: Node): string | null {
135
+ const nodeType = node.type || 'unknown';
136
+ const data = node.data as any;
137
+
138
+ // Handle message type separately due to type mapping
139
+ if (data.message) {
140
+ const id = data.message.data?.id || data.message.id || '';
141
+ const version = data.message.data?.version || data.message.version || '';
142
+ const collectionType = nodeType === 'events' ? 'events' : nodeType === 'commands' ? 'commands' : 'queries';
143
+ return id && version ? `/docs/${collectionType}/${id}/${version}` : null;
144
+ }
145
+
146
+ // Handle data/container nodes with nested data.data structure
147
+ if (data.data && nodeType === 'data') {
148
+ const id = data.data.id || '';
149
+ const version = data.data.version || '';
150
+ return id && version ? `/docs/containers/${id}/${version}` : null;
151
+ }
152
+
153
+ // Handle standard entity types
154
+ for (const [key, path] of Object.entries(DOC_PATH_MAP)) {
155
+ if (data[key]) {
156
+ const id = data[key].data?.id || data[key].id || '';
157
+ const version = data[key].data?.version || data[key].version || '';
158
+ return id && version ? `/docs/${path}/${id}/${version}` : null;
159
+ }
160
+ }
161
+
162
+ return null;
163
+ }
@@ -0,0 +1,99 @@
1
+ import React, { useState, useCallback, useEffect } from 'react';
2
+ import * as Dialog from '@radix-ui/react-dialog';
3
+ import { XIcon, FocusIcon } from 'lucide-react';
4
+ import { ReactFlowProvider, type Node, type Edge, type NodeTypes, type EdgeTypes } from '@xyflow/react';
5
+ import FocusModeContent from './FocusMode/FocusModeContent';
6
+ import { getNodeDisplayInfo } from './FocusMode/utils';
7
+
8
+ interface FocusModeModalProps {
9
+ isOpen: boolean;
10
+ onClose: () => void;
11
+ initialNodeId: string | null;
12
+ nodes: Node[];
13
+ edges: Edge[];
14
+ nodeTypes: NodeTypes;
15
+ edgeTypes: EdgeTypes;
16
+ }
17
+
18
+ const FocusModeModal: React.FC<FocusModeModalProps> = ({
19
+ isOpen,
20
+ onClose,
21
+ initialNodeId,
22
+ nodes,
23
+ edges,
24
+ nodeTypes,
25
+ edgeTypes,
26
+ }) => {
27
+ const [centerNodeId, setCenterNodeId] = useState<string | null>(initialNodeId);
28
+
29
+ // Reset center node when modal opens with new initial node
30
+ useEffect(() => {
31
+ if (isOpen && initialNodeId) {
32
+ setCenterNodeId(initialNodeId);
33
+ }
34
+ }, [isOpen, initialNodeId]);
35
+
36
+ const handleSwitchCenter = useCallback((newCenterNodeId: string, _direction: 'left' | 'right') => {
37
+ setCenterNodeId(newCenterNodeId);
38
+ }, []);
39
+
40
+ // Get center node info for title
41
+ const centerNode = centerNodeId ? nodes.find((n) => n.id === centerNodeId) : null;
42
+ const centerNodeInfo = centerNode ? getNodeDisplayInfo(centerNode) : null;
43
+
44
+ if (!centerNodeId) {
45
+ return null;
46
+ }
47
+
48
+ return (
49
+ <Dialog.Root open={isOpen} onOpenChange={(open) => !open && onClose()}>
50
+ <Dialog.Portal container={typeof document !== 'undefined' ? document.body : undefined}>
51
+ <div className="fixed inset-0 z-[99999]" style={{ isolation: 'isolate' }}>
52
+ <Dialog.Overlay className="fixed inset-0 bg-black/70 data-[state=open]:animate-overlayShow" />
53
+ <Dialog.Content className="fixed inset-4 md:inset-8 lg:inset-12 rounded-lg bg-[rgb(var(--ec-card-bg,var(--ec-page-bg)))] shadow-xl focus:outline-none data-[state=open]:animate-contentShow flex flex-col overflow-hidden">
54
+ {/* Header */}
55
+ <div className="flex items-center justify-between px-6 py-4 border-b border-[rgb(var(--ec-page-border))] flex-shrink-0">
56
+ <div className="flex items-center gap-3">
57
+ <div className="flex items-center justify-center w-10 h-10 rounded-lg bg-[rgb(var(--ec-accent-subtle))]">
58
+ <FocusIcon className="w-5 h-5 text-[rgb(var(--ec-accent))]" />
59
+ </div>
60
+ <div>
61
+ <Dialog.Title className="text-lg font-semibold text-[rgb(var(--ec-page-text))]">Focus Mode</Dialog.Title>
62
+ <Dialog.Description className="text-sm text-[rgb(var(--ec-page-text-muted))]">
63
+ {centerNodeInfo
64
+ ? `Exploring: ${centerNodeInfo.name} - Click on connected nodes to navigate`
65
+ : 'Explore node connections'}
66
+ </Dialog.Description>
67
+ </div>
68
+ </div>
69
+ <Dialog.Close asChild>
70
+ <button
71
+ className="flex items-center justify-center w-10 h-10 rounded-lg text-[rgb(var(--ec-icon-color))] hover:text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-content-hover,var(--ec-page-border)/0.5))] transition-colors"
72
+ aria-label="Close"
73
+ >
74
+ <XIcon className="w-5 h-5" />
75
+ </button>
76
+ </Dialog.Close>
77
+ </div>
78
+
79
+ {/* Content */}
80
+ <div className="flex-1 overflow-hidden">
81
+ <ReactFlowProvider>
82
+ <FocusModeContent
83
+ centerNodeId={centerNodeId}
84
+ nodes={nodes}
85
+ edges={edges}
86
+ nodeTypes={nodeTypes}
87
+ edgeTypes={edgeTypes}
88
+ onSwitchCenter={handleSwitchCenter}
89
+ />
90
+ </ReactFlowProvider>
91
+ </div>
92
+ </Dialog.Content>
93
+ </div>
94
+ </Dialog.Portal>
95
+ </Dialog.Root>
96
+ );
97
+ };
98
+
99
+ export default FocusModeModal;
@@ -19,7 +19,8 @@ import { getVersionFromCollection } from '@utils/collections/versions';
19
19
  import { pageDataLoader } from '@utils/page-loaders/page-data-loader';
20
20
  import { getNodesAndEdges as getNodesAndEdgesForContainer } from '@utils/node-graphs/container-node-graph';
21
21
  import config from '@config';
22
- import { isEventCatalogChatEnabled } from '@utils/feature';
22
+ import { isEventCatalogChatEnabled, isDevMode } from '@utils/feature';
23
+ import { loadSavedLayout, applyLayoutToNodes, buildResourceKey } from '@utils/node-graphs/layout-persistence';
23
24
 
24
25
  const isChatEnabled = isEventCatalogChatEnabled();
25
26
 
@@ -139,12 +140,17 @@ if (collection === 'services-containers') {
139
140
  nodes = fetchedNodes;
140
141
  edges = fetchedEdges;
141
142
  }
143
+
144
+ // Load and apply saved layout if it exists (dev mode feature)
145
+ const resourceKey = buildResourceKey(collection, id, version);
146
+ const savedLayout = await loadSavedLayout(resourceKey);
147
+ const nodesWithLayout = applyLayoutToNodes(nodes as any[], savedLayout);
142
148
  ---
143
149
 
144
150
  <div>
145
151
  <NodeGraphNew
146
152
  id={id}
147
- nodes={nodes}
153
+ nodes={nodesWithLayout}
148
154
  edges={edges}
149
155
  title={title}
150
156
  hrefLabel={href?.label}
@@ -159,6 +165,8 @@ if (collection === 'services-containers') {
159
165
  zoomOnScroll={zoomOnScroll}
160
166
  isChatEnabled={isChatEnabled}
161
167
  maxTextSize={config.mermaid?.maxTextSize}
168
+ isDevMode={isDevMode()}
169
+ resourceKey={resourceKey}
162
170
  />
163
171
  </div>
164
172