@eventcatalog/core 3.10.1 → 3.11.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-SABEVBE7.js → chunk-744TUGLY.js} +1 -1
- package/dist/{chunk-M3NSMBIJ.js → chunk-JGYH3AAT.js} +1 -1
- package/dist/{chunk-2MSR3Y2A.js → chunk-LHPQHOE5.js} +1 -1
- package/dist/{chunk-BR3CTGCW.js → chunk-Q6BRAXMP.js} +1 -1
- package/dist/{chunk-G7XOCNOX.js → chunk-TE67QRWX.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog.cjs +2 -1
- package/dist/eventcatalog.config.d.cts +5 -0
- package/dist/eventcatalog.config.d.ts +5 -0
- package/dist/eventcatalog.js +6 -5
- package/dist/generate.cjs +1 -1
- package/dist/generate.js +3 -3
- package/dist/utils/cli-logger.cjs +1 -1
- package/dist/utils/cli-logger.js +2 -2
- package/eventcatalog/integrations/eventcatalog-features.ts +13 -0
- package/eventcatalog/src/components/MDX/Design/Design.astro +10 -2
- package/eventcatalog/src/components/MDX/EntityMap/EntityMap.astro +10 -2
- package/eventcatalog/src/components/MDX/Flow/Flow.astro +10 -2
- package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.astro +10 -2
- package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.tsx +146 -1
- package/eventcatalog/src/components/MDX/NodeGraph/VisualizerDropdownContent.tsx +91 -2
- package/eventcatalog/src/enterprise/visualizer-layout/reset.ts +45 -0
- package/eventcatalog/src/enterprise/visualizer-layout/save.ts +57 -0
- package/eventcatalog/src/pages/api/catalog.ts +12 -6
- package/eventcatalog/src/utils/feature.ts +2 -0
- package/eventcatalog/src/utils/node-graphs/layout-persistence.ts +81 -0
- package/package.json +1 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
log_build_default
|
|
3
|
-
} from "../chunk-
|
|
4
|
-
import "../chunk-
|
|
3
|
+
} from "../chunk-LHPQHOE5.js";
|
|
4
|
+
import "../chunk-JGYH3AAT.js";
|
|
5
5
|
import "../chunk-4UVFXLPI.js";
|
|
6
|
-
import "../chunk-
|
|
6
|
+
import "../chunk-744TUGLY.js";
|
|
7
7
|
import "../chunk-UPONRQSN.js";
|
|
8
8
|
export {
|
|
9
9
|
log_build_default as default
|
package/dist/constants.cjs
CHANGED
package/dist/constants.js
CHANGED
package/dist/eventcatalog.cjs
CHANGED
|
@@ -109,7 +109,7 @@ var verifyRequiredFieldsAreInCatalogConfigFile = async (projectDirectory) => {
|
|
|
109
109
|
var import_picocolors = __toESM(require("picocolors"), 1);
|
|
110
110
|
|
|
111
111
|
// package.json
|
|
112
|
-
var version = "3.
|
|
112
|
+
var version = "3.11.0";
|
|
113
113
|
|
|
114
114
|
// src/constants.ts
|
|
115
115
|
var VERSION = version;
|
|
@@ -812,6 +812,7 @@ program.command("dev").description("Run development server of EventCatalog").opt
|
|
|
812
812
|
ENABLE_EMBED: canEmbedPages || isEventCatalogScale,
|
|
813
813
|
EVENTCATALOG_STARTER: isEventCatalogStarter,
|
|
814
814
|
EVENTCATALOG_SCALE: isEventCatalogScale,
|
|
815
|
+
EVENTCATALOG_DEV_MODE: "true",
|
|
815
816
|
NODE_NO_WARNINGS: "1"
|
|
816
817
|
}
|
|
817
818
|
}
|
|
@@ -121,6 +121,11 @@ interface Config {
|
|
|
121
121
|
sidebar?: (ManualSideBarConfig | AutoGeneratedSideBarConfig)[];
|
|
122
122
|
};
|
|
123
123
|
api?: {
|
|
124
|
+
/**
|
|
125
|
+
* Enable or disable the /api/catalog endpoint that dumps the entire catalog as JSON.
|
|
126
|
+
* Disabling this can significantly reduce memory usage during builds for large catalogs (1000+ files).
|
|
127
|
+
* @default true
|
|
128
|
+
*/
|
|
124
129
|
fullCatalogAPIEnabled?: boolean;
|
|
125
130
|
};
|
|
126
131
|
changelog?: {
|
|
@@ -121,6 +121,11 @@ interface Config {
|
|
|
121
121
|
sidebar?: (ManualSideBarConfig | AutoGeneratedSideBarConfig)[];
|
|
122
122
|
};
|
|
123
123
|
api?: {
|
|
124
|
+
/**
|
|
125
|
+
* Enable or disable the /api/catalog endpoint that dumps the entire catalog as JSON.
|
|
126
|
+
* Disabling this can significantly reduce memory usage during builds for large catalogs (1000+ files).
|
|
127
|
+
* @default true
|
|
128
|
+
*/
|
|
124
129
|
fullCatalogAPIEnabled?: boolean;
|
|
125
130
|
};
|
|
126
131
|
changelog?: {
|
package/dist/eventcatalog.js
CHANGED
|
@@ -6,8 +6,8 @@ import {
|
|
|
6
6
|
} from "./chunk-PLNJC7NZ.js";
|
|
7
7
|
import {
|
|
8
8
|
log_build_default
|
|
9
|
-
} from "./chunk-
|
|
10
|
-
import "./chunk-
|
|
9
|
+
} from "./chunk-LHPQHOE5.js";
|
|
10
|
+
import "./chunk-JGYH3AAT.js";
|
|
11
11
|
import "./chunk-4UVFXLPI.js";
|
|
12
12
|
import {
|
|
13
13
|
runMigrations
|
|
@@ -22,13 +22,13 @@ import {
|
|
|
22
22
|
} from "./chunk-5VBIXL6C.js";
|
|
23
23
|
import {
|
|
24
24
|
generate
|
|
25
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-Q6BRAXMP.js";
|
|
26
26
|
import {
|
|
27
27
|
logger
|
|
28
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-TE67QRWX.js";
|
|
29
29
|
import {
|
|
30
30
|
VERSION
|
|
31
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-744TUGLY.js";
|
|
32
32
|
import "./chunk-UPONRQSN.js";
|
|
33
33
|
|
|
34
34
|
// src/eventcatalog.ts
|
|
@@ -165,6 +165,7 @@ program.command("dev").description("Run development server of EventCatalog").opt
|
|
|
165
165
|
ENABLE_EMBED: canEmbedPages || isEventCatalogScale,
|
|
166
166
|
EVENTCATALOG_STARTER: isEventCatalogStarter,
|
|
167
167
|
EVENTCATALOG_SCALE: isEventCatalogScale,
|
|
168
|
+
EVENTCATALOG_DEV_MODE: "true",
|
|
168
169
|
NODE_NO_WARNINGS: "1"
|
|
169
170
|
}
|
|
170
171
|
}
|
package/dist/generate.cjs
CHANGED
package/dist/generate.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
generate
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
5
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-Q6BRAXMP.js";
|
|
4
|
+
import "./chunk-TE67QRWX.js";
|
|
5
|
+
import "./chunk-744TUGLY.js";
|
|
6
6
|
import "./chunk-UPONRQSN.js";
|
|
7
7
|
export {
|
|
8
8
|
generate
|
package/dist/utils/cli-logger.js
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
isEventCatalogScaleEnabled,
|
|
7
7
|
isEventCatalogStarterEnabled,
|
|
8
8
|
isEventCatalogMCPEnabled,
|
|
9
|
+
isDevMode,
|
|
9
10
|
} from '../src/utils/feature';
|
|
10
11
|
|
|
11
12
|
const catalogDirectory = process.env.CATALOG_DIR || process.cwd();
|
|
@@ -72,6 +73,18 @@ export default function eventCatalogIntegration(): AstroIntegration {
|
|
|
72
73
|
entrypoint: path.join(catalogDirectory, 'src/enterprise/plans/index.astro'),
|
|
73
74
|
});
|
|
74
75
|
}
|
|
76
|
+
|
|
77
|
+
// Dev-only routes for visualizer layout persistence
|
|
78
|
+
if (isDevMode()) {
|
|
79
|
+
params.injectRoute({
|
|
80
|
+
pattern: '/api/dev/visualizer-layout/save',
|
|
81
|
+
entrypoint: path.join(catalogDirectory, 'src/enterprise/visualizer-layout/save.ts'),
|
|
82
|
+
});
|
|
83
|
+
params.injectRoute({
|
|
84
|
+
pattern: '/api/dev/visualizer-layout/reset',
|
|
85
|
+
entrypoint: path.join(catalogDirectory, 'src/enterprise/visualizer-layout/reset.ts'),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
75
88
|
},
|
|
76
89
|
},
|
|
77
90
|
};
|
|
@@ -5,7 +5,8 @@ import { getAbsoluteFilePathForAstroFile } from '@utils/files';
|
|
|
5
5
|
import Admonition from '@components/MDX/Admonition';
|
|
6
6
|
import NodeGraph from '../NodeGraph/NodeGraph';
|
|
7
7
|
|
|
8
|
-
import { isVisualiserEnabled, isEventCatalogChatEnabled } from '@utils/feature';
|
|
8
|
+
import { isVisualiserEnabled, isEventCatalogChatEnabled, isDevMode } from '@utils/feature';
|
|
9
|
+
import { loadSavedLayout, applyLayoutToNodes, buildResourceKey } from '@utils/node-graphs/layout-persistence';
|
|
9
10
|
|
|
10
11
|
const isChatEnabled = isEventCatalogChatEnabled();
|
|
11
12
|
|
|
@@ -21,6 +22,11 @@ try {
|
|
|
21
22
|
} catch (error) {
|
|
22
23
|
console.error(`Error reading design file: ${error}`);
|
|
23
24
|
}
|
|
25
|
+
|
|
26
|
+
// Load and apply saved layout if it exists
|
|
27
|
+
const resourceKey = design ? buildResourceKey('designs', design.id || id) : '';
|
|
28
|
+
const savedLayout = resourceKey ? await loadSavedLayout(resourceKey) : null;
|
|
29
|
+
const nodesWithLayout = design ? applyLayoutToNodes(design.nodes || [], savedLayout) : ([] as any[]);
|
|
24
30
|
---
|
|
25
31
|
|
|
26
32
|
{
|
|
@@ -51,7 +57,7 @@ try {
|
|
|
51
57
|
<NodeGraph
|
|
52
58
|
id={id}
|
|
53
59
|
title={title ?? design.name}
|
|
54
|
-
nodes={
|
|
60
|
+
nodes={nodesWithLayout}
|
|
55
61
|
edges={design.edges || []}
|
|
56
62
|
hrefLabel={isVisualiserEnabled() ? 'View in visualizer' : undefined}
|
|
57
63
|
href={isVisualiserEnabled() ? `/visualiser/designs/${design.id}` : undefined}
|
|
@@ -60,6 +66,8 @@ try {
|
|
|
60
66
|
client:only="react"
|
|
61
67
|
showSearch={search}
|
|
62
68
|
isChatEnabled={isChatEnabled}
|
|
69
|
+
isDevMode={isDevMode()}
|
|
70
|
+
resourceKey={resourceKey}
|
|
63
71
|
/>
|
|
64
72
|
</div>
|
|
65
73
|
</div>
|
|
@@ -5,7 +5,8 @@ import Admonition from '@components/MDX/Admonition';
|
|
|
5
5
|
import NodeGraph from '../NodeGraph/NodeGraph';
|
|
6
6
|
import { getVersionFromCollection } from '@utils/collections/versions';
|
|
7
7
|
import { getServices } from '@utils/collections/services';
|
|
8
|
-
import { isEventCatalogChatEnabled } from '@utils/feature';
|
|
8
|
+
import { isEventCatalogChatEnabled, isDevMode } from '@utils/feature';
|
|
9
|
+
import { loadSavedLayout, applyLayoutToNodes, buildResourceKey } from '@utils/node-graphs/layout-persistence';
|
|
9
10
|
|
|
10
11
|
const isChatEnabled = isEventCatalogChatEnabled();
|
|
11
12
|
|
|
@@ -38,6 +39,11 @@ const { nodes, edges } = await getNodesAndEdges({
|
|
|
38
39
|
...(entities ? { entities } : {}), // Pass entities if provided
|
|
39
40
|
type: collection,
|
|
40
41
|
});
|
|
42
|
+
|
|
43
|
+
// Load and apply saved layout if it exists
|
|
44
|
+
const resourceKey = buildResourceKey(collection, resourceId, resource?.data?.version);
|
|
45
|
+
const savedLayout = await loadSavedLayout(resourceKey);
|
|
46
|
+
const nodesWithLayout = applyLayoutToNodes(nodes as any[], savedLayout);
|
|
41
47
|
---
|
|
42
48
|
|
|
43
49
|
{
|
|
@@ -66,7 +72,7 @@ const { nodes, edges } = await getNodesAndEdges({
|
|
|
66
72
|
<div>
|
|
67
73
|
<NodeGraph
|
|
68
74
|
id={id}
|
|
69
|
-
nodes={
|
|
75
|
+
nodes={nodesWithLayout}
|
|
70
76
|
edges={edges}
|
|
71
77
|
linkTo={'visualiser'}
|
|
72
78
|
mode="simple"
|
|
@@ -75,6 +81,8 @@ const { nodes, edges } = await getNodesAndEdges({
|
|
|
75
81
|
client:only="react"
|
|
76
82
|
portalId={`${id}-entity-map-portal`}
|
|
77
83
|
isChatEnabled={isChatEnabled}
|
|
84
|
+
isDevMode={isDevMode()}
|
|
85
|
+
resourceKey={resourceKey}
|
|
78
86
|
/>
|
|
79
87
|
</div>
|
|
80
88
|
|
|
@@ -4,7 +4,8 @@ import { getNodesAndEdges } from '@utils/node-graphs/flows-node-graph';
|
|
|
4
4
|
import Admonition from '@components/MDX/Admonition';
|
|
5
5
|
import NodeGraph from '../NodeGraph/NodeGraph';
|
|
6
6
|
import { getVersionFromCollection } from '@utils/collections/versions';
|
|
7
|
-
import { isVisualiserEnabled, isEventCatalogChatEnabled } from '@utils/feature';
|
|
7
|
+
import { isVisualiserEnabled, isEventCatalogChatEnabled, isDevMode } from '@utils/feature';
|
|
8
|
+
import { loadSavedLayout, applyLayoutToNodes, buildResourceKey } from '@utils/node-graphs/layout-persistence';
|
|
8
9
|
|
|
9
10
|
const isChatEnabled = isEventCatalogChatEnabled();
|
|
10
11
|
|
|
@@ -22,6 +23,11 @@ const { nodes, edges } = await getNodesAndEdges({
|
|
|
22
23
|
version: flow.data.version,
|
|
23
24
|
mode: mode,
|
|
24
25
|
});
|
|
26
|
+
|
|
27
|
+
// Load and apply saved layout if it exists
|
|
28
|
+
const resourceKey = buildResourceKey('flows', id, flow.data.version);
|
|
29
|
+
const savedLayout = await loadSavedLayout(resourceKey);
|
|
30
|
+
const nodesWithLayout = applyLayoutToNodes(nodes, savedLayout);
|
|
25
31
|
---
|
|
26
32
|
|
|
27
33
|
{
|
|
@@ -49,7 +55,7 @@ const { nodes, edges } = await getNodesAndEdges({
|
|
|
49
55
|
<div>
|
|
50
56
|
<NodeGraph
|
|
51
57
|
id={id}
|
|
52
|
-
nodes={
|
|
58
|
+
nodes={nodesWithLayout}
|
|
53
59
|
edges={edges}
|
|
54
60
|
hrefLabel={'View in visualizer'}
|
|
55
61
|
href={isVisualiserEnabled() ? `/visualiser/flows/${id}/${version}` : undefined}
|
|
@@ -60,6 +66,8 @@ const { nodes, edges } = await getNodesAndEdges({
|
|
|
60
66
|
showFlowWalkthrough={walkthrough}
|
|
61
67
|
showSearch={search}
|
|
62
68
|
isChatEnabled={isChatEnabled}
|
|
69
|
+
isDevMode={isDevMode()}
|
|
70
|
+
resourceKey={resourceKey}
|
|
63
71
|
/>
|
|
64
72
|
</div>
|
|
65
73
|
|
|
@@ -19,7 +19,8 @@ import { getVersionFromCollection } from '@utils/collections/versions';
|
|
|
19
19
|
import { pageDataLoader } from '@utils/page-loaders/page-data-loader';
|
|
20
20
|
import { getNodesAndEdges as getNodesAndEdgesForContainer } from '@utils/node-graphs/container-node-graph';
|
|
21
21
|
import config from '@config';
|
|
22
|
-
import { isEventCatalogChatEnabled } from '@utils/feature';
|
|
22
|
+
import { isEventCatalogChatEnabled, isDevMode } from '@utils/feature';
|
|
23
|
+
import { loadSavedLayout, applyLayoutToNodes, buildResourceKey } from '@utils/node-graphs/layout-persistence';
|
|
23
24
|
|
|
24
25
|
const isChatEnabled = isEventCatalogChatEnabled();
|
|
25
26
|
|
|
@@ -139,12 +140,17 @@ if (collection === 'services-containers') {
|
|
|
139
140
|
nodes = fetchedNodes;
|
|
140
141
|
edges = fetchedEdges;
|
|
141
142
|
}
|
|
143
|
+
|
|
144
|
+
// Load and apply saved layout if it exists (dev mode feature)
|
|
145
|
+
const resourceKey = buildResourceKey(collection, id, version);
|
|
146
|
+
const savedLayout = await loadSavedLayout(resourceKey);
|
|
147
|
+
const nodesWithLayout = applyLayoutToNodes(nodes as any[], savedLayout);
|
|
142
148
|
---
|
|
143
149
|
|
|
144
150
|
<div>
|
|
145
151
|
<NodeGraphNew
|
|
146
152
|
id={id}
|
|
147
|
-
nodes={
|
|
153
|
+
nodes={nodesWithLayout}
|
|
148
154
|
edges={edges}
|
|
149
155
|
title={title}
|
|
150
156
|
hrefLabel={href?.label}
|
|
@@ -159,6 +165,8 @@ if (collection === 'services-containers') {
|
|
|
159
165
|
zoomOnScroll={zoomOnScroll}
|
|
160
166
|
isChatEnabled={isChatEnabled}
|
|
161
167
|
maxTextSize={config.mermaid?.maxTextSize}
|
|
168
|
+
isDevMode={isDevMode()}
|
|
169
|
+
resourceKey={resourceKey}
|
|
162
170
|
/>
|
|
163
171
|
</div>
|
|
164
172
|
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
useEdgesState,
|
|
13
13
|
type Edge,
|
|
14
14
|
type Node,
|
|
15
|
+
type NodeChange,
|
|
15
16
|
useReactFlow,
|
|
16
17
|
getNodesBounds,
|
|
17
18
|
getViewportForBounds,
|
|
@@ -58,6 +59,9 @@ import VisualizerDropdownContent from './VisualizerDropdownContent';
|
|
|
58
59
|
import { convertToMermaid } from '@utils/node-graphs/export-mermaid';
|
|
59
60
|
import { copyToClipboard } from '@utils/clipboard';
|
|
60
61
|
|
|
62
|
+
// Minimum pixel change to detect layout modifications (avoids floating point comparison issues)
|
|
63
|
+
const POSITION_CHANGE_THRESHOLD = 1;
|
|
64
|
+
|
|
61
65
|
interface Props {
|
|
62
66
|
nodes: any;
|
|
63
67
|
edges: any;
|
|
@@ -78,6 +82,8 @@ interface Props {
|
|
|
78
82
|
setIsStudioModalOpen?: (isOpen: boolean) => void;
|
|
79
83
|
isChatEnabled?: boolean;
|
|
80
84
|
maxTextSize?: number;
|
|
85
|
+
isDevMode?: boolean;
|
|
86
|
+
resourceKey?: string;
|
|
81
87
|
}
|
|
82
88
|
|
|
83
89
|
const getVisualiserUrlForCollection = (collectionItem: CollectionEntry<CollectionTypes>) => {
|
|
@@ -101,6 +107,8 @@ const NodeGraphBuilder = ({
|
|
|
101
107
|
setIsStudioModalOpen = () => {},
|
|
102
108
|
isChatEnabled = false,
|
|
103
109
|
maxTextSize,
|
|
110
|
+
isDevMode = false,
|
|
111
|
+
resourceKey,
|
|
104
112
|
}: Props) => {
|
|
105
113
|
const nodeTypes = useMemo(
|
|
106
114
|
() =>
|
|
@@ -153,6 +161,9 @@ const NodeGraphBuilder = ({
|
|
|
153
161
|
const [shareUrlCopySuccess, setShareUrlCopySuccess] = useState(false);
|
|
154
162
|
const [isMermaidView, setIsMermaidView] = useState(false);
|
|
155
163
|
const [showMinimap, setShowMinimap] = useState(false);
|
|
164
|
+
const [hasLayoutChanges, setHasLayoutChanges] = useState(false);
|
|
165
|
+
const [isSavingLayout, setIsSavingLayout] = useState(false);
|
|
166
|
+
const initialPositionsRef = useRef<Record<string, { x: number; y: number }>>({});
|
|
156
167
|
// const [isStudioModalOpen, setIsStudioModalOpen] = useState(false);
|
|
157
168
|
|
|
158
169
|
// Check if there are channels to determine if we need the visualizer functionality
|
|
@@ -169,6 +180,49 @@ const NodeGraphBuilder = ({
|
|
|
169
180
|
const reactFlowWrapperRef = useRef<HTMLDivElement>(null);
|
|
170
181
|
const scrollableContainerRef = useRef<HTMLElement | null>(null);
|
|
171
182
|
|
|
183
|
+
// Store initial node positions for change detection (dev mode only)
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
if (isDevMode && initialNodes.length > 0) {
|
|
186
|
+
const positions: Record<string, { x: number; y: number }> = {};
|
|
187
|
+
initialNodes.forEach((node: Node) => {
|
|
188
|
+
positions[node.id] = { x: node.position.x, y: node.position.y };
|
|
189
|
+
});
|
|
190
|
+
initialPositionsRef.current = positions;
|
|
191
|
+
}
|
|
192
|
+
}, [isDevMode, initialNodes]);
|
|
193
|
+
|
|
194
|
+
// Detect layout changes by comparing current positions to initial positions
|
|
195
|
+
const checkForLayoutChanges = useCallback(() => {
|
|
196
|
+
if (!isDevMode) return;
|
|
197
|
+
const initial = initialPositionsRef.current;
|
|
198
|
+
if (Object.keys(initial).length === 0) return;
|
|
199
|
+
|
|
200
|
+
const hasChanges = nodes.some((node) => {
|
|
201
|
+
const initialPos = initial[node.id];
|
|
202
|
+
return (
|
|
203
|
+
initialPos &&
|
|
204
|
+
(Math.abs(node.position.x - initialPos.x) > POSITION_CHANGE_THRESHOLD ||
|
|
205
|
+
Math.abs(node.position.y - initialPos.y) > POSITION_CHANGE_THRESHOLD)
|
|
206
|
+
);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
setHasLayoutChanges(hasChanges);
|
|
210
|
+
}, [isDevMode, nodes]);
|
|
211
|
+
|
|
212
|
+
// Wrap onNodesChange to detect layout changes after node drag
|
|
213
|
+
const handleNodesChange = useCallback(
|
|
214
|
+
(changes: NodeChange[]) => {
|
|
215
|
+
onNodesChange(changes);
|
|
216
|
+
// Check for position changes after drag ends
|
|
217
|
+
const hasDragEnd = changes.some((change) => change.type === 'position' && !change.dragging);
|
|
218
|
+
if (hasDragEnd) {
|
|
219
|
+
// Use setTimeout to ensure state is updated
|
|
220
|
+
setTimeout(checkForLayoutChanges, 0);
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
[onNodesChange, checkForLayoutChanges]
|
|
224
|
+
);
|
|
225
|
+
|
|
172
226
|
const resetNodesAndEdges = useCallback(() => {
|
|
173
227
|
setNodes((nds) =>
|
|
174
228
|
nds.map((node) => {
|
|
@@ -397,6 +451,63 @@ const NodeGraphBuilder = ({
|
|
|
397
451
|
window.dispatchEvent(new CustomEvent('eventcatalog:open-chat'));
|
|
398
452
|
}, []);
|
|
399
453
|
|
|
454
|
+
// Layout persistence handlers (dev mode only)
|
|
455
|
+
const handleSaveLayout = useCallback(async (): Promise<boolean> => {
|
|
456
|
+
if (!resourceKey) return false;
|
|
457
|
+
|
|
458
|
+
const positions: Record<string, { x: number; y: number }> = {};
|
|
459
|
+
nodes.forEach((node) => {
|
|
460
|
+
positions[node.id] = {
|
|
461
|
+
x: node.position.x,
|
|
462
|
+
y: node.position.y,
|
|
463
|
+
};
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
try {
|
|
467
|
+
const response = await fetch('/api/dev/visualizer-layout/save', {
|
|
468
|
+
method: 'POST',
|
|
469
|
+
headers: { 'Content-Type': 'application/json' },
|
|
470
|
+
body: JSON.stringify({ resourceKey, positions }),
|
|
471
|
+
});
|
|
472
|
+
const result = await response.json();
|
|
473
|
+
return result.success === true;
|
|
474
|
+
} catch {
|
|
475
|
+
return false;
|
|
476
|
+
}
|
|
477
|
+
}, [nodes, resourceKey]);
|
|
478
|
+
|
|
479
|
+
const handleResetLayout = useCallback(async (): Promise<boolean> => {
|
|
480
|
+
if (!resourceKey) return false;
|
|
481
|
+
|
|
482
|
+
try {
|
|
483
|
+
const response = await fetch('/api/dev/visualizer-layout/reset', {
|
|
484
|
+
method: 'POST',
|
|
485
|
+
headers: { 'Content-Type': 'application/json' },
|
|
486
|
+
body: JSON.stringify({ resourceKey }),
|
|
487
|
+
});
|
|
488
|
+
const result = await response.json();
|
|
489
|
+
return result.success === true;
|
|
490
|
+
} catch {
|
|
491
|
+
return false;
|
|
492
|
+
}
|
|
493
|
+
}, [resourceKey]);
|
|
494
|
+
|
|
495
|
+
// Quick save handler for the change detection UI
|
|
496
|
+
const handleQuickSaveLayout = useCallback(async () => {
|
|
497
|
+
setIsSavingLayout(true);
|
|
498
|
+
const success = await handleSaveLayout();
|
|
499
|
+
setIsSavingLayout(false);
|
|
500
|
+
if (success) {
|
|
501
|
+
// Update initial positions to current positions after save
|
|
502
|
+
const positions: Record<string, { x: number; y: number }> = {};
|
|
503
|
+
nodes.forEach((node) => {
|
|
504
|
+
positions[node.id] = { x: node.position.x, y: node.position.y };
|
|
505
|
+
});
|
|
506
|
+
initialPositionsRef.current = positions;
|
|
507
|
+
setHasLayoutChanges(false);
|
|
508
|
+
}
|
|
509
|
+
}, [handleSaveLayout, nodes]);
|
|
510
|
+
|
|
400
511
|
const handleCopyArchitectureCode = useCallback(async () => {
|
|
401
512
|
await copyToClipboard(mermaidCode);
|
|
402
513
|
}, [mermaidCode]);
|
|
@@ -692,6 +803,9 @@ const NodeGraphBuilder = ({
|
|
|
692
803
|
setIsShareModalOpen={setIsShareModalOpen}
|
|
693
804
|
toggleFullScreen={toggleFullScreen}
|
|
694
805
|
openStudioModal={openStudioModal}
|
|
806
|
+
isDevMode={isDevMode}
|
|
807
|
+
onSaveLayout={handleSaveLayout}
|
|
808
|
+
onResetLayout={handleResetLayout}
|
|
695
809
|
/>
|
|
696
810
|
</DropdownMenu.Content>
|
|
697
811
|
</DropdownMenu.Portal>
|
|
@@ -720,7 +834,7 @@ const NodeGraphBuilder = ({
|
|
|
720
834
|
nodes={nodes}
|
|
721
835
|
edges={edges}
|
|
722
836
|
fitView
|
|
723
|
-
onNodesChange={
|
|
837
|
+
onNodesChange={handleNodesChange}
|
|
724
838
|
onEdgesChange={onEdgesChange}
|
|
725
839
|
connectionLineType={ConnectionLineType.SmoothStep}
|
|
726
840
|
nodeOrigin={[0.1, 0.1]}
|
|
@@ -772,6 +886,9 @@ const NodeGraphBuilder = ({
|
|
|
772
886
|
setIsShareModalOpen={setIsShareModalOpen}
|
|
773
887
|
toggleFullScreen={toggleFullScreen}
|
|
774
888
|
openStudioModal={openStudioModal}
|
|
889
|
+
isDevMode={isDevMode}
|
|
890
|
+
onSaveLayout={handleSaveLayout}
|
|
891
|
+
onResetLayout={handleResetLayout}
|
|
775
892
|
/>
|
|
776
893
|
</DropdownMenu.Content>
|
|
777
894
|
</DropdownMenu.Portal>
|
|
@@ -845,6 +962,28 @@ const NodeGraphBuilder = ({
|
|
|
845
962
|
/>
|
|
846
963
|
</Panel>
|
|
847
964
|
)}
|
|
965
|
+
{/* Dev Mode: Layout change indicator */}
|
|
966
|
+
{isDevMode && hasLayoutChanges && (
|
|
967
|
+
<Panel
|
|
968
|
+
position="bottom-left"
|
|
969
|
+
style={
|
|
970
|
+
isFlowVisualization && showFlowWalkthrough
|
|
971
|
+
? { marginBottom: '20px', marginLeft: '410px' }
|
|
972
|
+
: { marginLeft: '60px' }
|
|
973
|
+
}
|
|
974
|
+
>
|
|
975
|
+
<div className="bg-[rgb(var(--ec-card-bg))] border border-[rgb(var(--ec-page-border))] rounded-lg shadow-md px-3 py-2 flex items-center gap-3">
|
|
976
|
+
<span className="text-xs text-[rgb(var(--ec-page-text-muted))]">Layout changed</span>
|
|
977
|
+
<button
|
|
978
|
+
onClick={handleQuickSaveLayout}
|
|
979
|
+
disabled={isSavingLayout}
|
|
980
|
+
className="text-xs font-medium text-[rgb(var(--ec-accent-text))] bg-[rgb(var(--ec-accent-subtle))] hover:bg-[rgb(var(--ec-accent-subtle)/0.7)] px-2 py-1 rounded transition-colors disabled:opacity-50"
|
|
981
|
+
>
|
|
982
|
+
{isSavingLayout ? 'Saving...' : 'Save'}
|
|
983
|
+
</button>
|
|
984
|
+
</div>
|
|
985
|
+
</Panel>
|
|
986
|
+
)}
|
|
848
987
|
{includeKey && (
|
|
849
988
|
<Panel position="bottom-right" style={showMinimap ? { marginRight: '230px' } : undefined}>
|
|
850
989
|
<div className=" bg-white font-light px-4 text-[12px] shadow-md py-1 rounded-md">
|
|
@@ -952,6 +1091,8 @@ interface NodeGraphProps {
|
|
|
952
1091
|
designId?: string;
|
|
953
1092
|
isChatEnabled?: boolean;
|
|
954
1093
|
maxTextSize?: number;
|
|
1094
|
+
isDevMode?: boolean;
|
|
1095
|
+
resourceKey?: string;
|
|
955
1096
|
}
|
|
956
1097
|
|
|
957
1098
|
const NodeGraph = ({
|
|
@@ -974,6 +1115,8 @@ const NodeGraph = ({
|
|
|
974
1115
|
designId,
|
|
975
1116
|
isChatEnabled = false,
|
|
976
1117
|
maxTextSize,
|
|
1118
|
+
isDevMode = false,
|
|
1119
|
+
resourceKey,
|
|
977
1120
|
}: NodeGraphProps) => {
|
|
978
1121
|
const [elem, setElem] = useState(null);
|
|
979
1122
|
const [showFooter, setShowFooter] = useState(true);
|
|
@@ -1021,6 +1164,8 @@ const NodeGraph = ({
|
|
|
1021
1164
|
setIsStudioModalOpen={setIsStudioModalOpen}
|
|
1022
1165
|
isChatEnabled={isChatEnabled}
|
|
1023
1166
|
maxTextSize={maxTextSize}
|
|
1167
|
+
isDevMode={isDevMode}
|
|
1168
|
+
resourceKey={resourceKey}
|
|
1024
1169
|
/>
|
|
1025
1170
|
|
|
1026
1171
|
{showFooter && (
|
|
@@ -1,6 +1,20 @@
|
|
|
1
|
-
import React, { type RefObject } from 'react';
|
|
1
|
+
import React, { type RefObject, useState } from 'react';
|
|
2
2
|
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
Code,
|
|
5
|
+
Share2,
|
|
6
|
+
Search,
|
|
7
|
+
Grid3x3,
|
|
8
|
+
Maximize2,
|
|
9
|
+
Map,
|
|
10
|
+
Sparkles,
|
|
11
|
+
Zap,
|
|
12
|
+
EyeOff,
|
|
13
|
+
ExternalLink,
|
|
14
|
+
Save,
|
|
15
|
+
RotateCcw,
|
|
16
|
+
Loader2,
|
|
17
|
+
} from 'lucide-react';
|
|
4
18
|
import { DocumentArrowDownIcon, PresentationChartLineIcon } from '@heroicons/react/24/outline';
|
|
5
19
|
import type { VisualiserSearchRef } from './VisualiserSearch';
|
|
6
20
|
|
|
@@ -23,6 +37,9 @@ interface VisualizerDropdownContentProps {
|
|
|
23
37
|
setIsShareModalOpen: (value: boolean) => void;
|
|
24
38
|
toggleFullScreen: () => void;
|
|
25
39
|
openStudioModal: () => void;
|
|
40
|
+
isDevMode?: boolean;
|
|
41
|
+
onSaveLayout?: () => Promise<boolean>;
|
|
42
|
+
onResetLayout?: () => Promise<boolean>;
|
|
26
43
|
}
|
|
27
44
|
|
|
28
45
|
const VisualizerDropdownContent: React.FC<VisualizerDropdownContentProps> = ({
|
|
@@ -44,7 +61,33 @@ const VisualizerDropdownContent: React.FC<VisualizerDropdownContentProps> = ({
|
|
|
44
61
|
setIsShareModalOpen,
|
|
45
62
|
toggleFullScreen,
|
|
46
63
|
openStudioModal,
|
|
64
|
+
isDevMode = false,
|
|
65
|
+
onSaveLayout,
|
|
66
|
+
onResetLayout,
|
|
47
67
|
}) => {
|
|
68
|
+
const [layoutStatus, setLayoutStatus] = useState<'idle' | 'saving' | 'resetting'>('idle');
|
|
69
|
+
|
|
70
|
+
const handleSaveLayout = async () => {
|
|
71
|
+
if (!onSaveLayout) return;
|
|
72
|
+
setLayoutStatus('saving');
|
|
73
|
+
await onSaveLayout();
|
|
74
|
+
setLayoutStatus('idle');
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const handleResetLayout = async () => {
|
|
78
|
+
if (!onResetLayout) return;
|
|
79
|
+
if (!window.confirm('Reset layout to auto-positioning? This will delete your saved layout.')) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
setLayoutStatus('resetting');
|
|
83
|
+
const success = await onResetLayout();
|
|
84
|
+
if (success) {
|
|
85
|
+
window.location.reload();
|
|
86
|
+
} else {
|
|
87
|
+
setLayoutStatus('idle');
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
48
91
|
return (
|
|
49
92
|
<>
|
|
50
93
|
{/* Canvas Settings Submenu */}
|
|
@@ -157,6 +200,52 @@ const VisualizerDropdownContent: React.FC<VisualizerDropdownContentProps> = ({
|
|
|
157
200
|
</DropdownMenu.Portal>
|
|
158
201
|
</DropdownMenu.Sub>
|
|
159
202
|
|
|
203
|
+
{/* Dev Mode: Layout Submenu */}
|
|
204
|
+
{isDevMode && onSaveLayout && (
|
|
205
|
+
<DropdownMenu.Sub>
|
|
206
|
+
<DropdownMenu.SubTrigger className="flex items-center px-3 py-2 text-xs text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-accent-subtle)/0.3)] cursor-pointer transition-colors gap-2 outline-none">
|
|
207
|
+
<Save className="w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" />
|
|
208
|
+
<span className="flex-1 font-normal">Layout</span>
|
|
209
|
+
<span className="text-[10px] text-amber-600 font-medium">DEV</span>
|
|
210
|
+
<svg className="w-3 h-3 text-[rgb(var(--ec-page-text-muted))]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
211
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
|
212
|
+
</svg>
|
|
213
|
+
</DropdownMenu.SubTrigger>
|
|
214
|
+
<DropdownMenu.Portal>
|
|
215
|
+
<DropdownMenu.SubContent
|
|
216
|
+
className="min-w-[180px] bg-[rgb(var(--ec-card-bg))] rounded-lg shadow-xl border border-[rgb(var(--ec-page-border))] py-1.5 z-[60]"
|
|
217
|
+
sideOffset={8}
|
|
218
|
+
alignOffset={-8}
|
|
219
|
+
>
|
|
220
|
+
<DropdownMenu.Item
|
|
221
|
+
onClick={handleSaveLayout}
|
|
222
|
+
disabled={layoutStatus !== 'idle'}
|
|
223
|
+
className="px-3 py-2 text-xs text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-accent-subtle)/0.3)] cursor-pointer flex items-center gap-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
224
|
+
>
|
|
225
|
+
{layoutStatus === 'saving' ? (
|
|
226
|
+
<Loader2 className="w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0 animate-spin" />
|
|
227
|
+
) : (
|
|
228
|
+
<Save className="w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" />
|
|
229
|
+
)}
|
|
230
|
+
<span className="flex-1 font-normal">{layoutStatus === 'saving' ? 'Saving...' : 'Save Layout'}</span>
|
|
231
|
+
</DropdownMenu.Item>
|
|
232
|
+
<DropdownMenu.Item
|
|
233
|
+
onClick={handleResetLayout}
|
|
234
|
+
disabled={layoutStatus !== 'idle'}
|
|
235
|
+
className="px-3 py-2 text-xs text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-accent-subtle)/0.3)] cursor-pointer flex items-center gap-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
236
|
+
>
|
|
237
|
+
{layoutStatus === 'resetting' ? (
|
|
238
|
+
<Loader2 className="w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0 animate-spin" />
|
|
239
|
+
) : (
|
|
240
|
+
<RotateCcw className="w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" />
|
|
241
|
+
)}
|
|
242
|
+
<span className="flex-1 font-normal">{layoutStatus === 'resetting' ? 'Resetting...' : 'Reset Layout'}</span>
|
|
243
|
+
</DropdownMenu.Item>
|
|
244
|
+
</DropdownMenu.SubContent>
|
|
245
|
+
</DropdownMenu.Portal>
|
|
246
|
+
</DropdownMenu.Sub>
|
|
247
|
+
)}
|
|
248
|
+
|
|
160
249
|
{/* Ask AI */}
|
|
161
250
|
{isChatEnabled && (
|
|
162
251
|
<>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { getLayoutFilePath } from '@utils/node-graphs/layout-persistence';
|
|
4
|
+
|
|
5
|
+
export const POST: APIRoute = async ({ request }) => {
|
|
6
|
+
try {
|
|
7
|
+
const body = await request.json();
|
|
8
|
+
const { resourceKey } = body;
|
|
9
|
+
|
|
10
|
+
// Validate input
|
|
11
|
+
if (!resourceKey) {
|
|
12
|
+
return new Response(JSON.stringify({ error: 'Missing required field: resourceKey' }), {
|
|
13
|
+
status: 400,
|
|
14
|
+
headers: { 'Content-Type': 'application/json' },
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const filePath = getLayoutFilePath(resourceKey);
|
|
19
|
+
|
|
20
|
+
// Check if file exists
|
|
21
|
+
try {
|
|
22
|
+
await fs.promises.access(filePath);
|
|
23
|
+
} catch {
|
|
24
|
+
// File doesn't exist, nothing to delete
|
|
25
|
+
return new Response(JSON.stringify({ success: true, message: 'No saved layout to reset' }), {
|
|
26
|
+
headers: { 'Content-Type': 'application/json' },
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Delete the layout file
|
|
31
|
+
await fs.promises.unlink(filePath);
|
|
32
|
+
|
|
33
|
+
return new Response(JSON.stringify({ success: true, message: 'Layout reset successfully' }), {
|
|
34
|
+
headers: { 'Content-Type': 'application/json' },
|
|
35
|
+
});
|
|
36
|
+
} catch (error) {
|
|
37
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
38
|
+
return new Response(JSON.stringify({ error: `Failed to reset layout: ${message}` }), {
|
|
39
|
+
status: 500,
|
|
40
|
+
headers: { 'Content-Type': 'application/json' },
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const prerender = false;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { getLayoutFilePath, type SavedLayout } from '@utils/node-graphs/layout-persistence';
|
|
5
|
+
|
|
6
|
+
export const POST: APIRoute = async ({ request }) => {
|
|
7
|
+
try {
|
|
8
|
+
const body = await request.json();
|
|
9
|
+
const { resourceKey, positions } = body;
|
|
10
|
+
|
|
11
|
+
// Validate input
|
|
12
|
+
if (!resourceKey || !positions) {
|
|
13
|
+
return new Response(JSON.stringify({ error: 'Missing required fields: resourceKey and positions' }), {
|
|
14
|
+
status: 400,
|
|
15
|
+
headers: { 'Content-Type': 'application/json' },
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Validate resourceKey doesn't contain path traversal
|
|
20
|
+
if (resourceKey.includes('..') || resourceKey.includes('~')) {
|
|
21
|
+
return new Response(JSON.stringify({ error: 'Invalid resourceKey' }), {
|
|
22
|
+
status: 400,
|
|
23
|
+
headers: { 'Content-Type': 'application/json' },
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Build file path from resource key using shared utility
|
|
28
|
+
const filePath = getLayoutFilePath(resourceKey);
|
|
29
|
+
|
|
30
|
+
// Create directory structure if needed
|
|
31
|
+
const fileDir = path.dirname(filePath);
|
|
32
|
+
await fs.promises.mkdir(fileDir, { recursive: true });
|
|
33
|
+
|
|
34
|
+
// Build layout data
|
|
35
|
+
const layoutData: SavedLayout = {
|
|
36
|
+
version: 1,
|
|
37
|
+
savedAt: new Date().toISOString(),
|
|
38
|
+
resourceKey,
|
|
39
|
+
positions,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Write file with pretty formatting
|
|
43
|
+
await fs.promises.writeFile(filePath, JSON.stringify(layoutData, null, 2));
|
|
44
|
+
|
|
45
|
+
return new Response(JSON.stringify({ success: true, filePath }), {
|
|
46
|
+
headers: { 'Content-Type': 'application/json' },
|
|
47
|
+
});
|
|
48
|
+
} catch (error) {
|
|
49
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
50
|
+
return new Response(JSON.stringify({ error: `Failed to save layout: ${message}` }), {
|
|
51
|
+
status: 500,
|
|
52
|
+
headers: { 'Content-Type': 'application/json' },
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const prerender = false;
|
|
@@ -2,18 +2,20 @@ import type { APIRoute } from 'astro';
|
|
|
2
2
|
import utils from '@eventcatalog/sdk';
|
|
3
3
|
import config from '@config';
|
|
4
4
|
|
|
5
|
+
const isFullCatalogAPIEnabled = config.api?.fullCatalogAPIEnabled ?? true;
|
|
6
|
+
|
|
5
7
|
/**
|
|
6
|
-
* Route
|
|
8
|
+
* Route that dumps the whole catalog as JSON (without markdown)
|
|
7
9
|
* Experimental API
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
+
*
|
|
11
|
+
* Can be disabled via eventcatalog.config.js:
|
|
12
|
+
* api: { fullCatalogAPIEnabled: false }
|
|
10
13
|
*/
|
|
11
|
-
export const GET: APIRoute = async (
|
|
12
|
-
const isFullCatalogAPIEnabled = config.api?.fullCatalogAPIEnabled ?? true;
|
|
13
|
-
|
|
14
|
+
export const GET: APIRoute = async () => {
|
|
14
15
|
if (!isFullCatalogAPIEnabled) {
|
|
15
16
|
return new Response(JSON.stringify({ error: 'Full catalog API is not enabled' }), {
|
|
16
17
|
status: 404,
|
|
18
|
+
headers: { 'Content-Type': 'application/json' },
|
|
17
19
|
});
|
|
18
20
|
}
|
|
19
21
|
|
|
@@ -26,3 +28,7 @@ export const GET: APIRoute = async ({ params, request }) => {
|
|
|
26
28
|
},
|
|
27
29
|
});
|
|
28
30
|
};
|
|
31
|
+
|
|
32
|
+
// Only prerender if the API is enabled - this avoids loading all catalog data during build
|
|
33
|
+
// when the feature is disabled, saving memory for large catalogs
|
|
34
|
+
export const prerender = isFullCatalogAPIEnabled;
|
|
@@ -71,3 +71,5 @@ export const isCustomStylesEnabled = () => {
|
|
|
71
71
|
export const isDiagramComparisonEnabled = () => isEventCatalogScaleEnabled();
|
|
72
72
|
|
|
73
73
|
export const isEventCatalogMCPEnabled = () => isEventCatalogScaleEnabled() && isSSR();
|
|
74
|
+
|
|
75
|
+
export const isDevMode = () => process.env.EVENTCATALOG_DEV_MODE === 'true';
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export interface SavedPosition {
|
|
5
|
+
x: number;
|
|
6
|
+
y: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface SavedLayout {
|
|
10
|
+
version: number;
|
|
11
|
+
savedAt: string;
|
|
12
|
+
resourceKey: string;
|
|
13
|
+
positions: Record<string, SavedPosition>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Builds a resource key for layout persistence from collection, id, and version.
|
|
18
|
+
* e.g., buildResourceKey('services', 'OrderService', '1.0.0') -> 'services/OrderService/1.0.0'
|
|
19
|
+
*/
|
|
20
|
+
export function buildResourceKey(collection: string, id: string, version?: string): string {
|
|
21
|
+
if (version) {
|
|
22
|
+
return `${collection}/${id}/${version}`;
|
|
23
|
+
}
|
|
24
|
+
return `${collection}/${id}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Converts a resource key to a file path for the saved layout
|
|
29
|
+
* e.g., services/OrderService/1.0.0 -> _data/visualizer-layouts/services/OrderService/1.0.0.json
|
|
30
|
+
* Uses path.join with spread segments for cross-platform compatibility (Windows/Mac/Linux)
|
|
31
|
+
*/
|
|
32
|
+
export function getLayoutFilePath(resourceKey: string): string {
|
|
33
|
+
const PROJECT_DIR = process.env.PROJECT_DIR || process.cwd();
|
|
34
|
+
const cleanKey = resourceKey.replace(/^\//, '').replace(/\/$/, '');
|
|
35
|
+
// Split path segments and rejoin with platform-specific separator
|
|
36
|
+
const segments = cleanKey.split('/').filter(Boolean);
|
|
37
|
+
const fileName = `${segments.pop()}.json`;
|
|
38
|
+
return path.join(PROJECT_DIR, '_data', 'visualizer-layouts', ...segments, fileName);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Loads saved layout from disk if it exists
|
|
43
|
+
*/
|
|
44
|
+
export async function loadSavedLayout(resourceKey: string): Promise<SavedLayout | null> {
|
|
45
|
+
const filePath = getLayoutFilePath(resourceKey);
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const content = await fs.promises.readFile(filePath, 'utf-8');
|
|
49
|
+
return JSON.parse(content) as SavedLayout;
|
|
50
|
+
} catch {
|
|
51
|
+
// File doesn't exist or parse error - return null
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Applies saved positions to nodes. Nodes with saved positions get them applied,
|
|
58
|
+
* nodes without saved positions keep their Dagre-calculated positions.
|
|
59
|
+
*/
|
|
60
|
+
export function applyLayoutToNodes<T extends { id: string; position: { x: number; y: number } }>(
|
|
61
|
+
nodes: T[],
|
|
62
|
+
savedLayout: SavedLayout | null
|
|
63
|
+
): T[];
|
|
64
|
+
export function applyLayoutToNodes(nodes: any[], savedLayout: SavedLayout | null): any[] {
|
|
65
|
+
if (!savedLayout) {
|
|
66
|
+
return nodes; // No saved layout, use original Dagre positions
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return nodes.map((node) => {
|
|
70
|
+
const savedPosition = savedLayout.positions[node.id];
|
|
71
|
+
if (savedPosition) {
|
|
72
|
+
// Use saved position
|
|
73
|
+
return {
|
|
74
|
+
...node,
|
|
75
|
+
position: { x: savedPosition.x, y: savedPosition.y },
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
// Keep Dagre-calculated position for new/unmatched nodes
|
|
79
|
+
return node;
|
|
80
|
+
});
|
|
81
|
+
}
|