@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.
Files changed (28) 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-WHVRKXDZ.js → chunk-AK2PL55W.js} +1 -1
  6. package/dist/{chunk-WK7LQRVD.js → chunk-TVMUTOOL.js} +1 -1
  7. package/dist/{chunk-PYIL7PRQ.js → chunk-ZDOGODWQ.js} +1 -1
  8. package/dist/constants.cjs +1 -1
  9. package/dist/constants.js +1 -1
  10. package/dist/eventcatalog.cjs +1 -1
  11. package/dist/eventcatalog.js +3 -3
  12. package/eventcatalog/astro.config.mjs +1 -1
  13. package/eventcatalog/src/components/MDX/Admonition.tsx +2 -2
  14. package/eventcatalog/src/components/MDX/EntityMap/EntityMap.astro +61 -0
  15. package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.astro +10 -0
  16. package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.tsx +38 -21
  17. package/eventcatalog/src/components/MDX/NodeGraph/Nodes/Entity.tsx +151 -0
  18. package/eventcatalog/src/components/MDX/NodeGraph/VisualiserSearch.tsx +10 -0
  19. package/eventcatalog/src/components/MDX/components.tsx +2 -0
  20. package/eventcatalog/src/components/SideNav/ListViewSideBar/index.tsx +74 -60
  21. package/eventcatalog/src/content.config.ts +3 -0
  22. package/eventcatalog/src/hooks/eventcatalog-visualizer.ts +14 -2
  23. package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/entity-map/_index.data.ts +78 -0
  24. package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/entity-map/index.astro +52 -0
  25. package/eventcatalog/src/utils/collections/domains.ts +4 -0
  26. package/eventcatalog/src/utils/entities.ts +1 -1
  27. package/eventcatalog/src/utils/node-graphs/domain-entity-map.ts +218 -0
  28. package/package.json +2 -1
@@ -37,7 +37,7 @@ var import_axios = __toESM(require("axios"), 1);
37
37
  var import_os = __toESM(require("os"), 1);
38
38
 
39
39
  // package.json
40
- var version = "2.48.5";
40
+ var version = "2.49.1";
41
41
 
42
42
  // src/constants.ts
43
43
  var VERSION = version;
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  raiseEvent
3
- } from "../chunk-WK7LQRVD.js";
4
- import "../chunk-PYIL7PRQ.js";
3
+ } from "../chunk-TVMUTOOL.js";
4
+ import "../chunk-ZDOGODWQ.js";
5
5
  export {
6
6
  raiseEvent
7
7
  };
@@ -106,7 +106,7 @@ var import_axios = __toESM(require("axios"), 1);
106
106
  var import_os = __toESM(require("os"), 1);
107
107
 
108
108
  // package.json
109
- var version = "2.48.5";
109
+ var version = "2.49.1";
110
110
 
111
111
  // src/constants.ts
112
112
  var VERSION = version;
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  log_build_default
3
- } from "../chunk-WHVRKXDZ.js";
4
- import "../chunk-WK7LQRVD.js";
5
- import "../chunk-PYIL7PRQ.js";
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
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  raiseEvent
3
- } from "./chunk-WK7LQRVD.js";
3
+ } from "./chunk-TVMUTOOL.js";
4
4
  import {
5
5
  getEventCatalogConfigFile,
6
6
  verifyRequiredFieldsAreInCatalogConfigFile
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  VERSION
3
- } from "./chunk-PYIL7PRQ.js";
3
+ } from "./chunk-ZDOGODWQ.js";
4
4
 
5
5
  // src/analytics/analytics.js
6
6
  import axios from "axios";
@@ -1,5 +1,5 @@
1
1
  // package.json
2
- var version = "2.48.5";
2
+ var version = "2.49.1";
3
3
 
4
4
  // src/constants.ts
5
5
  var VERSION = version;
