@eventcatalog/core 2.44.5 → 2.45.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.
- 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-ULCIAC3E.js → chunk-DRD67V64.js} +1 -1
- package/dist/{chunk-WO7XQZNY.js → chunk-L4YKSQ4D.js} +1 -1
- package/dist/{chunk-WG64IAI3.js → chunk-XBSAAKTV.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog.cjs +1 -1
- package/dist/eventcatalog.js +3 -3
- package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.astro +7 -0
- package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.tsx +2 -0
- package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Domain.tsx +155 -0
- package/eventcatalog/src/components/MDX/NodeGraph/VisualiserSearch.tsx +50 -9
- package/eventcatalog/src/components/SideNav/ListViewSideBar/index.tsx +29 -3
- package/eventcatalog/src/components/SideNav/ListViewSideBar/types.ts +11 -3
- package/eventcatalog/src/components/SideNav/ListViewSideBar/utils.ts +6 -2
- package/eventcatalog/src/pages/visualiser/domain-integrations/index.astro +31 -0
- package/eventcatalog/src/utils/node-graphs/domains-canvas.ts +251 -0
- package/package.json +1 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
log_build_default
|
|
3
|
-
} from "../chunk-
|
|
4
|
-
import "../chunk-
|
|
5
|
-
import "../chunk-
|
|
3
|
+
} from "../chunk-L4YKSQ4D.js";
|
|
4
|
+
import "../chunk-DRD67V64.js";
|
|
5
|
+
import "../chunk-XBSAAKTV.js";
|
|
6
6
|
import "../chunk-E7TXTI7G.js";
|
|
7
7
|
export {
|
|
8
8
|
log_build_default as default
|
package/dist/constants.cjs
CHANGED
package/dist/constants.js
CHANGED
package/dist/eventcatalog.cjs
CHANGED
package/dist/eventcatalog.js
CHANGED
|
@@ -6,8 +6,8 @@ import {
|
|
|
6
6
|
} from "./chunk-DCLTVJDP.js";
|
|
7
7
|
import {
|
|
8
8
|
log_build_default
|
|
9
|
-
} from "./chunk-
|
|
10
|
-
import "./chunk-
|
|
9
|
+
} from "./chunk-L4YKSQ4D.js";
|
|
10
|
+
import "./chunk-DRD67V64.js";
|
|
11
11
|
import {
|
|
12
12
|
catalogToAstro,
|
|
13
13
|
checkAndConvertMdToMdx
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
import "./chunk-EXAALOQA.js";
|
|
16
16
|
import {
|
|
17
17
|
VERSION
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-XBSAAKTV.js";
|
|
19
19
|
import {
|
|
20
20
|
isAuthEnabled,
|
|
21
21
|
isBackstagePluginEnabled,
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
getNodesAndEdges as getNodesAndEdgesForDomain,
|
|
11
11
|
getNodesAndEdgesForDomainContextMap,
|
|
12
12
|
} from '@utils/node-graphs/domains-node-graph';
|
|
13
|
+
import { getDomainsCanvasData } from '@utils/node-graphs/domains-canvas';
|
|
13
14
|
import { getNodesAndEdges as getNodesAndEdgesForFlows } from '@utils/node-graphs/flows-node-graph';
|
|
14
15
|
import { buildUrl } from '@utils/url-builder';
|
|
15
16
|
import { getVersionFromCollection } from '@utils/collections/versions';
|
|
@@ -89,6 +90,12 @@ if (collection === 'domain-context-map') {
|
|
|
89
90
|
nodes = fetchedNodes;
|
|
90
91
|
edges = fetchedEdges;
|
|
91
92
|
}
|
|
93
|
+
|
|
94
|
+
if (collection === 'domains-canvas') {
|
|
95
|
+
const { domainNodes, messageNodes, edges: fetchedEdges } = await getDomainsCanvasData();
|
|
96
|
+
nodes = [...domainNodes, ...messageNodes];
|
|
97
|
+
edges = fetchedEdges;
|
|
98
|
+
}
|
|
92
99
|
---
|
|
93
100
|
|
|
94
101
|
<div>
|
|
@@ -28,6 +28,7 @@ import UserNode from './Nodes/User';
|
|
|
28
28
|
import StepNode from './Nodes/Step';
|
|
29
29
|
import CommandNode from './Nodes/Command';
|
|
30
30
|
import ExternalSystemNode from './Nodes/ExternalSystem';
|
|
31
|
+
import DomainNode from './Nodes/Domain';
|
|
31
32
|
import AnimatedMessageEdge from './Edges/AnimatedMessageEdge';
|
|
32
33
|
import FlowEdge from './Edges/FlowEdge';
|
|
33
34
|
import CustomNode from './Nodes/Custom';
|
|
@@ -78,6 +79,7 @@ const NodeGraphBuilder = ({
|
|
|
78
79
|
channels: ChannelNode,
|
|
79
80
|
queries: QueryNode,
|
|
80
81
|
commands: CommandNode,
|
|
82
|
+
domains: DomainNode,
|
|
81
83
|
step: StepNode,
|
|
82
84
|
user: UserNode,
|
|
83
85
|
actor: UserNode,
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { CollectionEntry } from 'astro:content';
|
|
2
|
+
import { Handle, useReactFlow, useOnSelectionChange, Position } from '@xyflow/react';
|
|
3
|
+
import * as ContextMenu from '@radix-ui/react-context-menu';
|
|
4
|
+
import { buildUrl } from '@utils/url-builder';
|
|
5
|
+
import { getIcon } from '@utils/badges';
|
|
6
|
+
import { useState } from 'react';
|
|
7
|
+
|
|
8
|
+
interface Data {
|
|
9
|
+
mode: 'simple' | 'full';
|
|
10
|
+
domain: CollectionEntry<'domains'>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default function DomainNode({ data, id: nodeId }: any) {
|
|
14
|
+
const { mode, domain } = data as Data;
|
|
15
|
+
const reactFlow = useReactFlow();
|
|
16
|
+
const [highlightedServices, setHighlightedServices] = useState<Set<string>>(new Set());
|
|
17
|
+
|
|
18
|
+
const { id, version, name, services = [], styles } = domain.data;
|
|
19
|
+
const { icon = 'RectangleGroupIcon' } = styles || {};
|
|
20
|
+
|
|
21
|
+
const Icon = getIcon(icon);
|
|
22
|
+
const ServerIcon = getIcon('ServerIcon');
|
|
23
|
+
|
|
24
|
+
// Listen for selection changes to highlight connected services
|
|
25
|
+
useOnSelectionChange({
|
|
26
|
+
onChange: ({ nodes: selectedNodes }) => {
|
|
27
|
+
if (selectedNodes.length === 0) {
|
|
28
|
+
setHighlightedServices(new Set());
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const selectedNode = selectedNodes[0];
|
|
33
|
+
if (!selectedNode) {
|
|
34
|
+
setHighlightedServices(new Set());
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Get all edges
|
|
39
|
+
const edges = reactFlow.getEdges();
|
|
40
|
+
const connectedServiceIds = new Set<string>();
|
|
41
|
+
|
|
42
|
+
// Find services connected to the selected node
|
|
43
|
+
edges.forEach((edge) => {
|
|
44
|
+
if (edge.source === selectedNode.id || edge.target === selectedNode.id) {
|
|
45
|
+
// Check if this edge connects to our domain
|
|
46
|
+
if (edge.source === nodeId && edge.sourceHandle) {
|
|
47
|
+
// Extract service ID from sourceHandle (format: "serviceId-source")
|
|
48
|
+
const serviceId = edge.sourceHandle.replace('-source', '');
|
|
49
|
+
connectedServiceIds.add(serviceId);
|
|
50
|
+
}
|
|
51
|
+
if (edge.target === nodeId && edge.targetHandle) {
|
|
52
|
+
// Extract service ID from targetHandle (format: "serviceId-target")
|
|
53
|
+
const serviceId = edge.targetHandle.replace('-target', '');
|
|
54
|
+
connectedServiceIds.add(serviceId);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
setHighlightedServices(connectedServiceIds);
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<ContextMenu.Root>
|
|
65
|
+
<ContextMenu.Trigger>
|
|
66
|
+
<div className="w-full rounded-lg border-2 border-yellow-400 bg-white shadow-lg">
|
|
67
|
+
<div className="bg-yellow-100 px-3 py-2 flex items-center space-x-2">
|
|
68
|
+
{Icon && <Icon className="w-4 h-4 text-yellow-700" />}
|
|
69
|
+
<div>
|
|
70
|
+
<span className="text-sm font-bold text-yellow-900">{name}</span>
|
|
71
|
+
<span className="text-xs text-yellow-700 ml-2">v{version}</span>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
{mode === 'full' && services.length > 0 && (
|
|
75
|
+
<div>
|
|
76
|
+
{services.map((service: any, index: number) => {
|
|
77
|
+
const isHighlighted = highlightedServices.has(service.data.id);
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<ContextMenu.Root key={`${service.data.id}-${index}`}>
|
|
81
|
+
<ContextMenu.Trigger asChild>
|
|
82
|
+
<div
|
|
83
|
+
className={`relative flex items-center justify-between px-3 py-2 cursor-pointer ${index !== services.length - 1 ? 'border-b border-gray-300' : ''} ${isHighlighted ? 'bg-pink-100 border-pink-300' : ''}`}
|
|
84
|
+
>
|
|
85
|
+
<Handle
|
|
86
|
+
type="target"
|
|
87
|
+
position={Position.Left}
|
|
88
|
+
id={`${service.data.id}-target`}
|
|
89
|
+
className="!left-[-1px] !w-2 !h-2 !bg-gray-400 !border !border-gray-500 !rounded-full !z-10"
|
|
90
|
+
style={{ left: '-1px' }}
|
|
91
|
+
/>
|
|
92
|
+
<Handle
|
|
93
|
+
type="source"
|
|
94
|
+
position={Position.Right}
|
|
95
|
+
id={`${service.data.id}-source`}
|
|
96
|
+
className="!right-[-1px] !w-2 !h-2 !bg-gray-400 !border !border-gray-500 !rounded-full !z-10"
|
|
97
|
+
style={{ right: '-1px' }}
|
|
98
|
+
/>
|
|
99
|
+
<div className="flex items-center space-x-3">
|
|
100
|
+
<div className="flex items-center justify-center w-5 h-5 bg-pink-500 rounded">
|
|
101
|
+
{ServerIcon && <ServerIcon className="w-3 h-3 text-white" />}
|
|
102
|
+
</div>
|
|
103
|
+
<span className="text-sm font-medium text-gray-900">{service.data.name || service.data.id}</span>
|
|
104
|
+
</div>
|
|
105
|
+
<div className="flex items-center space-x-4 text-sm text-gray-600">
|
|
106
|
+
<span className="text-xs">v{service.data.version}</span>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
</ContextMenu.Trigger>
|
|
110
|
+
<ContextMenu.Portal>
|
|
111
|
+
<ContextMenu.Content className="min-w-[220px] bg-white rounded-md p-1 shadow-md border border-gray-200">
|
|
112
|
+
<ContextMenu.Item
|
|
113
|
+
className="text-sm px-2 py-1.5 outline-none cursor-pointer hover:bg-orange-100 rounded-sm flex items-center"
|
|
114
|
+
onClick={() =>
|
|
115
|
+
(window.location.href = buildUrl(`/docs/services/${service.data.id}/${service.data.version}`))
|
|
116
|
+
}
|
|
117
|
+
>
|
|
118
|
+
View Service Documentation
|
|
119
|
+
</ContextMenu.Item>
|
|
120
|
+
<ContextMenu.Item
|
|
121
|
+
className="text-sm px-2 py-1.5 outline-none cursor-pointer hover:bg-orange-100 rounded-sm flex items-center"
|
|
122
|
+
onClick={() =>
|
|
123
|
+
(window.location.href = buildUrl(`/visualiser/services/${service.data.id}/${service.data.version}`))
|
|
124
|
+
}
|
|
125
|
+
>
|
|
126
|
+
View Service Visualizer
|
|
127
|
+
</ContextMenu.Item>
|
|
128
|
+
</ContextMenu.Content>
|
|
129
|
+
</ContextMenu.Portal>
|
|
130
|
+
</ContextMenu.Root>
|
|
131
|
+
);
|
|
132
|
+
})}
|
|
133
|
+
</div>
|
|
134
|
+
)}
|
|
135
|
+
</div>
|
|
136
|
+
</ContextMenu.Trigger>
|
|
137
|
+
<ContextMenu.Portal>
|
|
138
|
+
<ContextMenu.Content className="min-w-[220px] bg-white rounded-md p-1 shadow-md border border-gray-200">
|
|
139
|
+
<ContextMenu.Item
|
|
140
|
+
className="text-sm px-2 py-1.5 outline-none cursor-pointer hover:bg-orange-100 rounded-sm flex items-center"
|
|
141
|
+
onClick={() => (window.location.href = buildUrl(`/docs/domains/${id}/${version}`))}
|
|
142
|
+
>
|
|
143
|
+
View Domain Documentation
|
|
144
|
+
</ContextMenu.Item>
|
|
145
|
+
<ContextMenu.Item
|
|
146
|
+
className="text-sm px-2 py-1.5 outline-none cursor-pointer hover:bg-orange-100 rounded-sm flex items-center"
|
|
147
|
+
onClick={() => (window.location.href = buildUrl(`/visualiser/domains/${id}/${version}`))}
|
|
148
|
+
>
|
|
149
|
+
View Domain Visualizer
|
|
150
|
+
</ContextMenu.Item>
|
|
151
|
+
</ContextMenu.Content>
|
|
152
|
+
</ContextMenu.Portal>
|
|
153
|
+
</ContextMenu.Root>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
@@ -1,9 +1,42 @@
|
|
|
1
1
|
import { useState, useCallback, useRef, useEffect, forwardRef, useImperativeHandle } from 'react';
|
|
2
2
|
import type { Node } from '@xyflow/react';
|
|
3
3
|
|
|
4
|
+
// Define interfaces for different node data structures
|
|
5
|
+
interface MessageData {
|
|
6
|
+
name: string;
|
|
7
|
+
version?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface ServiceData {
|
|
11
|
+
name: string;
|
|
12
|
+
version?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface DomainData {
|
|
16
|
+
name: string;
|
|
17
|
+
version?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface NodeDataContent extends Record<string, unknown> {
|
|
21
|
+
message?: {
|
|
22
|
+
data: MessageData;
|
|
23
|
+
};
|
|
24
|
+
service?: {
|
|
25
|
+
data: ServiceData;
|
|
26
|
+
};
|
|
27
|
+
domain?: {
|
|
28
|
+
data: DomainData;
|
|
29
|
+
};
|
|
30
|
+
name?: string;
|
|
31
|
+
version?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Extend the Node type with our custom data structure
|
|
35
|
+
type CustomNode = Node<NodeDataContent>;
|
|
36
|
+
|
|
4
37
|
interface VisualiserSearchProps {
|
|
5
|
-
nodes:
|
|
6
|
-
onNodeSelect: (node:
|
|
38
|
+
nodes: CustomNode[];
|
|
39
|
+
onNodeSelect: (node: CustomNode) => void;
|
|
7
40
|
onClear: () => void;
|
|
8
41
|
onPaneClick?: () => void;
|
|
9
42
|
}
|
|
@@ -15,7 +48,7 @@ export interface VisualiserSearchRef {
|
|
|
15
48
|
const VisualiserSearch = forwardRef<VisualiserSearchRef, VisualiserSearchProps>(
|
|
16
49
|
({ nodes, onNodeSelect, onClear, onPaneClick }, ref) => {
|
|
17
50
|
const [searchQuery, setSearchQuery] = useState('');
|
|
18
|
-
const [filteredSuggestions, setFilteredSuggestions] = useState<
|
|
51
|
+
const [filteredSuggestions, setFilteredSuggestions] = useState<CustomNode[]>([]);
|
|
19
52
|
const [showSuggestions, setShowSuggestions] = useState(false);
|
|
20
53
|
const [selectedSuggestionIndex, setSelectedSuggestionIndex] = useState(-1);
|
|
21
54
|
const searchInputRef = useRef<HTMLInputElement>(null);
|
|
@@ -34,11 +67,18 @@ const VisualiserSearch = forwardRef<VisualiserSearchRef, VisualiserSearchProps>(
|
|
|
34
67
|
[hideSuggestions]
|
|
35
68
|
);
|
|
36
69
|
|
|
37
|
-
const getNodeDisplayName = useCallback((node:
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
70
|
+
const getNodeDisplayName = useCallback((node: CustomNode) => {
|
|
71
|
+
const name =
|
|
72
|
+
node.data?.message?.data?.name ||
|
|
73
|
+
node.data?.service?.data?.name ||
|
|
74
|
+
node.data?.domain?.data?.name ||
|
|
75
|
+
node.data?.name ||
|
|
76
|
+
node.id;
|
|
77
|
+
const version =
|
|
78
|
+
node.data?.message?.data?.version ||
|
|
79
|
+
node.data?.service?.data?.version ||
|
|
80
|
+
node.data?.domain?.data?.version ||
|
|
81
|
+
node.data?.version;
|
|
42
82
|
return version ? `${name} (v${version})` : name;
|
|
43
83
|
}, []);
|
|
44
84
|
|
|
@@ -50,6 +90,7 @@ const VisualiserSearch = forwardRef<VisualiserSearchRef, VisualiserSearchProps>(
|
|
|
50
90
|
commands: 'bg-blue-600 text-white',
|
|
51
91
|
queries: 'bg-green-600 text-white',
|
|
52
92
|
channels: 'bg-gray-600 text-white',
|
|
93
|
+
domains: 'bg-yellow-500 text-white',
|
|
53
94
|
externalSystem: 'bg-pink-600 text-white',
|
|
54
95
|
actor: 'bg-yellow-500 text-white',
|
|
55
96
|
step: 'bg-gray-700 text-white',
|
|
@@ -90,7 +131,7 @@ const VisualiserSearch = forwardRef<VisualiserSearchRef, VisualiserSearchProps>(
|
|
|
90
131
|
}, [nodes, searchQuery]);
|
|
91
132
|
|
|
92
133
|
const handleSuggestionClick = useCallback(
|
|
93
|
-
(node:
|
|
134
|
+
(node: CustomNode) => {
|
|
94
135
|
setSearchQuery(getNodeDisplayName(node));
|
|
95
136
|
setShowSuggestions(false);
|
|
96
137
|
onNodeSelect(node);
|
|
@@ -4,7 +4,7 @@ import { buildUrl, buildUrlWithParams } from '@utils/url-builder';
|
|
|
4
4
|
import CollapsibleGroup from './components/CollapsibleGroup';
|
|
5
5
|
import MessageList from './components/MessageList';
|
|
6
6
|
import SpecificationsList from './components/SpecificationList';
|
|
7
|
-
import type { MessageItem, ServiceItem, ListViewSideBarProps } from './types';
|
|
7
|
+
import type { MessageItem, ServiceItem, ListViewSideBarProps, DomainItem, FlowItem, Resources } from './types';
|
|
8
8
|
const STORAGE_KEY = 'EventCatalog:catalogSidebarCollapsedGroups';
|
|
9
9
|
const DEBOUNCE_DELAY = 300; // 300ms debounce delay
|
|
10
10
|
|
|
@@ -228,7 +228,7 @@ const ListViewSideBar: React.FC<ListViewSideBarProps> = ({ resources, currentPat
|
|
|
228
228
|
}, [searchTerm]);
|
|
229
229
|
|
|
230
230
|
// Filter data based on search term
|
|
231
|
-
const filteredData = useMemo(() => {
|
|
231
|
+
const filteredData: Resources = useMemo(() => {
|
|
232
232
|
if (!debouncedSearchTerm) return data;
|
|
233
233
|
|
|
234
234
|
const filterItem = (item: { label: string; id?: string }) => {
|
|
@@ -245,6 +245,7 @@ const ListViewSideBar: React.FC<ListViewSideBarProps> = ({ resources, currentPat
|
|
|
245
245
|
};
|
|
246
246
|
|
|
247
247
|
return {
|
|
248
|
+
'context-map': data['context-map']?.filter(filterItem) || [],
|
|
248
249
|
domains: data.domains?.filter(filterItem) || [],
|
|
249
250
|
services:
|
|
250
251
|
data.services
|
|
@@ -326,6 +327,7 @@ const ListViewSideBar: React.FC<ListViewSideBarProps> = ({ resources, currentPat
|
|
|
326
327
|
|
|
327
328
|
const hasNoResults =
|
|
328
329
|
debouncedSearchTerm &&
|
|
330
|
+
!filteredData['context-map']?.length &&
|
|
329
331
|
!filteredData.domains?.length &&
|
|
330
332
|
!filteredData.services?.length &&
|
|
331
333
|
!filteredData.flows?.length &&
|
|
@@ -345,9 +347,33 @@ const ListViewSideBar: React.FC<ListViewSideBarProps> = ({ resources, currentPat
|
|
|
345
347
|
<NoResultsFound searchTerm={debouncedSearchTerm} />
|
|
346
348
|
) : (
|
|
347
349
|
<>
|
|
350
|
+
{/* Bounded Context Map (Visualiser only) */}
|
|
351
|
+
{filteredData['context-map'] && filteredData.domains && filteredData.domains.length > 0 && (
|
|
352
|
+
<div className="pt-0">
|
|
353
|
+
<ul className="space-y-1">
|
|
354
|
+
{filteredData['context-map'].map((item: any) => (
|
|
355
|
+
<li key={item.href}>
|
|
356
|
+
<a
|
|
357
|
+
href={item.href}
|
|
358
|
+
className={`flex items-center justify-between px-2 py-0.5 text-xs font-bold rounded-md ${
|
|
359
|
+
decodedCurrentPath === item.href ? 'bg-purple-100 text-purple-900' : 'hover:bg-purple-100'
|
|
360
|
+
}`}
|
|
361
|
+
>
|
|
362
|
+
<span className="truncate flex flex-col items-start">
|
|
363
|
+
<HighlightedText text={item.label} searchTerm={debouncedSearchTerm} />
|
|
364
|
+
<span className="text-[10px] text-gray-500 font-light">Explore integrations between domains</span>
|
|
365
|
+
</span>
|
|
366
|
+
<span className="text-blue-600 ml-2 text-[10px] font-medium bg-blue-50 px-2 py-0.5 rounded">DOMAINS</span>
|
|
367
|
+
</a>
|
|
368
|
+
</li>
|
|
369
|
+
))}
|
|
370
|
+
</ul>
|
|
371
|
+
</div>
|
|
372
|
+
)}
|
|
373
|
+
|
|
348
374
|
{/* Domains */}
|
|
349
375
|
{filteredData['domains'] && (
|
|
350
|
-
<div>
|
|
376
|
+
<div className={`${isVisualizer ? 'pt-4 pb-2' : 'p-0'}`}>
|
|
351
377
|
<ul className="space-y-2">
|
|
352
378
|
{filteredData['domains'].map((item: any) => (
|
|
353
379
|
<li key={item.href} className="space-y-0" data-active={decodedCurrentPath === item.href}>
|
|
@@ -5,6 +5,7 @@ export interface MessageItem {
|
|
|
5
5
|
id: string;
|
|
6
6
|
direction: 'sends' | 'receives';
|
|
7
7
|
type: 'command' | 'query' | 'event';
|
|
8
|
+
collection: string;
|
|
8
9
|
data: {
|
|
9
10
|
name: string;
|
|
10
11
|
};
|
|
@@ -35,21 +36,28 @@ export interface ServiceItem {
|
|
|
35
36
|
}[];
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
interface DomainItem {
|
|
39
|
+
export interface DomainItem {
|
|
39
40
|
href: string;
|
|
40
41
|
label: string;
|
|
41
42
|
id: string;
|
|
42
43
|
name: string;
|
|
43
44
|
services: any[];
|
|
44
45
|
domains: any[];
|
|
46
|
+
entities: EntityItem[];
|
|
45
47
|
}
|
|
46
48
|
|
|
47
|
-
interface FlowItem {
|
|
49
|
+
export interface FlowItem {
|
|
48
50
|
href: string;
|
|
49
51
|
label: string;
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
interface Resources {
|
|
54
|
+
export interface Resources {
|
|
55
|
+
'context-map'?: Array<{
|
|
56
|
+
href: string;
|
|
57
|
+
label: string;
|
|
58
|
+
id: string;
|
|
59
|
+
name: string;
|
|
60
|
+
}>;
|
|
53
61
|
domains?: DomainItem[];
|
|
54
62
|
services?: ServiceItem[];
|
|
55
63
|
flows?: FlowItem[];
|
|
@@ -107,8 +107,12 @@ export async function getResourcesForNavigation({ currentPath }: { currentPath:
|
|
|
107
107
|
const sideNav = {
|
|
108
108
|
...(currentPath.includes('visualiser')
|
|
109
109
|
? {
|
|
110
|
-
'
|
|
111
|
-
{
|
|
110
|
+
'context-map': [
|
|
111
|
+
{
|
|
112
|
+
label: 'Integration Landscape',
|
|
113
|
+
href: buildUrl('/visualiser/domain-integrations'),
|
|
114
|
+
collection: 'domain-integrations',
|
|
115
|
+
},
|
|
112
116
|
],
|
|
113
117
|
}
|
|
114
118
|
: {}),
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
import NodeGraph from '@components/MDX/NodeGraph/NodeGraph.astro';
|
|
3
|
+
import VisualiserLayout from '@layouts/VisualiserLayout.astro';
|
|
4
|
+
import { buildUrl } from '@utils/url-builder';
|
|
5
|
+
import { ClientRouter } from 'astro:transitions';
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
<VisualiserLayout title={`Visualiser | Domain Architecture`} description="High-level view of all domains and their relationships">
|
|
9
|
+
<div class="bg-gray-100/50 m-4">
|
|
10
|
+
<div
|
|
11
|
+
class="h-[calc(100vh-130px)] w-full relative border border-gray-200"
|
|
12
|
+
id="domains-canvas-portal"
|
|
13
|
+
transition:animate="fade"
|
|
14
|
+
>
|
|
15
|
+
</div>
|
|
16
|
+
<NodeGraph
|
|
17
|
+
id="domains-canvas"
|
|
18
|
+
collection="domains-canvas"
|
|
19
|
+
title="Domain Integrations"
|
|
20
|
+
mode="full"
|
|
21
|
+
linkTo="visualiser"
|
|
22
|
+
version="1.0.0"
|
|
23
|
+
linksToVisualiser={false}
|
|
24
|
+
href={{
|
|
25
|
+
label: 'View Documentation',
|
|
26
|
+
url: buildUrl('/docs'),
|
|
27
|
+
}}
|
|
28
|
+
/>
|
|
29
|
+
</div>
|
|
30
|
+
<ClientRouter />
|
|
31
|
+
</VisualiserLayout>
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { getCollection, type CollectionEntry } from 'astro:content';
|
|
2
|
+
import dagre from 'dagre';
|
|
3
|
+
import { generateIdForNode, createDagreGraph, calculatedNodes, createEdge } from '@utils/node-graphs/utils/utils';
|
|
4
|
+
import { getItemsFromCollectionByIdAndSemverOrLatest } from '@utils/collections/util';
|
|
5
|
+
import type { Node, Edge } from '@xyflow/react';
|
|
6
|
+
import { getDomains } from '@utils/collections/domains';
|
|
7
|
+
|
|
8
|
+
interface DomainCanvasData {
|
|
9
|
+
domainNodes: Node[];
|
|
10
|
+
messageNodes: Node[];
|
|
11
|
+
edges: Edge[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const getDomainsCanvasData = async (): Promise<DomainCanvasData> => {
|
|
15
|
+
let domains = await getDomains({ getAllVersions: false });
|
|
16
|
+
|
|
17
|
+
// only interested in domains that are not parent domains (e.g domains that have subdoamins we dont want to display them here)
|
|
18
|
+
domains = domains.filter((domain) => !domain.data.domains?.length);
|
|
19
|
+
|
|
20
|
+
const domainNodes: Node[] = [];
|
|
21
|
+
const messageNodes: Node[] = [];
|
|
22
|
+
const edges: Edge[] = [];
|
|
23
|
+
|
|
24
|
+
// Create dagre graph for layout
|
|
25
|
+
const dagreGraph = createDagreGraph({ ranksep: 400, nodesep: 200 });
|
|
26
|
+
|
|
27
|
+
// Create a map to store domain data for calculating relationships
|
|
28
|
+
const domainDataMap = new Map();
|
|
29
|
+
|
|
30
|
+
for (let index = 0; index < domains.length; index++) {
|
|
31
|
+
const domain = domains[index];
|
|
32
|
+
const domainNodeId = generateIdForNode(domain);
|
|
33
|
+
const domainServices = domain.data.services as unknown as CollectionEntry<'services'>[];
|
|
34
|
+
|
|
35
|
+
// Count total messages for this domain
|
|
36
|
+
let totalMessages = 0;
|
|
37
|
+
for (const service of domainServices) {
|
|
38
|
+
const receives = service.data.receives ?? [];
|
|
39
|
+
const sends = service.data.sends ?? [];
|
|
40
|
+
totalMessages += receives.length + sends.length;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
domainDataMap.set(domainNodeId, {
|
|
44
|
+
domain,
|
|
45
|
+
services: domainServices,
|
|
46
|
+
servicesCount: domainServices.length,
|
|
47
|
+
messagesCount: totalMessages,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Domain Overview Node (position will be calculated by dagre)
|
|
51
|
+
domainNodes.push({
|
|
52
|
+
id: domainNodeId,
|
|
53
|
+
type: 'domains',
|
|
54
|
+
position: { x: 0, y: 0 }, // Temporary position, will be calculated by dagre
|
|
55
|
+
data: {
|
|
56
|
+
mode: 'full',
|
|
57
|
+
domain: {
|
|
58
|
+
...domain,
|
|
59
|
+
data: {
|
|
60
|
+
...domain.data,
|
|
61
|
+
services: domainServices,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
servicesCount: domainServices.length,
|
|
65
|
+
messagesCount: totalMessages,
|
|
66
|
+
},
|
|
67
|
+
sourcePosition: 'right',
|
|
68
|
+
targetPosition: 'left',
|
|
69
|
+
} as Node);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Get all messages for version resolution
|
|
73
|
+
const allMessages = await getCollection('events')
|
|
74
|
+
.then((events) => Promise.all([events, getCollection('commands'), getCollection('queries')]))
|
|
75
|
+
.then(([events, commands, queries]) => [...events, ...commands, ...queries]);
|
|
76
|
+
|
|
77
|
+
// Map to track unique messages and their publishers/consumers across domains
|
|
78
|
+
const messageRelationships = new Map<
|
|
79
|
+
string,
|
|
80
|
+
{
|
|
81
|
+
message: any;
|
|
82
|
+
publishers: Array<{ domainId: string; service: any }>;
|
|
83
|
+
consumers: Array<{ domainId: string; service: any }>;
|
|
84
|
+
}
|
|
85
|
+
>();
|
|
86
|
+
|
|
87
|
+
// Find all message relationships across domains
|
|
88
|
+
domainDataMap.forEach((domainData, domainId) => {
|
|
89
|
+
domainData.services.forEach((service: any) => {
|
|
90
|
+
// Track messages this service sends
|
|
91
|
+
const sendsRaw = service.data.sends ?? [];
|
|
92
|
+
const sendsHydrated = sendsRaw
|
|
93
|
+
.map((message: any) => getItemsFromCollectionByIdAndSemverOrLatest(allMessages, message.id, message.version))
|
|
94
|
+
.flat()
|
|
95
|
+
.filter((e: any) => e !== undefined);
|
|
96
|
+
|
|
97
|
+
sendsHydrated.forEach((sentMessage: any) => {
|
|
98
|
+
const messageKey = `${sentMessage.data.id}-${sentMessage.data.version}`;
|
|
99
|
+
|
|
100
|
+
if (!messageRelationships.has(messageKey)) {
|
|
101
|
+
messageRelationships.set(messageKey, {
|
|
102
|
+
message: sentMessage.data,
|
|
103
|
+
publishers: [],
|
|
104
|
+
consumers: [],
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const relationship = messageRelationships.get(messageKey)!;
|
|
109
|
+
// Add publisher if not already added
|
|
110
|
+
if (!relationship.publishers.some((p) => p.domainId === domainId && p.service.data.id === service.data.id)) {
|
|
111
|
+
relationship.publishers.push({ domainId, service });
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Track messages this service receives
|
|
116
|
+
const receivesRaw = service.data.receives ?? [];
|
|
117
|
+
const receivesHydrated = receivesRaw
|
|
118
|
+
.map((message: any) => getItemsFromCollectionByIdAndSemverOrLatest(allMessages, message.id, message.version))
|
|
119
|
+
.flat()
|
|
120
|
+
.filter((e: any) => e !== undefined);
|
|
121
|
+
|
|
122
|
+
receivesHydrated.forEach((receivedMessage: any) => {
|
|
123
|
+
const messageKey = `${receivedMessage.data.id}-${receivedMessage.data.version}`;
|
|
124
|
+
|
|
125
|
+
if (!messageRelationships.has(messageKey)) {
|
|
126
|
+
messageRelationships.set(messageKey, {
|
|
127
|
+
message: receivedMessage.data,
|
|
128
|
+
publishers: [],
|
|
129
|
+
consumers: [],
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const relationship = messageRelationships.get(messageKey)!;
|
|
134
|
+
// Add consumer if not already added
|
|
135
|
+
if (!relationship.consumers.some((c) => c.domainId === domainId && c.service.data.id === service.data.id)) {
|
|
136
|
+
relationship.consumers.push({ domainId, service });
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Create message nodes and edges for cross-domain relationships
|
|
143
|
+
|
|
144
|
+
// Only create message nodes for messages that cross domain boundaries
|
|
145
|
+
messageRelationships.forEach(({ message, publishers, consumers }, messageKey) => {
|
|
146
|
+
// Check if this message crosses domain boundaries
|
|
147
|
+
const publisherDomains = new Set(publishers.map((p) => p.domainId));
|
|
148
|
+
const consumerDomains = new Set(consumers.map((c) => c.domainId));
|
|
149
|
+
|
|
150
|
+
// Only create a message node if it connects different domains
|
|
151
|
+
const crossesDomainBoundary = [...publisherDomains].some((pubDomain) =>
|
|
152
|
+
[...consumerDomains].some((conDomain) => pubDomain !== conDomain)
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
if (crossesDomainBoundary) {
|
|
156
|
+
// Find the actual message object
|
|
157
|
+
const messageObject = allMessages.find((m) => m.data.id === message.id && m.data.version === message.version);
|
|
158
|
+
|
|
159
|
+
if (messageObject) {
|
|
160
|
+
const messageNodeId = `message-${messageKey}`;
|
|
161
|
+
|
|
162
|
+
// Create a single message node for this unique message
|
|
163
|
+
messageNodes.push({
|
|
164
|
+
id: messageNodeId,
|
|
165
|
+
type: messageObject.collection, // events, commands, or queries
|
|
166
|
+
position: { x: 0, y: 0 }, // Temporary position, will be calculated by dagre
|
|
167
|
+
data: {
|
|
168
|
+
mode: 'simple',
|
|
169
|
+
message: messageObject,
|
|
170
|
+
},
|
|
171
|
+
sourcePosition: 'right',
|
|
172
|
+
targetPosition: 'left',
|
|
173
|
+
} as Node);
|
|
174
|
+
|
|
175
|
+
// Create edges from all publishers to the message node
|
|
176
|
+
publishers.forEach(({ domainId, service }) => {
|
|
177
|
+
// Only create edge if there's a consumer in a different domain
|
|
178
|
+
const hasExternalConsumer = consumers.some((c) => c.domainId !== domainId);
|
|
179
|
+
|
|
180
|
+
if (hasExternalConsumer) {
|
|
181
|
+
edges.push(
|
|
182
|
+
createEdge({
|
|
183
|
+
id: `edge-${domainId}-${service.data.id}-${messageNodeId}`,
|
|
184
|
+
source: domainId,
|
|
185
|
+
sourceHandle: `${service.data.id}-source`,
|
|
186
|
+
target: messageNodeId,
|
|
187
|
+
type: 'animated',
|
|
188
|
+
animated: true,
|
|
189
|
+
label: 'publishes',
|
|
190
|
+
data: {
|
|
191
|
+
message: messageObject,
|
|
192
|
+
type: 'domain-to-message',
|
|
193
|
+
publisherService: service,
|
|
194
|
+
},
|
|
195
|
+
})
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Create edges from the message node to all consumers
|
|
201
|
+
consumers.forEach(({ domainId, service }) => {
|
|
202
|
+
// Only create edge if there's a publisher in a different domain
|
|
203
|
+
const hasExternalPublisher = publishers.some((p) => p.domainId !== domainId);
|
|
204
|
+
|
|
205
|
+
if (hasExternalPublisher) {
|
|
206
|
+
edges.push(
|
|
207
|
+
createEdge({
|
|
208
|
+
id: `edge-${messageNodeId}-${domainId}-${service.data.id}`,
|
|
209
|
+
source: messageNodeId,
|
|
210
|
+
target: domainId,
|
|
211
|
+
targetHandle: `${service.data.id}-target`,
|
|
212
|
+
type: 'animated',
|
|
213
|
+
animated: true,
|
|
214
|
+
label: 'consumed by',
|
|
215
|
+
data: {
|
|
216
|
+
message: messageObject,
|
|
217
|
+
type: 'message-to-domain',
|
|
218
|
+
consumerService: service,
|
|
219
|
+
},
|
|
220
|
+
})
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Add all nodes to dagre graph for layout calculation
|
|
229
|
+
const allNodes = [...domainNodes, ...messageNodes];
|
|
230
|
+
|
|
231
|
+
allNodes.forEach((node) => {
|
|
232
|
+
dagreGraph.setNode(node.id, { width: 250, height: 120 });
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
edges.forEach((edge) => {
|
|
236
|
+
dagreGraph.setEdge(edge.source, edge.target);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// Calculate layout using dagre
|
|
240
|
+
dagre.layout(dagreGraph);
|
|
241
|
+
|
|
242
|
+
// Apply calculated positions to nodes
|
|
243
|
+
const layoutedDomainNodes = calculatedNodes(dagreGraph, domainNodes);
|
|
244
|
+
const layoutedMessageNodes = calculatedNodes(dagreGraph, messageNodes);
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
domainNodes: layoutedDomainNodes,
|
|
248
|
+
messageNodes: layoutedMessageNodes,
|
|
249
|
+
edges,
|
|
250
|
+
};
|
|
251
|
+
};
|