@eventcatalog/core 2.48.5 → 2.49.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analytics/analytics.cjs +1 -1
- package/dist/analytics/analytics.js +2 -2
- package/dist/analytics/log-build.cjs +1 -1
- package/dist/analytics/log-build.js +3 -3
- package/dist/{chunk-WHVRKXDZ.js → chunk-AK2PL55W.js} +1 -1
- package/dist/{chunk-WK7LQRVD.js → chunk-TVMUTOOL.js} +1 -1
- package/dist/{chunk-PYIL7PRQ.js → chunk-ZDOGODWQ.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/astro.config.mjs +1 -1
- package/eventcatalog/src/components/MDX/Admonition.tsx +2 -2
- package/eventcatalog/src/components/MDX/EntityMap/EntityMap.astro +61 -0
- package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.astro +10 -0
- package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.tsx +38 -21
- package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Entity.tsx +151 -0
- package/eventcatalog/src/components/MDX/NodeGraph/VisualiserSearch.tsx +10 -0
- package/eventcatalog/src/components/MDX/components.tsx +2 -0
- package/eventcatalog/src/components/SideNav/ListViewSideBar/index.tsx +74 -60
- package/eventcatalog/src/content.config.ts +3 -0
- package/eventcatalog/src/hooks/eventcatalog-visualizer.ts +14 -2
- package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/entity-map/_index.data.ts +78 -0
- package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/entity-map/index.astro +52 -0
- package/eventcatalog/src/utils/collections/domains.ts +4 -0
- package/eventcatalog/src/utils/entities.ts +1 -1
- package/eventcatalog/src/utils/node-graphs/domain-entity-map.ts +218 -0
- package/package.json +2 -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-AK2PL55W.js";
|
|
4
|
+
import "../chunk-TVMUTOOL.js";
|
|
5
|
+
import "../chunk-ZDOGODWQ.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-XE6PFSH5.js";
|
|
7
7
|
import {
|
|
8
8
|
log_build_default
|
|
9
|
-
} from "./chunk-
|
|
10
|
-
import "./chunk-
|
|
9
|
+
} from "./chunk-AK2PL55W.js";
|
|
10
|
+
import "./chunk-TVMUTOOL.js";
|
|
11
11
|
import {
|
|
12
12
|
catalogToAstro,
|
|
13
13
|
checkAndConvertMdToMdx
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
import "./chunk-LDBRNJIL.js";
|
|
16
16
|
import {
|
|
17
17
|
VERSION
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-ZDOGODWQ.js";
|
|
19
19
|
import {
|
|
20
20
|
isAuthEnabled,
|
|
21
21
|
isBackstagePluginEnabled,
|
|
@@ -20,7 +20,7 @@ import expressiveCode from 'astro-expressive-code';
|
|
|
20
20
|
const projectDirectory = process.env.PROJECT_DIR || process.cwd();
|
|
21
21
|
const base = config.base || '/';
|
|
22
22
|
const host = config.host || false;
|
|
23
|
-
const compress = config.compress
|
|
23
|
+
const compress = config.compress ?? true;
|
|
24
24
|
|
|
25
25
|
// https://astro.build/config
|
|
26
26
|
export default defineConfig({
|
|
@@ -25,9 +25,9 @@ export default function Admonition({ children, type = 'info', className = '', ti
|
|
|
25
25
|
const Icon = config.icon;
|
|
26
26
|
|
|
27
27
|
return (
|
|
28
|
-
<div className={`bg-${config.color}-50 border-l-4 border-${config.color}-500 p-4 my-4 ${className} rounded-md`}>
|
|
28
|
+
<div className={`bg-${config.color}-50 border-l-4 border-${config.color}-500 p-4 my-4 ${className} rounded-md not-prose`}>
|
|
29
29
|
<div className="flex flex-col">
|
|
30
|
-
<div className="flex items-center">
|
|
30
|
+
<div className="flex items-center justify-start">
|
|
31
31
|
<Icon className={`h-6 w-6 text-${config.color}-500 stroke-2`} aria-hidden="true" />
|
|
32
32
|
<h3 className={`ml-2 text-${config.color}-600 font-bold text-md`}>{title || config.title}</h3>
|
|
33
33
|
</div>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { getDomains } from '@utils/collections/domains';
|
|
3
|
+
import { getNodesAndEdges } from '@utils/node-graphs/domain-entity-map.ts';
|
|
4
|
+
import Admonition from '@components/MDX/Admonition';
|
|
5
|
+
import NodeGraph from '../NodeGraph/NodeGraph';
|
|
6
|
+
import { getVersionFromCollection } from '@utils/collections/versions';
|
|
7
|
+
|
|
8
|
+
const { id, version = 'latest', maxHeight, includeKey = true } = Astro.props;
|
|
9
|
+
|
|
10
|
+
// Find the flow for the given id and version
|
|
11
|
+
const domains = await getDomains();
|
|
12
|
+
const domainCollection = getVersionFromCollection(domains, id, version) || [];
|
|
13
|
+
const domain = domainCollection[0];
|
|
14
|
+
|
|
15
|
+
const { nodes, edges } = await getNodesAndEdges({
|
|
16
|
+
id: id,
|
|
17
|
+
version: domain?.data?.version,
|
|
18
|
+
});
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
{
|
|
22
|
+
!domain && (
|
|
23
|
+
<Admonition type="warning">
|
|
24
|
+
<div>
|
|
25
|
+
<span class="block font-bold">{`<EntityMap/>`} failed to load</span>
|
|
26
|
+
<span class="block">
|
|
27
|
+
Tried to load domain id: {id} with version {version}. Make sure you have this domain defined in your project.
|
|
28
|
+
</span>
|
|
29
|
+
</div>
|
|
30
|
+
</Admonition>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
<div
|
|
35
|
+
class="h-[30em] my-6 mb-12 w-full relative border border-gray-200 rounded-md"
|
|
36
|
+
id={`${id}-entity-map-portal`}
|
|
37
|
+
style={{
|
|
38
|
+
maxHeight: maxHeight ? `${maxHeight}em` : `30em`,
|
|
39
|
+
}}
|
|
40
|
+
>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<div>
|
|
44
|
+
<NodeGraph
|
|
45
|
+
id={id}
|
|
46
|
+
nodes={nodes}
|
|
47
|
+
edges={edges}
|
|
48
|
+
linkTo={'visualiser'}
|
|
49
|
+
mode="simple"
|
|
50
|
+
includeKey={includeKey}
|
|
51
|
+
footerLabel=`Entity Map - ${domain?.data?.name} - v(${domain?.data?.version})`
|
|
52
|
+
client:only="react"
|
|
53
|
+
portalId={`${id}-entity-map-portal`}
|
|
54
|
+
/>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<style is:global>
|
|
58
|
+
.react-flow__attribution {
|
|
59
|
+
display: none;
|
|
60
|
+
}
|
|
61
|
+
</style>
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
getNodesAndEdges as getNodesAndEdgesForDomain,
|
|
11
11
|
getNodesAndEdgesForDomainContextMap,
|
|
12
12
|
} from '@utils/node-graphs/domains-node-graph';
|
|
13
|
+
import { getNodesAndEdges as getNodesAndEdgesForDomainEntityMap } from '@utils/node-graphs/domain-entity-map';
|
|
13
14
|
import { getDomainsCanvasData } from '@utils/node-graphs/domains-canvas';
|
|
14
15
|
import { getNodesAndEdges as getNodesAndEdgesForFlows } from '@utils/node-graphs/flows-node-graph';
|
|
15
16
|
import { buildUrl } from '@utils/url-builder';
|
|
@@ -96,6 +97,15 @@ if (collection === 'domains-canvas') {
|
|
|
96
97
|
nodes = [...domainNodes, ...messageNodes];
|
|
97
98
|
edges = fetchedEdges;
|
|
98
99
|
}
|
|
100
|
+
|
|
101
|
+
if (collection === 'domains-entities') {
|
|
102
|
+
const { nodes: fetchedNodes, edges: fetchedEdges } = await getNodesAndEdgesForDomainEntityMap({
|
|
103
|
+
id,
|
|
104
|
+
version,
|
|
105
|
+
});
|
|
106
|
+
nodes = fetchedNodes;
|
|
107
|
+
edges = fetchedEdges;
|
|
108
|
+
}
|
|
99
109
|
---
|
|
100
110
|
|
|
101
111
|
<div>
|
|
@@ -23,6 +23,7 @@ import { DocumentArrowDownIcon } from '@heroicons/react/24/outline';
|
|
|
23
23
|
import ServiceNode from './Nodes/Service';
|
|
24
24
|
import FlowNode from './Nodes/Flow';
|
|
25
25
|
import EventNode from './Nodes/Event';
|
|
26
|
+
import EntityNode from './Nodes/Entity';
|
|
26
27
|
import QueryNode from './Nodes/Query';
|
|
27
28
|
import UserNode from './Nodes/User';
|
|
28
29
|
import StepNode from './Nodes/Step';
|
|
@@ -85,6 +86,7 @@ const NodeGraphBuilder = ({
|
|
|
85
86
|
actor: UserNode,
|
|
86
87
|
custom: CustomNode,
|
|
87
88
|
externalSystem: ExternalSystemNode,
|
|
89
|
+
entities: EntityNode,
|
|
88
90
|
}),
|
|
89
91
|
[]
|
|
90
92
|
);
|
|
@@ -101,7 +103,16 @@ const NodeGraphBuilder = ({
|
|
|
101
103
|
const [isAnimated, setIsAnimated] = useState(false);
|
|
102
104
|
const [animateMessages, setAnimateMessages] = useState(false);
|
|
103
105
|
const [activeStepIndex, setActiveStepIndex] = useState<number | null>(null);
|
|
104
|
-
|
|
106
|
+
|
|
107
|
+
// Check if there are channels to determine if we need the visualizer functionality
|
|
108
|
+
const hasChannels = useMemo(() => initialNodes.some((node: any) => node.type === 'channels'), [initialNodes]);
|
|
109
|
+
const { hideChannels, toggleChannelsVisibility } = useEventCatalogVisualiser({
|
|
110
|
+
nodes,
|
|
111
|
+
edges,
|
|
112
|
+
setNodes,
|
|
113
|
+
setEdges,
|
|
114
|
+
skipProcessing: !hasChannels, // Pass flag to skip processing when no channels
|
|
115
|
+
});
|
|
105
116
|
const { fitView, getNodes } = useReactFlow();
|
|
106
117
|
const searchRef = useRef<VisualiserSearchRef>(null);
|
|
107
118
|
|
|
@@ -540,27 +551,29 @@ const NodeGraphBuilder = ({
|
|
|
540
551
|
</div>
|
|
541
552
|
<p className="text-[10px] text-gray-500">Animate events, queries and commands.</p>
|
|
542
553
|
</div>
|
|
543
|
-
|
|
544
|
-
<div
|
|
545
|
-
<
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
hideChannels ? 'bg-purple-600' : 'bg-gray-200'
|
|
553
|
-
} relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2`}
|
|
554
|
-
>
|
|
555
|
-
<span
|
|
554
|
+
{hasChannels && (
|
|
555
|
+
<div>
|
|
556
|
+
<div className="flex items-center justify-between">
|
|
557
|
+
<label htmlFor="hide-channels-toggle" className="text-sm font-medium text-gray-700">
|
|
558
|
+
Hide Channels
|
|
559
|
+
</label>
|
|
560
|
+
<button
|
|
561
|
+
id="hide-channels-toggle"
|
|
562
|
+
onClick={toggleChannelsVisibility}
|
|
556
563
|
className={`${
|
|
557
|
-
hideChannels ? '
|
|
558
|
-
} inline-
|
|
559
|
-
|
|
560
|
-
|
|
564
|
+
hideChannels ? 'bg-purple-600' : 'bg-gray-200'
|
|
565
|
+
} relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2`}
|
|
566
|
+
>
|
|
567
|
+
<span
|
|
568
|
+
className={`${
|
|
569
|
+
hideChannels ? 'translate-x-6' : 'translate-x-1'
|
|
570
|
+
} inline-block h-4 w-4 transform rounded-full bg-white transition-transform`}
|
|
571
|
+
/>
|
|
572
|
+
</button>
|
|
573
|
+
</div>
|
|
574
|
+
<p className="text-[10px] text-gray-500">Show or hide channels in the visualizer.</p>
|
|
561
575
|
</div>
|
|
562
|
-
|
|
563
|
-
</div>
|
|
576
|
+
)}
|
|
564
577
|
<div className="pt-4 border-t border-gray-200">
|
|
565
578
|
<button
|
|
566
579
|
onClick={handleExportVisual}
|
|
@@ -623,6 +636,7 @@ interface NodeGraphProps {
|
|
|
623
636
|
linksToVisualiser?: boolean;
|
|
624
637
|
links?: { label: string; url: string }[];
|
|
625
638
|
mode?: 'full' | 'simple';
|
|
639
|
+
portalId?: string;
|
|
626
640
|
}
|
|
627
641
|
|
|
628
642
|
const NodeGraph = ({
|
|
@@ -638,13 +652,16 @@ const NodeGraph = ({
|
|
|
638
652
|
linksToVisualiser = false,
|
|
639
653
|
links = [],
|
|
640
654
|
mode = 'full',
|
|
655
|
+
portalId,
|
|
641
656
|
}: NodeGraphProps) => {
|
|
642
657
|
const [elem, setElem] = useState(null);
|
|
643
658
|
const [showFooter, setShowFooter] = useState(true);
|
|
644
659
|
|
|
660
|
+
const containerToRenderInto = portalId || `${id}-portal`;
|
|
661
|
+
|
|
645
662
|
useEffect(() => {
|
|
646
663
|
// @ts-ignore
|
|
647
|
-
setElem(document.getElementById(
|
|
664
|
+
setElem(document.getElementById(containerToRenderInto));
|
|
648
665
|
}, []);
|
|
649
666
|
|
|
650
667
|
useEffect(() => {
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { CubeIcon } from '@heroicons/react/16/solid';
|
|
2
|
+
import type { CollectionEntry } from 'astro:content';
|
|
3
|
+
import { Handle, Position } from '@xyflow/react';
|
|
4
|
+
import { getIcon } from '@utils/badges';
|
|
5
|
+
import * as ContextMenu from '@radix-ui/react-context-menu';
|
|
6
|
+
import { buildUrl } from '@utils/url-builder';
|
|
7
|
+
import { useState } from 'react';
|
|
8
|
+
|
|
9
|
+
interface Data {
|
|
10
|
+
title: string;
|
|
11
|
+
label: string;
|
|
12
|
+
bgColor: string;
|
|
13
|
+
color: string;
|
|
14
|
+
mode: 'simple' | 'full';
|
|
15
|
+
entity: CollectionEntry<'entities'>;
|
|
16
|
+
showTarget?: boolean;
|
|
17
|
+
showSource?: boolean;
|
|
18
|
+
externalToDomain?: boolean;
|
|
19
|
+
domainName?: string;
|
|
20
|
+
domainId?: string;
|
|
21
|
+
group?: {
|
|
22
|
+
type: string;
|
|
23
|
+
value: string;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function classNames(...classes: any) {
|
|
28
|
+
return classes.filter(Boolean).join(' ');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default function EntityNode({ data, sourcePosition, targetPosition }: any) {
|
|
32
|
+
const { mode, entity, externalToDomain, domainName } = data as Data;
|
|
33
|
+
const { name, version, properties = [], aggregateRoot, styles, sidebar } = entity.data;
|
|
34
|
+
|
|
35
|
+
const { node: { color = 'blue', label } = {}, icon = 'CubeIcon' } = styles || {};
|
|
36
|
+
|
|
37
|
+
const Icon = getIcon(icon);
|
|
38
|
+
|
|
39
|
+
const [hoveredProperty, setHoveredProperty] = useState<string | null>(null);
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<ContextMenu.Root>
|
|
43
|
+
<ContextMenu.Trigger>
|
|
44
|
+
<div
|
|
45
|
+
className={classNames(
|
|
46
|
+
'bg-white border border-blue-300 rounded-lg shadow-sm min-w-[200px]',
|
|
47
|
+
externalToDomain ? 'border-yellow-400' : ''
|
|
48
|
+
)}
|
|
49
|
+
>
|
|
50
|
+
{/* Table Header */}
|
|
51
|
+
<div
|
|
52
|
+
className={classNames(
|
|
53
|
+
'bg-blue-100 px-4 py-2 rounded-t-lg border-b border-gray-300',
|
|
54
|
+
externalToDomain ? 'bg-yellow-400' : ''
|
|
55
|
+
)}
|
|
56
|
+
>
|
|
57
|
+
<div className="flex items-center gap-2">
|
|
58
|
+
{Icon && <Icon className="w-4 h-4 text-gray-600" />}
|
|
59
|
+
<span className="font-semibold text-gray-800 text-sm">{name}</span>
|
|
60
|
+
{aggregateRoot && <span className="text-xs bg-yellow-100 text-yellow-800 px-1.5 py-0.5 rounded">AR</span>}
|
|
61
|
+
</div>
|
|
62
|
+
{/* {externalToDomain && domainName && ( */}
|
|
63
|
+
<div className="text-xs text-yellow-800 font-medium mt-1">from {domainName} domain</div>
|
|
64
|
+
{/* )} */}
|
|
65
|
+
{mode === 'full' && <div className="text-xs text-gray-600 mt-1">v{version}</div>}
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
{/* Properties Table */}
|
|
69
|
+
{properties.length > 0 ? (
|
|
70
|
+
<div className="divide-y divide-gray-200 relative">
|
|
71
|
+
{properties.map((property: any, index: number) => {
|
|
72
|
+
const propertyKey = `${property.name}-${index}`;
|
|
73
|
+
const isHovered = hoveredProperty === propertyKey;
|
|
74
|
+
return (
|
|
75
|
+
<div
|
|
76
|
+
key={propertyKey}
|
|
77
|
+
className="relative flex items-center justify-between px-4 py-2 hover:bg-gray-50"
|
|
78
|
+
onMouseEnter={() => property.description && setHoveredProperty(propertyKey)}
|
|
79
|
+
onMouseLeave={() => setHoveredProperty(null)}
|
|
80
|
+
>
|
|
81
|
+
{/* Target handle */}
|
|
82
|
+
<Handle
|
|
83
|
+
type="target"
|
|
84
|
+
position={Position.Left}
|
|
85
|
+
id={`${property.name}-target`}
|
|
86
|
+
className="!w-3 !h-3 !bg-white !border-2 !border-gray-400 !rounded-full !left-[-0px]"
|
|
87
|
+
style={{ left: '-6px' }}
|
|
88
|
+
/>
|
|
89
|
+
|
|
90
|
+
{/* Source handle */}
|
|
91
|
+
<Handle
|
|
92
|
+
type="source"
|
|
93
|
+
position={Position.Right}
|
|
94
|
+
id={`${property.name}-source`}
|
|
95
|
+
className="!w-3 !h-3 !bg-white !border-2 !border-gray-400 !rounded-full !right-[-0px]"
|
|
96
|
+
style={{ right: '-6px' }}
|
|
97
|
+
/>
|
|
98
|
+
|
|
99
|
+
{/* Property content */}
|
|
100
|
+
<div className="flex-1 flex items-center justify-between">
|
|
101
|
+
<div className="flex items-center gap-1">
|
|
102
|
+
<span className="text-sm font-medium text-gray-900">{property.name}</span>
|
|
103
|
+
{property.required && <span className="text-red-500 text-xs">*</span>}
|
|
104
|
+
</div>
|
|
105
|
+
<span className="text-sm text-gray-600 font-mono">{property.type}</span>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
{/* Reference indicator */}
|
|
109
|
+
{property.references && (
|
|
110
|
+
<div className="absolute right-2 top-1/2 transform -translate-y-1/2">
|
|
111
|
+
<div className="w-2 h-2 bg-blue-500 rounded-full" title={`References ${property.references}`}></div>
|
|
112
|
+
</div>
|
|
113
|
+
)}
|
|
114
|
+
|
|
115
|
+
{/* Property Tooltip */}
|
|
116
|
+
{isHovered && property.description && (
|
|
117
|
+
<div className="absolute left-full ml-2 top-1/2 transform -translate-y-1/2 z-[9999] w-[200px] bg-gray-900 text-white text-xs rounded-lg py-2 px-3 pointer-events-none shadow-xl max-w-xl opacity-100">
|
|
118
|
+
<div className="text-gray-200 whitespace-normal break-words">{property.description}</div>
|
|
119
|
+
<div className="absolute right-full top-1/2 transform -translate-y-1/2 border-4 border-transparent border-r-gray-900"></div>
|
|
120
|
+
</div>
|
|
121
|
+
)}
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
})}
|
|
125
|
+
</div>
|
|
126
|
+
) : (
|
|
127
|
+
<div className="px-4 py-3 text-sm text-gray-500 text-center">No properties defined</div>
|
|
128
|
+
)}
|
|
129
|
+
|
|
130
|
+
{/* Main node handles (if no properties) */}
|
|
131
|
+
{properties.length === 0 && (
|
|
132
|
+
<>
|
|
133
|
+
{targetPosition && <Handle type="target" position={targetPosition} />}
|
|
134
|
+
{sourcePosition && <Handle type="source" position={sourcePosition} />}
|
|
135
|
+
</>
|
|
136
|
+
)}
|
|
137
|
+
</div>
|
|
138
|
+
</ContextMenu.Trigger>
|
|
139
|
+
<ContextMenu.Portal>
|
|
140
|
+
<ContextMenu.Content className="min-w-[220px] bg-white rounded-md p-1 shadow-md border border-gray-200">
|
|
141
|
+
<ContextMenu.Item
|
|
142
|
+
asChild
|
|
143
|
+
className="text-sm px-2 py-1.5 outline-none cursor-pointer hover:bg-orange-100 rounded-sm flex items-center"
|
|
144
|
+
>
|
|
145
|
+
<a href={buildUrl(`/docs/entities/${entity.data.id}/${version}`)}>Read documentation</a>
|
|
146
|
+
</ContextMenu.Item>
|
|
147
|
+
</ContextMenu.Content>
|
|
148
|
+
</ContextMenu.Portal>
|
|
149
|
+
</ContextMenu.Root>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
@@ -17,6 +17,11 @@ interface DomainData {
|
|
|
17
17
|
version?: string;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
interface EntityData {
|
|
21
|
+
name: string;
|
|
22
|
+
version?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
20
25
|
interface NodeDataContent extends Record<string, unknown> {
|
|
21
26
|
message?: {
|
|
22
27
|
data: MessageData;
|
|
@@ -27,6 +32,9 @@ interface NodeDataContent extends Record<string, unknown> {
|
|
|
27
32
|
domain?: {
|
|
28
33
|
data: DomainData;
|
|
29
34
|
};
|
|
35
|
+
entity?: {
|
|
36
|
+
data: EntityData;
|
|
37
|
+
};
|
|
30
38
|
name?: string;
|
|
31
39
|
version?: string;
|
|
32
40
|
}
|
|
@@ -72,12 +80,14 @@ const VisualiserSearch = forwardRef<VisualiserSearchRef, VisualiserSearchProps>(
|
|
|
72
80
|
node.data?.message?.data?.name ||
|
|
73
81
|
node.data?.service?.data?.name ||
|
|
74
82
|
node.data?.domain?.data?.name ||
|
|
83
|
+
node.data?.entity?.data?.name ||
|
|
75
84
|
node.data?.name ||
|
|
76
85
|
node.id;
|
|
77
86
|
const version =
|
|
78
87
|
node.data?.message?.data?.version ||
|
|
79
88
|
node.data?.service?.data?.version ||
|
|
80
89
|
node.data?.domain?.data?.version ||
|
|
90
|
+
node.data?.entity?.data?.version ||
|
|
81
91
|
node.data?.version;
|
|
82
92
|
return version ? `${name} (v${version})` : name;
|
|
83
93
|
}, []);
|
|
@@ -4,6 +4,7 @@ import File from '@components/MDX/File';
|
|
|
4
4
|
import Accordion from '@components/MDX/Accordion/Accordion.astro';
|
|
5
5
|
import AccordionGroup from '@components/MDX/Accordion/AccordionGroup.astro';
|
|
6
6
|
import Flow from '@components/MDX/Flow/Flow.astro';
|
|
7
|
+
import EntityMap from '@components/MDX/EntityMap/EntityMap.astro';
|
|
7
8
|
import Tiles from '@components/MDX/Tiles/Tiles.astro';
|
|
8
9
|
import Tile from '@components/MDX/Tiles/Tile.astro';
|
|
9
10
|
import Steps from '@components/MDX/Steps/Steps.astro';
|
|
@@ -41,6 +42,7 @@ const components = (props: any) => {
|
|
|
41
42
|
MessageTable: (mdxProp: any) => jsx(MessageTable, { ...props, ...mdxProp }),
|
|
42
43
|
EntityPropertiesTable: (mdxProp: any) => jsx(EntityPropertiesTable, { ...props, ...mdxProp }),
|
|
43
44
|
NodeGraph: (mdxProp: any) => jsx(NodeGraphPortal, { ...props.data, ...mdxProp, props, mdxProp }),
|
|
45
|
+
EntityMap,
|
|
44
46
|
OpenAPI,
|
|
45
47
|
ResourceGroupTable: (mdxProp: any) => jsx(ResourceGroupTable, { ...props, ...mdxProp }),
|
|
46
48
|
ResourceLink: (mdxProp: any) => jsx(ResourceLink, { ...props, ...mdxProp }),
|
|
@@ -485,72 +485,86 @@ const ListViewSideBar: React.FC<ListViewSideBarProps> = ({ resources, currentPat
|
|
|
485
485
|
);
|
|
486
486
|
|
|
487
487
|
// Component to render domain content (Overview, Architecture, etc.)
|
|
488
|
-
const DomainContent = React.memo(
|
|
489
|
-
|
|
488
|
+
const DomainContent = React.memo(
|
|
489
|
+
({ item, nestingLevel = 0, className = '' }: { item: any; nestingLevel?: number; className?: string }) => {
|
|
490
|
+
const marginLeft = nestingLevel > 0 ? `ml-${nestingLevel * 4}` : '';
|
|
491
|
+
const hasEntities = item.entities && item.entities.length > 0;
|
|
490
492
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
<a
|
|
497
|
-
href={`${item.href}`}
|
|
498
|
-
data-active={decodedCurrentPath === item.href}
|
|
499
|
-
className={`flex items-center px-2 py-1.5 text-xs text-gray-600 hover:bg-purple-100 rounded-md ${
|
|
500
|
-
decodedCurrentPath === item.href ? 'bg-purple-100 ' : 'hover:bg-purple-100'
|
|
501
|
-
}`}
|
|
502
|
-
>
|
|
503
|
-
<span className="truncate">Overview</span>
|
|
504
|
-
</a>
|
|
505
|
-
{!isVisualizer && (
|
|
506
|
-
<a
|
|
507
|
-
href={buildUrlWithParams('/architecture/docs/services', {
|
|
508
|
-
serviceIds: item.services.map((service: any) => service.data.id).join(','),
|
|
509
|
-
domainId: item.id,
|
|
510
|
-
domainName: item.name,
|
|
511
|
-
})}
|
|
512
|
-
data-active={window.location.href.includes(`domainId=${item.id}`)}
|
|
513
|
-
className={`flex items-center px-2 py-1.5 text-xs text-gray-600 hover:bg-purple-100 rounded-md ${
|
|
514
|
-
window.location.href.includes(`domainId=${item.id}`) ? 'bg-purple-100 ' : 'hover:bg-purple-100'
|
|
515
|
-
}`}
|
|
516
|
-
>
|
|
517
|
-
<span className="truncate">Architecture</span>
|
|
518
|
-
</a>
|
|
519
|
-
)}
|
|
520
|
-
{!isVisualizer && (
|
|
493
|
+
return (
|
|
494
|
+
<div
|
|
495
|
+
className={`overflow-hidden transition-[height] duration-150 ease-out ${collapsedGroups[item.href] ? 'h-0' : 'h-auto'} ${className}`}
|
|
496
|
+
>
|
|
497
|
+
<div className={`space-y-0.5 border-gray-200/80 border-l pl-4 mt-1 ${marginLeft ? marginLeft : 'ml-[9px]'}`}>
|
|
521
498
|
<a
|
|
522
|
-
href={
|
|
523
|
-
data-active={decodedCurrentPath
|
|
499
|
+
href={`${item.href}`}
|
|
500
|
+
data-active={decodedCurrentPath === item.href}
|
|
524
501
|
className={`flex items-center px-2 py-1.5 text-xs text-gray-600 hover:bg-purple-100 rounded-md ${
|
|
525
|
-
decodedCurrentPath
|
|
502
|
+
decodedCurrentPath === item.href ? 'bg-purple-100 ' : 'hover:bg-purple-100'
|
|
526
503
|
}`}
|
|
527
504
|
>
|
|
528
|
-
<span className="truncate">
|
|
505
|
+
<span className="truncate">Overview</span>
|
|
529
506
|
</a>
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
507
|
+
{isVisualizer && hasEntities && (
|
|
508
|
+
<a
|
|
509
|
+
href={buildUrl(`/${item.href}/entity-map`)}
|
|
510
|
+
data-active={decodedCurrentPath === `${item.href}/entity-map`}
|
|
511
|
+
className={`flex items-center px-2 py-1.5 text-xs text-gray-600 hover:bg-purple-100 rounded-md ${
|
|
512
|
+
decodedCurrentPath === `${item.href}/entity-map` ? 'bg-purple-100 ' : 'hover:bg-purple-100'
|
|
513
|
+
}`}
|
|
514
|
+
>
|
|
515
|
+
<span className="truncate">Entity Map</span>
|
|
516
|
+
</a>
|
|
517
|
+
)}
|
|
518
|
+
{!isVisualizer && (
|
|
519
|
+
<a
|
|
520
|
+
href={buildUrlWithParams('/architecture/docs/services', {
|
|
521
|
+
serviceIds: item.services.map((service: any) => service.data.id).join(','),
|
|
522
|
+
domainId: item.id,
|
|
523
|
+
domainName: item.name,
|
|
524
|
+
})}
|
|
525
|
+
data-active={window.location.href.includes(`domainId=${item.id}`)}
|
|
526
|
+
className={`flex items-center px-2 py-1.5 text-xs text-gray-600 hover:bg-purple-100 rounded-md ${
|
|
527
|
+
window.location.href.includes(`domainId=${item.id}`) ? 'bg-purple-100 ' : 'hover:bg-purple-100'
|
|
528
|
+
}`}
|
|
529
|
+
>
|
|
530
|
+
<span className="truncate">Architecture</span>
|
|
531
|
+
</a>
|
|
532
|
+
)}
|
|
533
|
+
{!isVisualizer && (
|
|
534
|
+
<a
|
|
535
|
+
href={buildUrl(`/docs/domains/${item.id}/language`)}
|
|
536
|
+
data-active={decodedCurrentPath.includes(`/docs/domains/${item.id}/language`)}
|
|
537
|
+
className={`flex items-center px-2 py-1.5 text-xs text-gray-600 hover:bg-purple-100 rounded-md ${
|
|
538
|
+
decodedCurrentPath.includes(`/docs/domains/${item.id}/language`) ? 'bg-purple-100 ' : 'hover:bg-purple-100'
|
|
539
|
+
}`}
|
|
540
|
+
>
|
|
541
|
+
<span className="truncate">Ubiquitous Language</span>
|
|
542
|
+
</a>
|
|
543
|
+
)}
|
|
544
|
+
{item.entities.length > 0 && !isVisualizer && (
|
|
545
|
+
<CollapsibleGroup
|
|
546
|
+
isCollapsed={collapsedGroups[`${item.href}-entities`]}
|
|
547
|
+
onToggle={() => toggleGroupCollapse(`${item.href}-entities`)}
|
|
548
|
+
title={
|
|
549
|
+
<button
|
|
550
|
+
onClick={(e) => {
|
|
551
|
+
e.stopPropagation();
|
|
552
|
+
toggleGroupCollapse(`${item.href}-entities`);
|
|
553
|
+
}}
|
|
554
|
+
className="truncate underline ml-2 text-xs mb-1 py-1"
|
|
555
|
+
>
|
|
556
|
+
Entities ({item.entities.length})
|
|
557
|
+
</button>
|
|
558
|
+
}
|
|
559
|
+
>
|
|
560
|
+
<MessageList messages={item.entities} decodedCurrentPath={decodedCurrentPath} searchTerm={debouncedSearchTerm} />
|
|
561
|
+
</CollapsibleGroup>
|
|
562
|
+
)}
|
|
563
|
+
</div>
|
|
550
564
|
</div>
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
);
|
|
554
568
|
|
|
555
569
|
if (!isInitialized) return null;
|
|
556
570
|
|
|
@@ -661,7 +675,7 @@ const ListViewSideBar: React.FC<ListViewSideBarProps> = ({ resources, currentPat
|
|
|
661
675
|
data-active={decodedCurrentPath === subdomain.href}
|
|
662
676
|
>
|
|
663
677
|
<DomainItem item={subdomain} isSubdomain={true} nestingLevel={1} />
|
|
664
|
-
<DomainContent item={subdomain} nestingLevel={
|
|
678
|
+
<DomainContent item={subdomain} nestingLevel={3} className="ml-6" />
|
|
665
679
|
</div>
|
|
666
680
|
))}
|
|
667
681
|
</div>
|
|
@@ -429,6 +429,9 @@ const entities = defineCollection({
|
|
|
429
429
|
type: z.string(),
|
|
430
430
|
required: z.boolean().optional(),
|
|
431
431
|
description: z.string().optional(),
|
|
432
|
+
references: z.string().optional(),
|
|
433
|
+
referencesIdentifier: z.string().optional(),
|
|
434
|
+
relationType: z.string().optional(),
|
|
432
435
|
})
|
|
433
436
|
)
|
|
434
437
|
.optional(),
|
|
@@ -16,9 +16,16 @@ interface EventCatalogVisualizerProps {
|
|
|
16
16
|
edges: Edge[];
|
|
17
17
|
setNodes: (nodes: Node[]) => void;
|
|
18
18
|
setEdges: (edges: Edge[]) => void;
|
|
19
|
+
skipProcessing?: boolean;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
export const useEventCatalogVisualiser = ({
|
|
22
|
+
export const useEventCatalogVisualiser = ({
|
|
23
|
+
nodes,
|
|
24
|
+
edges,
|
|
25
|
+
setNodes,
|
|
26
|
+
setEdges,
|
|
27
|
+
skipProcessing = false,
|
|
28
|
+
}: EventCatalogVisualizerProps) => {
|
|
22
29
|
const [hideChannels, setHideChannels] = useState(false);
|
|
23
30
|
const [initialNodes] = useState(nodes);
|
|
24
31
|
const [initialEdges] = useState(edges);
|
|
@@ -77,6 +84,11 @@ export const useEventCatalogVisualiser = ({ nodes, edges, setNodes, setEdges }:
|
|
|
77
84
|
}, [edges, channels]);
|
|
78
85
|
|
|
79
86
|
useEffect(() => {
|
|
87
|
+
// Skip processing if there are no channels to manage
|
|
88
|
+
if (skipProcessing) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
80
92
|
if (hideChannels) {
|
|
81
93
|
const { nodes: newNodes, edges: newEdges } = getNodesAndEdgesFromDagre({ nodes: updatedNodes, edges: updatedEdges });
|
|
82
94
|
setNodes(newNodes);
|
|
@@ -85,7 +97,7 @@ export const useEventCatalogVisualiser = ({ nodes, edges, setNodes, setEdges }:
|
|
|
85
97
|
setNodes(initialNodes);
|
|
86
98
|
setEdges(initialEdges);
|
|
87
99
|
}
|
|
88
|
-
}, [hideChannels]);
|
|
100
|
+
}, [hideChannels, skipProcessing]);
|
|
89
101
|
|
|
90
102
|
return {
|
|
91
103
|
hideChannels,
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { HybridPage } from '@utils/page-loaders/hybrid-page';
|
|
2
|
+
import { isAuthEnabled } from '@utils/feature';
|
|
3
|
+
import { domainHasEntities, getDomains, type Domain } from '@utils/collections/domains';
|
|
4
|
+
|
|
5
|
+
export class Page extends HybridPage {
|
|
6
|
+
static async getStaticPaths(): Promise<Array<{ params: any; props: any }>> {
|
|
7
|
+
if (isAuthEnabled()) {
|
|
8
|
+
return [];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const domains = await getDomains();
|
|
12
|
+
const domainsWithEntities = domains.filter((domain) => domainHasEntities(domain));
|
|
13
|
+
|
|
14
|
+
return domainsWithEntities.flatMap((domain) => {
|
|
15
|
+
return {
|
|
16
|
+
params: {
|
|
17
|
+
type: 'domains',
|
|
18
|
+
id: domain.data.id,
|
|
19
|
+
version: domain.data.version,
|
|
20
|
+
},
|
|
21
|
+
props: {
|
|
22
|
+
type: 'domains',
|
|
23
|
+
...domain,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
protected static async fetchData(params: any) {
|
|
30
|
+
const { id, version } = params;
|
|
31
|
+
|
|
32
|
+
if (!id || !version) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Get all items of the specified type
|
|
37
|
+
const items = await getDomains();
|
|
38
|
+
|
|
39
|
+
// Find the specific item by id and version, and only if it has entities
|
|
40
|
+
const item = items.find((i) => i.data.id === id && i.data.version === version && domainHasEntities(i));
|
|
41
|
+
|
|
42
|
+
console.log('ITEM', item);
|
|
43
|
+
|
|
44
|
+
if (!item) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return item;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
protected static createNotFoundResponse(): Response {
|
|
52
|
+
return new Response(null, {
|
|
53
|
+
status: 404,
|
|
54
|
+
statusText: 'Domain entity map page not found',
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
static get clientAuthScript(): string {
|
|
59
|
+
if (!isAuthEnabled()) {
|
|
60
|
+
return '';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return `
|
|
64
|
+
if (typeof window !== 'undefined' && !import.meta.env.SSR) {
|
|
65
|
+
fetch('/api/auth/session')
|
|
66
|
+
.then(res => res.json())
|
|
67
|
+
.then(session => {
|
|
68
|
+
if (!session?.user) {
|
|
69
|
+
window.location.href = '/auth/login?callbackUrl=' + encodeURIComponent(window.location.pathname);
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
.catch(() => {
|
|
73
|
+
window.location.href = '/auth/login?callbackUrl=' + encodeURIComponent(window.location.pathname);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
`;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
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
|
+
import { Page } from './_index.data';
|
|
8
|
+
|
|
9
|
+
export const prerender = Page.prerender;
|
|
10
|
+
export const getStaticPaths = Page.getStaticPaths;
|
|
11
|
+
|
|
12
|
+
// Get data
|
|
13
|
+
const props = await Page.getData(Astro);
|
|
14
|
+
|
|
15
|
+
const {
|
|
16
|
+
data: { id },
|
|
17
|
+
collection,
|
|
18
|
+
} = props;
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
<VisualiserLayout title={`Visualiser | ${props.data.name} (${props.collection})`} description={props.data.summary}>
|
|
22
|
+
<div class="bg-gray-100/50 m-4">
|
|
23
|
+
<div class="h-[calc(100vh-130px)] w-full relative border border-gray-200" id={`${id}-portal`} transition:animate="fade"></div>
|
|
24
|
+
<NodeGraph
|
|
25
|
+
id={id}
|
|
26
|
+
collection={`${collection}-entities`}
|
|
27
|
+
title={`${props.data.name} (v${props.data.version}) - Entity Map`}
|
|
28
|
+
mode="full"
|
|
29
|
+
linkTo="visualiser"
|
|
30
|
+
version={props.data.version}
|
|
31
|
+
linksToVisualiser={false}
|
|
32
|
+
href={{
|
|
33
|
+
label: `Open documentation for ${props.data.name} v${props.data.version}`,
|
|
34
|
+
url: buildUrl(`/docs/${props.collection}/${props.data.id}/${props.data.version}`),
|
|
35
|
+
}}
|
|
36
|
+
/>
|
|
37
|
+
</div>
|
|
38
|
+
<ClientRouter />
|
|
39
|
+
</VisualiserLayout>
|
|
40
|
+
|
|
41
|
+
<script define:vars={{ id }}>
|
|
42
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
43
|
+
const urlSearchParams = new URLSearchParams(window.location.search);
|
|
44
|
+
const params = Object.fromEntries(urlSearchParams.entries());
|
|
45
|
+
const embeded = params.embed === 'true' ? true : false;
|
|
46
|
+
const viewport = document.getElementById(`${id}-portal`);
|
|
47
|
+
|
|
48
|
+
if (embeded) {
|
|
49
|
+
viewport.style.height = 'calc(100vh - 30px)';
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
</script>
|
|
@@ -147,3 +147,7 @@ export const getDomainsForService = async (service: Service): Promise<Domain[]>
|
|
|
147
147
|
return services.some((s) => s.data.id === service.data.id);
|
|
148
148
|
});
|
|
149
149
|
};
|
|
150
|
+
|
|
151
|
+
export const domainHasEntities = (domain: Domain): boolean => {
|
|
152
|
+
return (domain.data.entities && domain.data.entities.length > 0) || false;
|
|
153
|
+
};
|
|
@@ -5,7 +5,7 @@ import { getVersionForCollectionItem, satisfies } from './collections/util';
|
|
|
5
5
|
|
|
6
6
|
const PROJECT_DIR = process.env.PROJECT_DIR || process.cwd();
|
|
7
7
|
|
|
8
|
-
type Entity = CollectionEntry<'entities'> & {
|
|
8
|
+
export type Entity = CollectionEntry<'entities'> & {
|
|
9
9
|
catalog: {
|
|
10
10
|
path: string;
|
|
11
11
|
filePath: string;
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { getCollection, getEntry } from 'astro:content';
|
|
2
|
+
import { generateIdForNode } from './utils/utils';
|
|
3
|
+
import ELK from 'elkjs/lib/elk.bundled.js';
|
|
4
|
+
import { MarkerType } from '@xyflow/react';
|
|
5
|
+
import { getItemsFromCollectionByIdAndSemverOrLatest } from '@utils/collections/util';
|
|
6
|
+
import { getVersionFromCollection } from '@utils/collections/versions';
|
|
7
|
+
import { getEntities, type Entity } from '@utils/entities';
|
|
8
|
+
import { getDomains, type Domain } from '@utils/collections/domains';
|
|
9
|
+
|
|
10
|
+
const elk = new ELK();
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
id: string;
|
|
14
|
+
version: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const getNodesAndEdges = async ({ id, version }: Props) => {
|
|
18
|
+
let nodes = [] as any,
|
|
19
|
+
edges = [] as any;
|
|
20
|
+
|
|
21
|
+
const allDomains = await getDomains();
|
|
22
|
+
const entities = await getEntities();
|
|
23
|
+
|
|
24
|
+
const domain = getVersionFromCollection(allDomains, id, version)[0] as Domain;
|
|
25
|
+
const domainEntities = (domain?.data?.entities ?? []) as any;
|
|
26
|
+
|
|
27
|
+
const entitiesWithReferences = domainEntities.filter((entity: Entity) =>
|
|
28
|
+
entity.data.properties?.some((property: any) => property.references)
|
|
29
|
+
);
|
|
30
|
+
// Creates all the entity nodes for the domain
|
|
31
|
+
for (const entity of domainEntities) {
|
|
32
|
+
const nodeId = generateIdForNode(entity);
|
|
33
|
+
nodes.push({
|
|
34
|
+
id: nodeId,
|
|
35
|
+
type: 'entities',
|
|
36
|
+
position: { x: 0, y: 0 },
|
|
37
|
+
data: { label: entity.data.name, entity, domainName: domain?.data.name, domainId: domain?.data.id },
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Create entities that are referenced but not owned by this domain
|
|
42
|
+
const listOfReferencedEntities = entitiesWithReferences
|
|
43
|
+
.map((entity: Entity) => entity.data.properties?.map((property: any) => property.references))
|
|
44
|
+
.flat()
|
|
45
|
+
.filter((ref: any) => ref !== undefined);
|
|
46
|
+
|
|
47
|
+
const externalToDomain = [...new Set(listOfReferencedEntities)] // Remove duplicates
|
|
48
|
+
.filter((entityId: any) => !domainEntities.some((domainEntity: any) => domainEntity.id === entityId));
|
|
49
|
+
|
|
50
|
+
// Helper function to find which domain an entity belongs to
|
|
51
|
+
const findEntityDomain = (entityId: string) => {
|
|
52
|
+
return allDomains.find((domain) => domain.data.entities?.some((domainEntity: any) => domainEntity.data.id === entityId));
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const addedExternalEntities = [];
|
|
56
|
+
for (const entityId of externalToDomain) {
|
|
57
|
+
const externalEntity = getItemsFromCollectionByIdAndSemverOrLatest(entities, entityId as string, 'latest')[0] as Entity;
|
|
58
|
+
|
|
59
|
+
if (externalEntity) {
|
|
60
|
+
const nodeId = generateIdForNode(externalEntity);
|
|
61
|
+
|
|
62
|
+
// Find which domain this entity belongs to
|
|
63
|
+
const entityDomain = findEntityDomain(entityId as string);
|
|
64
|
+
|
|
65
|
+
const domainName = entityDomain?.data.name || 'Unknown Domain';
|
|
66
|
+
const domainId = entityDomain?.data.id || 'unknown';
|
|
67
|
+
|
|
68
|
+
// Check if we haven't already added this entity
|
|
69
|
+
if (!nodes.some((node: any) => node.id === nodeId)) {
|
|
70
|
+
nodes.push({
|
|
71
|
+
id: nodeId,
|
|
72
|
+
type: 'entities',
|
|
73
|
+
position: { x: 0, y: 0 },
|
|
74
|
+
data: {
|
|
75
|
+
label: externalEntity.data.name,
|
|
76
|
+
entity: externalEntity,
|
|
77
|
+
externalToDomain: true,
|
|
78
|
+
domainName: domainName,
|
|
79
|
+
domainId: domainId,
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
addedExternalEntities.push(externalEntity);
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
console.warn(`Entity "${entityId}" not found in catalog`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Add external entities to the references list so edges will be created
|
|
90
|
+
entitiesWithReferences.push(...addedExternalEntities);
|
|
91
|
+
|
|
92
|
+
// Create complete list of entities for edge creation and layout
|
|
93
|
+
const allEntitiesInGraph = [...domainEntities, ...addedExternalEntities];
|
|
94
|
+
|
|
95
|
+
// Go through any entities that are related to other entities
|
|
96
|
+
for (const entity of entitiesWithReferences) {
|
|
97
|
+
// Get a list of properties that reference other entities
|
|
98
|
+
const allReferencesForEntity = entity.data.properties?.filter((property: any) => property.references) ?? [];
|
|
99
|
+
|
|
100
|
+
for (const referenceProperty of allReferencesForEntity) {
|
|
101
|
+
// Find the referenced entity by matching the references field with entity IDs
|
|
102
|
+
// Look in both domain entities and external entities
|
|
103
|
+
const referencedEntity = allEntitiesInGraph.find((targetEntity) => targetEntity.data.id === referenceProperty.references);
|
|
104
|
+
|
|
105
|
+
if (referencedEntity) {
|
|
106
|
+
const sourceNodeId = generateIdForNode(entity);
|
|
107
|
+
const targetNodeId = generateIdForNode(referencedEntity);
|
|
108
|
+
|
|
109
|
+
// Use the property name as the source handle
|
|
110
|
+
const sourceHandle = `${referenceProperty.name}-source`;
|
|
111
|
+
|
|
112
|
+
// Use referencesIdentifier if provided, otherwise use identifier or first property
|
|
113
|
+
let targetHandle = '';
|
|
114
|
+
if (referenceProperty.referencesIdentifier) {
|
|
115
|
+
targetHandle = `${referenceProperty.referencesIdentifier}-target`;
|
|
116
|
+
} else if (referencedEntity.data.identifier) {
|
|
117
|
+
targetHandle = `${referencedEntity.data.identifier}-target`;
|
|
118
|
+
} else if (referencedEntity.data.properties && referencedEntity.data.properties.length > 0) {
|
|
119
|
+
// Default to the first property if no identifier is specified
|
|
120
|
+
targetHandle = `${referencedEntity.data.properties[0].name}-target`;
|
|
121
|
+
} else {
|
|
122
|
+
// Skip this edge if we can't determine the target handle
|
|
123
|
+
console.warn(
|
|
124
|
+
`Could not determine target handle for reference from ${entity.data.name}.${referenceProperty.name} to ${referencedEntity.data.name}`
|
|
125
|
+
);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const edgeId = `${sourceNodeId}-${referenceProperty.name}-to-${targetNodeId}-${targetHandle.replace('-target', '')}`;
|
|
130
|
+
|
|
131
|
+
edges.push({
|
|
132
|
+
id: edgeId,
|
|
133
|
+
source: sourceNodeId,
|
|
134
|
+
sourceHandle: sourceHandle,
|
|
135
|
+
target: targetNodeId,
|
|
136
|
+
targetHandle: targetHandle,
|
|
137
|
+
type: 'animated',
|
|
138
|
+
animated: true,
|
|
139
|
+
label: referenceProperty.relationType || 'references',
|
|
140
|
+
style: {
|
|
141
|
+
strokeWidth: 2,
|
|
142
|
+
stroke: '#000', // gray color for relationship lines
|
|
143
|
+
strokeDasharray: '5,5', // dashed line
|
|
144
|
+
},
|
|
145
|
+
markerEnd: {
|
|
146
|
+
type: MarkerType.ArrowClosed,
|
|
147
|
+
width: 20,
|
|
148
|
+
height: 20,
|
|
149
|
+
color: '#000',
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
} else {
|
|
153
|
+
console.warn(
|
|
154
|
+
`Referenced entity "${referenceProperty.references}" not found for ${entity.data.name}.${referenceProperty.name}`
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// No virtual edges - only show actual relationships between entities
|
|
161
|
+
|
|
162
|
+
// Separate entities with and without relationships (including external entities)
|
|
163
|
+
const entitiesWithRelationships = allEntitiesInGraph.filter((entity) => {
|
|
164
|
+
// Has outgoing references
|
|
165
|
+
const hasOutgoingRefs = entity.data.properties?.some((property: any) => property.references);
|
|
166
|
+
// Has incoming references (is referenced by others)
|
|
167
|
+
const hasIncomingRefs = entitiesWithReferences.some((e: any) =>
|
|
168
|
+
e.data.properties?.some((prop: any) => prop.references === entity.data.id)
|
|
169
|
+
);
|
|
170
|
+
return hasOutgoingRefs || hasIncomingRefs;
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Prepare ELK graph structure
|
|
174
|
+
const elkNodes = nodes.map((node: any) => ({
|
|
175
|
+
id: node.id,
|
|
176
|
+
width: 280,
|
|
177
|
+
height: 200,
|
|
178
|
+
}));
|
|
179
|
+
|
|
180
|
+
const elkEdges = edges.map((edge: any) => ({
|
|
181
|
+
id: edge.id,
|
|
182
|
+
sources: [edge.source],
|
|
183
|
+
targets: [edge.target],
|
|
184
|
+
}));
|
|
185
|
+
|
|
186
|
+
const elkGraph = {
|
|
187
|
+
id: 'root',
|
|
188
|
+
layoutOptions: {
|
|
189
|
+
'elk.algorithm': 'force',
|
|
190
|
+
'elk.force.repulsivePower': '2.0',
|
|
191
|
+
'elk.force.iterations': '500',
|
|
192
|
+
'elk.spacing.nodeNode': '150',
|
|
193
|
+
'elk.spacing.edgeNode': '75',
|
|
194
|
+
'elk.spacing.edgeEdge': '30',
|
|
195
|
+
'elk.padding': '[top=50,left=50,bottom=50,right=50]',
|
|
196
|
+
'elk.separateConnectedComponents': 'true',
|
|
197
|
+
},
|
|
198
|
+
children: elkNodes,
|
|
199
|
+
edges: elkEdges,
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// Run ELK layout
|
|
203
|
+
const layoutedGraph = await elk.layout(elkGraph);
|
|
204
|
+
|
|
205
|
+
// Apply positions to nodes
|
|
206
|
+
const positionedNodes = nodes.map((node: any) => {
|
|
207
|
+
const elkNode = layoutedGraph.children?.find((n: any) => n.id === node.id);
|
|
208
|
+
return {
|
|
209
|
+
...node,
|
|
210
|
+
position: {
|
|
211
|
+
x: elkNode?.x || 0,
|
|
212
|
+
y: elkNode?.y || 0,
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
return { nodes: positionedNodes, edges };
|
|
218
|
+
};
|
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"url": "https://github.com/event-catalog/eventcatalog.git"
|
|
7
7
|
},
|
|
8
8
|
"type": "module",
|
|
9
|
-
"version": "2.
|
|
9
|
+
"version": "2.49.1",
|
|
10
10
|
"publishConfig": {
|
|
11
11
|
"access": "public"
|
|
12
12
|
},
|
|
@@ -71,6 +71,7 @@
|
|
|
71
71
|
"diff": "^7.0.0",
|
|
72
72
|
"diff2html": "^3.4.48",
|
|
73
73
|
"dotenv": "^16.5.0",
|
|
74
|
+
"elkjs": "^0.10.0",
|
|
74
75
|
"glob": "^10.4.1",
|
|
75
76
|
"gray-matter": "^4.0.3",
|
|
76
77
|
"html-to-image": "^1.11.11",
|