@@ -25,7 +25,7 @@ __export(constants_exports, {
25
25
  module.exports = __toCommonJS(constants_exports);
26
26
 
27
27
  // package.json
28
- var version = "2.48.5";
28
+ var version = "2.49.1";
29
29
 
30
30
  // src/constants.ts
31
31
  var VERSION = version;
package/dist/constants.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  VERSION
3
- } from "./chunk-PYIL7PRQ.js";
3
+ } from "./chunk-ZDOGODWQ.js";
4
4
  export {
5
5
  VERSION
6
6
  };
@@ -157,7 +157,7 @@ var import_axios = __toESM(require("axios"), 1);
157
157
  var import_os = __toESM(require("os"), 1);
158
158
 
159
159
  // package.json
160
- var version = "2.48.5";
160
+ var version = "2.49.1";
161
161
 
162
162
  // src/constants.ts
163
163
  var VERSION = version;
@@ -6,8 +6,8 @@ import {
6
6
  } from "./chunk-XE6PFSH5.js";
7
7
  import {
8
8
  log_build_default
9
- } from "./chunk-WHVRKXDZ.js";
10
- import "./chunk-WK7LQRVD.js";
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-PYIL7PRQ.js";
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 || true;
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
- const { hideChannels, toggleChannelsVisibility } = useEventCatalogVisualiser({ nodes, edges, setNodes, setEdges });
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
- <div>
544
- <div className="flex items-center justify-between">
545
- <label htmlFor="message-animation-toggle" className="text-sm font-medium text-gray-700">
546
- Hide Channels
547
- </label>
548
- <button
549
- id="message-animation-toggle"
550
- onClick={toggleChannelsVisibility}
551
- className={`${
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 ? 'translate-x-6' : 'translate-x-1'
558
- } inline-block h-4 w-4 transform rounded-full bg-white transition-transform`}
559
- />
560
- </button>
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
- <p className="text-[10px] text-gray-500">Show or hide channels in the visualizer.</p>
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(`${id}-portal`));
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(({ item, nestingLevel = 0 }: { item: any; nestingLevel?: number }) => {
489
- const marginLeft = nestingLevel > 0 ? `ml-${nestingLevel * 4}` : '';
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
- return (
492
- <div
493
- className={`overflow-hidden transition-[height] duration-150 ease-out ${collapsedGroups[item.href] ? 'h-0' : 'h-auto'}`}
494
- >
495
- <div className={`space-y-0.5 border-gray-200/80 border-l pl-4 ml-[9px] mt-1 ${marginLeft}`}>
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={buildUrl(`/docs/domains/${item.id}/language`)}
523
- data-active={decodedCurrentPath.includes(`/docs/domains/${item.id}/language`)}
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.includes(`/docs/domains/${item.id}/language`) ? 'bg-purple-100 ' : 'hover:bg-purple-100'
502
+ decodedCurrentPath === item.href ? 'bg-purple-100 ' : 'hover:bg-purple-100'
526
503
  }`}
527
504
  >
528
- <span className="truncate">Ubiquitous Language</span>
505
+ <span className="truncate">Overview</span>
529
506
  </a>
530
- )}
531
- {item.entities.length > 0 && !isVisualizer && (
532
- <CollapsibleGroup
533
- isCollapsed={collapsedGroups[`${item.href}-entities`]}
534
- onToggle={() => toggleGroupCollapse(`${item.href}-entities`)}
535
- title={
536
- <button
537
- onClick={(e) => {
538
- e.stopPropagation();
539
- toggleGroupCollapse(`${item.href}-entities`);
540
- }}
541
- className="truncate underline ml-2 text-xs mb-1 py-1"
542
- >
543
- Entities ({item.entities.length})
544
- </button>
545
- }
546
- >
547
- <MessageList messages={item.entities} decodedCurrentPath={decodedCurrentPath} searchTerm={debouncedSearchTerm} />
548
- </CollapsibleGroup>
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
- </div>
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={1} />
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 = ({ nodes, edges, setNodes, setEdges }: EventCatalogVisualizerProps) => {
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.48.5",
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",