@eventcatalog/core 4.6.3 → 4.7.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-LB5VXS6X.js → chunk-3HMY77MN.js} +1 -1
- package/dist/{chunk-4BBXOL6K.js → chunk-CZ6C3FBV.js} +1 -1
- package/dist/{chunk-FZJDCQZE.js → chunk-HRSTURLY.js} +1 -1
- package/dist/{chunk-54KFOI6Z.js → chunk-WJVRBHVC.js} +3 -3
- package/dist/{chunk-PWY4SP6L.js → chunk-ZU5WMIH4.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 +6 -6
- 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/src/components/MDX/NodeGraph/NodeGraph.astro +2 -0
- package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +8 -85
- package/eventcatalog/src/components/SideNav/NestedSideBar/utils.spec.ts +19 -1
- package/eventcatalog/src/components/SideNav/NestedSideBar/utils.ts +80 -0
- package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/_index.data.ts +1 -0
- package/eventcatalog/src/stores/sidebar-store/state.ts +14 -1
- package/eventcatalog/src/utils/node-graphs/channel-node-graph.ts +395 -3
- package/package.json +2 -2
|
@@ -140,7 +140,7 @@ var verifyRequiredFieldsAreInCatalogConfigFile = async (projectDirectory) => {
|
|
|
140
140
|
var import_os = __toESM(require("os"), 1);
|
|
141
141
|
|
|
142
142
|
// package.json
|
|
143
|
-
var version = "4.
|
|
143
|
+
var version = "4.7.0";
|
|
144
144
|
|
|
145
145
|
// src/constants.ts
|
|
146
146
|
var VERSION = version;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
log_build_default
|
|
3
|
-
} from "../chunk-
|
|
4
|
-
import "../chunk-PWY4SP6L.js";
|
|
3
|
+
} from "../chunk-WJVRBHVC.js";
|
|
5
4
|
import "../chunk-K2XIENVT.js";
|
|
6
|
-
import "../chunk-
|
|
5
|
+
import "../chunk-ZU5WMIH4.js";
|
|
6
|
+
import "../chunk-3HMY77MN.js";
|
|
7
7
|
import "../chunk-6QENHZZP.js";
|
|
8
8
|
export {
|
|
9
9
|
log_build_default as default
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
raiseEvent
|
|
3
|
-
} from "./chunk-PWY4SP6L.js";
|
|
4
1
|
import {
|
|
5
2
|
countResources,
|
|
6
3
|
serializeCounts
|
|
7
4
|
} from "./chunk-K2XIENVT.js";
|
|
5
|
+
import {
|
|
6
|
+
raiseEvent
|
|
7
|
+
} from "./chunk-ZU5WMIH4.js";
|
|
8
8
|
import {
|
|
9
9
|
getEventCatalogConfigFile,
|
|
10
10
|
verifyRequiredFieldsAreInCatalogConfigFile
|
package/dist/constants.cjs
CHANGED
package/dist/constants.js
CHANGED
package/dist/eventcatalog.cjs
CHANGED
|
@@ -144,7 +144,7 @@ var verifyRequiredFieldsAreInCatalogConfigFile = async (projectDirectory) => {
|
|
|
144
144
|
var import_picocolors = __toESM(require("picocolors"), 1);
|
|
145
145
|
|
|
146
146
|
// package.json
|
|
147
|
-
var version = "4.
|
|
147
|
+
var version = "4.7.0";
|
|
148
148
|
|
|
149
149
|
// src/constants.ts
|
|
150
150
|
var VERSION = version;
|
package/dist/eventcatalog.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
log_build_default
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-WJVRBHVC.js";
|
|
4
|
+
import "./chunk-K2XIENVT.js";
|
|
4
5
|
import {
|
|
5
6
|
FederationConflictError,
|
|
6
7
|
federateCatalog
|
|
@@ -20,10 +21,10 @@ import {
|
|
|
20
21
|
} from "./chunk-B7HCX5HM.js";
|
|
21
22
|
import {
|
|
22
23
|
generate
|
|
23
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-CZ6C3FBV.js";
|
|
24
25
|
import {
|
|
25
26
|
logger
|
|
26
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-HRSTURLY.js";
|
|
27
28
|
import {
|
|
28
29
|
resolve_catalog_dependencies_default
|
|
29
30
|
} from "./chunk-LHR4G2UO.js";
|
|
@@ -33,8 +34,7 @@ import {
|
|
|
33
34
|
import {
|
|
34
35
|
watch
|
|
35
36
|
} from "./chunk-CAJUIMDJ.js";
|
|
36
|
-
import "./chunk-
|
|
37
|
-
import "./chunk-K2XIENVT.js";
|
|
37
|
+
import "./chunk-ZU5WMIH4.js";
|
|
38
38
|
import {
|
|
39
39
|
createAstroDevLineFilter,
|
|
40
40
|
createAstroLineFilter
|
|
@@ -45,7 +45,7 @@ import {
|
|
|
45
45
|
import "./chunk-W5JQON7Z.js";
|
|
46
46
|
import {
|
|
47
47
|
VERSION
|
|
48
|
-
} from "./chunk-
|
|
48
|
+
} from "./chunk-3HMY77MN.js";
|
|
49
49
|
import {
|
|
50
50
|
linkCoreNodeModules,
|
|
51
51
|
resolveInstalledCoreNodeModules
|
package/dist/generate.cjs
CHANGED
|
@@ -108,7 +108,7 @@ var getEventCatalogConfigFile = async (projectDirectory) => {
|
|
|
108
108
|
var import_picocolors = __toESM(require("picocolors"), 1);
|
|
109
109
|
|
|
110
110
|
// package.json
|
|
111
|
-
var version = "4.
|
|
111
|
+
var version = "4.7.0";
|
|
112
112
|
|
|
113
113
|
// src/constants.ts
|
|
114
114
|
var VERSION = version;
|
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-CZ6C3FBV.js";
|
|
4
|
+
import "./chunk-HRSTURLY.js";
|
|
5
|
+
import "./chunk-3HMY77MN.js";
|
|
6
6
|
import "./chunk-6QENHZZP.js";
|
|
7
7
|
export {
|
|
8
8
|
generate
|
package/dist/utils/cli-logger.js
CHANGED
|
@@ -22,6 +22,7 @@ import { buildUrl } from '@utils/url-builder';
|
|
|
22
22
|
import { getVersionFromCollection } from '@utils/collections/versions';
|
|
23
23
|
import { pageDataLoader } from '@utils/page-loaders/page-data-loader';
|
|
24
24
|
import { getNodesAndEdges as getNodesAndEdgesForContainer } from '@utils/node-graphs/container-node-graph';
|
|
25
|
+
import { getNodesAndEdges as getNodesAndEdgesForChannel } from '@utils/node-graphs/channel-node-graph';
|
|
25
26
|
import config from '@config';
|
|
26
27
|
import { isEventCatalogChatEnabled, isDevMode } from '@utils/feature';
|
|
27
28
|
import { loadSavedLayout, applyLayoutToNodes, buildResourceKey } from '@utils/node-graphs/layout-persistence';
|
|
@@ -79,6 +80,7 @@ const getNodesAndEdgesFunctions = {
|
|
|
79
80
|
domains: getNodesAndEdgesForDomain,
|
|
80
81
|
flows: getNodesAndEdgesForFlows,
|
|
81
82
|
containers: getNodesAndEdgesForContainer,
|
|
83
|
+
channels: getNodesAndEdgesForChannel,
|
|
82
84
|
'data-products': getNodesAndEdgesForDataProduct,
|
|
83
85
|
systems: getNodesAndEdgesForSystem,
|
|
84
86
|
};
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
removeFavorite as removeFavoriteAction,
|
|
14
14
|
type FavoriteItem,
|
|
15
15
|
} from '@stores/favorites-store';
|
|
16
|
-
import { canCollapseGroup, getBadgeClasses, getGroupLabel, isGroupCollapsed } from './utils';
|
|
16
|
+
import { canCollapseGroup, findNodeKeyByUrl, getBadgeClasses, getGroupLabel, isGroupCollapsed } from './utils';
|
|
17
17
|
import { resolveIconUrl } from '@utils/icon';
|
|
18
18
|
|
|
19
19
|
const cn = (...classes: (string | false | undefined)[]) => classes.filter(Boolean).join(' ');
|
|
@@ -201,86 +201,9 @@ export default function NestedSideBar() {
|
|
|
201
201
|
/**
|
|
202
202
|
* Find a node key by matching URL patterns
|
|
203
203
|
*/
|
|
204
|
-
const
|
|
204
|
+
const findNodeKeyForUrl = useCallback(
|
|
205
205
|
(url: string): string | null => {
|
|
206
|
-
|
|
207
|
-
const normalizedUrl = stripBasePath(url);
|
|
208
|
-
|
|
209
|
-
// URL patterns to match resources with version
|
|
210
|
-
const urlPatternsWithVersion = [
|
|
211
|
-
// Domains
|
|
212
|
-
{ pattern: /^\/docs\/domains\/([^/]+)\/([^/]+)/, type: 'domain' },
|
|
213
|
-
{ pattern: /^\/visualiser\/domains\/([^/]+)\/([^/]+)/, type: 'domain' },
|
|
214
|
-
{ pattern: /^\/architecture\/domains\/([^/]+)\/([^/]+)/, type: 'domain' },
|
|
215
|
-
// Systems (key prefix is `system`, url segment is `systems`).
|
|
216
|
-
// The visualiser pattern also matches the System Diagram
|
|
217
|
-
// (/visualiser/systems/:id/:version/context) since it is not end-anchored.
|
|
218
|
-
{ pattern: /^\/docs\/systems\/([^/]+)\/([^/]+)/, type: 'system' },
|
|
219
|
-
{ pattern: /^\/visualiser\/systems\/([^/]+)\/([^/]+)/, type: 'system' },
|
|
220
|
-
{ pattern: /^\/architecture\/systems\/([^/]+)\/([^/]+)/, type: 'system' },
|
|
221
|
-
// Agents
|
|
222
|
-
{ pattern: /^\/docs\/agents\/([^/]+)\/([^/]+)/, type: 'agent' },
|
|
223
|
-
{ pattern: /^\/visualiser\/agents\/([^/]+)\/([^/]+)/, type: 'agent' },
|
|
224
|
-
// Decision Records
|
|
225
|
-
{ pattern: /^\/docs\/adrs\/([^/]+)\/([^/]+)/, type: 'adr' },
|
|
226
|
-
// Services
|
|
227
|
-
{ pattern: /^\/docs\/services\/([^/]+)\/([^/]+)/, type: 'service' },
|
|
228
|
-
{ pattern: /^\/architecture\/services\/([^/]+)\/([^/]+)/, type: 'service' },
|
|
229
|
-
{ pattern: /^\/visualiser\/services\/([^/]+)\/([^/]+)/, type: 'service' },
|
|
230
|
-
// Messages (events, commands, queries) - note: keys use singular form
|
|
231
|
-
{ pattern: /^\/docs\/events\/([^/]+)\/([^/]+)/, type: 'event' },
|
|
232
|
-
{ pattern: /^\/docs\/commands\/([^/]+)\/([^/]+)/, type: 'command' },
|
|
233
|
-
{ pattern: /^\/docs\/queries\/([^/]+)\/([^/]+)/, type: 'query' },
|
|
234
|
-
{ pattern: /^\/visualiser\/messages\/([^/]+)\/([^/]+)/, type: 'message' },
|
|
235
|
-
{ pattern: /^\/visualiser\/events\/([^/]+)\/([^/]+)/, type: 'event' },
|
|
236
|
-
{ pattern: /^\/visualiser\/commands\/([^/]+)\/([^/]+)/, type: 'command' },
|
|
237
|
-
{ pattern: /^\/visualiser\/queries\/([^/]+)\/([^/]+)/, type: 'query' },
|
|
238
|
-
// Containers
|
|
239
|
-
{ pattern: /^\/docs\/containers\/([^/]+)\/([^/]+)/, type: 'container' },
|
|
240
|
-
{ pattern: /^\/visualiser\/containers\/([^/]+)\/([^/]+)/, type: 'container' },
|
|
241
|
-
// Flows
|
|
242
|
-
{ pattern: /^\/docs\/flows\/([^/]+)\/([^/]+)/, type: 'flow' },
|
|
243
|
-
{ pattern: /^\/visualiser\/flows\/([^/]+)\/([^/]+)/, type: 'flow' },
|
|
244
|
-
// Data Products
|
|
245
|
-
{ pattern: /^\/docs\/data-products\/([^/]+)\/([^/]+)/, type: 'data-product' },
|
|
246
|
-
{ pattern: /^\/visualiser\/data-products\/([^/]+)\/([^/]+)/, type: 'data-product' },
|
|
247
|
-
// Entities
|
|
248
|
-
{ pattern: /^\/docs\/entities\/([^/]+)\/([^/]+)/, type: 'entity' },
|
|
249
|
-
];
|
|
250
|
-
|
|
251
|
-
// URL patterns without version (language pages, etc)
|
|
252
|
-
const urlPatternsWithoutVersion = [{ pattern: /^\/docs\/domains\/([^/]+)\/language/, type: 'domain' }];
|
|
253
|
-
|
|
254
|
-
// First try to match patterns with version
|
|
255
|
-
for (const { pattern, type } of urlPatternsWithVersion) {
|
|
256
|
-
const match = normalizedUrl.match(pattern);
|
|
257
|
-
if (match) {
|
|
258
|
-
const id = match[1];
|
|
259
|
-
const version = match[2];
|
|
260
|
-
|
|
261
|
-
// First try with version
|
|
262
|
-
const keyWithVersion = `${type}:${id}:${version}`;
|
|
263
|
-
if (nodes[keyWithVersion]) {
|
|
264
|
-
return keyWithVersion;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// Fallback to lookup without version (for latest)
|
|
268
|
-
const foundNodeKey = nodeLookup.get(`${type}:${id}`);
|
|
269
|
-
if (foundNodeKey) return foundNodeKey;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// Then try patterns without version
|
|
274
|
-
for (const { pattern, type } of urlPatternsWithoutVersion) {
|
|
275
|
-
const match = normalizedUrl.match(pattern);
|
|
276
|
-
if (match) {
|
|
277
|
-
const id = match[1];
|
|
278
|
-
const foundNodeKey = nodeLookup.get(`${type}:${id}`);
|
|
279
|
-
if (foundNodeKey) return foundNodeKey;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
return null;
|
|
206
|
+
return findNodeKeyByUrl(stripBasePath(url), nodes, nodeLookup);
|
|
284
207
|
},
|
|
285
208
|
[nodeLookup, nodes]
|
|
286
209
|
);
|
|
@@ -337,7 +260,7 @@ export default function NestedSideBar() {
|
|
|
337
260
|
*/
|
|
338
261
|
const findAndNavigateToUrl = useCallback(
|
|
339
262
|
(url: string) => {
|
|
340
|
-
const foundNodeKey =
|
|
263
|
+
const foundNodeKey = findNodeKeyForUrl(url);
|
|
341
264
|
|
|
342
265
|
if (foundNodeKey) {
|
|
343
266
|
setNavigationStack((currentStack) => {
|
|
@@ -373,7 +296,7 @@ export default function NestedSideBar() {
|
|
|
373
296
|
}
|
|
374
297
|
return false;
|
|
375
298
|
},
|
|
376
|
-
[
|
|
299
|
+
[findNodeKeyForUrl, tryConnectStack, resolveRef, roots]
|
|
377
300
|
);
|
|
378
301
|
|
|
379
302
|
/**
|
|
@@ -393,7 +316,7 @@ export default function NestedSideBar() {
|
|
|
393
316
|
}
|
|
394
317
|
|
|
395
318
|
const savedState = loadState();
|
|
396
|
-
const targetKey =
|
|
319
|
+
const targetKey = findNodeKeyForUrl(currentUrl);
|
|
397
320
|
|
|
398
321
|
let finalStack: NavigationLevel[] | null = null;
|
|
399
322
|
|
|
@@ -432,7 +355,7 @@ export default function NestedSideBar() {
|
|
|
432
355
|
}
|
|
433
356
|
|
|
434
357
|
setIsInitialized(true);
|
|
435
|
-
}, [data, roots, isInitialized, buildStackFromPath,
|
|
358
|
+
}, [data, roots, isInitialized, buildStackFromPath, findNodeKeyForUrl, tryConnectStack, resolveRef]);
|
|
436
359
|
|
|
437
360
|
/**
|
|
438
361
|
* Save state whenever navigation changes
|
|
@@ -510,7 +433,7 @@ export default function NestedSideBar() {
|
|
|
510
433
|
return stripBasePath(left).replace(/\/$/, '') === stripBasePath(right).replace(/\/$/, '');
|
|
511
434
|
}, []);
|
|
512
435
|
|
|
513
|
-
const activeNodeKey = useMemo(() =>
|
|
436
|
+
const activeNodeKey = useMemo(() => findNodeKeyForUrl(currentPath), [currentPath, findNodeKeyForUrl]);
|
|
514
437
|
|
|
515
438
|
// Show loading state if no data yet
|
|
516
439
|
if (!data || roots.length === 0) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { canCollapseGroup, getGroupLabel, isGroupCollapsed } from './utils';
|
|
2
|
+
import { canCollapseGroup, findNodeKeyByUrl, getGroupLabel, isGroupCollapsed } from './utils';
|
|
3
3
|
|
|
4
4
|
const preferences = (expanded: string[] = []) => ({
|
|
5
5
|
expanded: new Set(expanded),
|
|
@@ -41,3 +41,21 @@ describe('isGroupCollapsed', () => {
|
|
|
41
41
|
expect(isGroupCollapsed(true, 'outbound-messages', preferences(['outbound-messages']))).toBe(false);
|
|
42
42
|
});
|
|
43
43
|
});
|
|
44
|
+
|
|
45
|
+
describe('findNodeKeyByUrl', () => {
|
|
46
|
+
const channelKey = 'channel:product-events:1.0.0';
|
|
47
|
+
const nodes = { [channelKey]: {} };
|
|
48
|
+
const nodeLookup = new Map([['channel:product-events', channelKey]]);
|
|
49
|
+
|
|
50
|
+
it('selects the channel sidebar for channel documentation', () => {
|
|
51
|
+
expect(findNodeKeyByUrl('/docs/channels/product-events/1.0.0', nodes, nodeLookup)).toBe(channelKey);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('keeps the channel sidebar selected on the channel map', () => {
|
|
55
|
+
expect(findNodeKeyByUrl('/visualiser/channels/product-events/1.0.0', nodes, nodeLookup)).toBe(channelKey);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('falls back to the available channel version', () => {
|
|
59
|
+
expect(findNodeKeyByUrl('/docs/channels/product-events/latest', nodes, nodeLookup)).toBe(channelKey);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -17,6 +17,86 @@ export const isGroupCollapsed = (canCollapse: boolean, groupId: string, preferen
|
|
|
17
17
|
return canCollapse && !preferences.expanded.has(groupId);
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Finds the sidebar node represented by a resource URL.
|
|
22
|
+
* The URL must have its configured base path removed before it is passed in.
|
|
23
|
+
*/
|
|
24
|
+
export const findNodeKeyByUrl = (
|
|
25
|
+
url: string,
|
|
26
|
+
nodes: Record<string, unknown>,
|
|
27
|
+
nodeLookup: ReadonlyMap<string, string>
|
|
28
|
+
): string | null => {
|
|
29
|
+
const urlPatternsWithVersion = [
|
|
30
|
+
// Domains
|
|
31
|
+
{ pattern: /^\/docs\/domains\/([^/]+)\/([^/]+)/, type: 'domain' },
|
|
32
|
+
{ pattern: /^\/visualiser\/domains\/([^/]+)\/([^/]+)/, type: 'domain' },
|
|
33
|
+
{ pattern: /^\/architecture\/domains\/([^/]+)\/([^/]+)/, type: 'domain' },
|
|
34
|
+
// Systems (key prefix is `system`, url segment is `systems`).
|
|
35
|
+
// The visualiser pattern also matches the System Diagram
|
|
36
|
+
// (/visualiser/systems/:id/:version/context) since it is not end-anchored.
|
|
37
|
+
{ pattern: /^\/docs\/systems\/([^/]+)\/([^/]+)/, type: 'system' },
|
|
38
|
+
{ pattern: /^\/visualiser\/systems\/([^/]+)\/([^/]+)/, type: 'system' },
|
|
39
|
+
{ pattern: /^\/architecture\/systems\/([^/]+)\/([^/]+)/, type: 'system' },
|
|
40
|
+
// Agents
|
|
41
|
+
{ pattern: /^\/docs\/agents\/([^/]+)\/([^/]+)/, type: 'agent' },
|
|
42
|
+
{ pattern: /^\/visualiser\/agents\/([^/]+)\/([^/]+)/, type: 'agent' },
|
|
43
|
+
// Decision Records
|
|
44
|
+
{ pattern: /^\/docs\/adrs\/([^/]+)\/([^/]+)/, type: 'adr' },
|
|
45
|
+
// Services
|
|
46
|
+
{ pattern: /^\/docs\/services\/([^/]+)\/([^/]+)/, type: 'service' },
|
|
47
|
+
{ pattern: /^\/architecture\/services\/([^/]+)\/([^/]+)/, type: 'service' },
|
|
48
|
+
{ pattern: /^\/visualiser\/services\/([^/]+)\/([^/]+)/, type: 'service' },
|
|
49
|
+
// Messages (events, commands, queries) - note: keys use singular form
|
|
50
|
+
{ pattern: /^\/docs\/events\/([^/]+)\/([^/]+)/, type: 'event' },
|
|
51
|
+
{ pattern: /^\/docs\/commands\/([^/]+)\/([^/]+)/, type: 'command' },
|
|
52
|
+
{ pattern: /^\/docs\/queries\/([^/]+)\/([^/]+)/, type: 'query' },
|
|
53
|
+
{ pattern: /^\/visualiser\/messages\/([^/]+)\/([^/]+)/, type: 'message' },
|
|
54
|
+
{ pattern: /^\/visualiser\/events\/([^/]+)\/([^/]+)/, type: 'event' },
|
|
55
|
+
{ pattern: /^\/visualiser\/commands\/([^/]+)\/([^/]+)/, type: 'command' },
|
|
56
|
+
{ pattern: /^\/visualiser\/queries\/([^/]+)\/([^/]+)/, type: 'query' },
|
|
57
|
+
// Channels
|
|
58
|
+
{ pattern: /^\/docs\/channels\/([^/]+)\/([^/]+)/, type: 'channel' },
|
|
59
|
+
{ pattern: /^\/visualiser\/channels\/([^/]+)\/([^/]+)/, type: 'channel' },
|
|
60
|
+
// Containers
|
|
61
|
+
{ pattern: /^\/docs\/containers\/([^/]+)\/([^/]+)/, type: 'container' },
|
|
62
|
+
{ pattern: /^\/visualiser\/containers\/([^/]+)\/([^/]+)/, type: 'container' },
|
|
63
|
+
// Flows
|
|
64
|
+
{ pattern: /^\/docs\/flows\/([^/]+)\/([^/]+)/, type: 'flow' },
|
|
65
|
+
{ pattern: /^\/visualiser\/flows\/([^/]+)\/([^/]+)/, type: 'flow' },
|
|
66
|
+
// Data Products
|
|
67
|
+
{ pattern: /^\/docs\/data-products\/([^/]+)\/([^/]+)/, type: 'data-product' },
|
|
68
|
+
{ pattern: /^\/visualiser\/data-products\/([^/]+)\/([^/]+)/, type: 'data-product' },
|
|
69
|
+
// Entities
|
|
70
|
+
{ pattern: /^\/docs\/entities\/([^/]+)\/([^/]+)/, type: 'entity' },
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
const urlPatternsWithoutVersion = [{ pattern: /^\/docs\/domains\/([^/]+)\/language/, type: 'domain' }];
|
|
74
|
+
|
|
75
|
+
for (const { pattern, type } of urlPatternsWithVersion) {
|
|
76
|
+
const match = url.match(pattern);
|
|
77
|
+
if (match) {
|
|
78
|
+
const id = match[1];
|
|
79
|
+
const version = match[2];
|
|
80
|
+
const keyWithVersion = `${type}:${id}:${version}`;
|
|
81
|
+
|
|
82
|
+
if (nodes[keyWithVersion]) return keyWithVersion;
|
|
83
|
+
|
|
84
|
+
const foundNodeKey = nodeLookup.get(`${type}:${id}`);
|
|
85
|
+
if (foundNodeKey) return foundNodeKey;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for (const { pattern, type } of urlPatternsWithoutVersion) {
|
|
90
|
+
const match = url.match(pattern);
|
|
91
|
+
if (match) {
|
|
92
|
+
const foundNodeKey = nodeLookup.get(`${type}:${match[1]}`);
|
|
93
|
+
if (foundNodeKey) return foundNodeKey;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return null;
|
|
98
|
+
};
|
|
99
|
+
|
|
20
100
|
/**
|
|
21
101
|
* Returns Tailwind classes for badge styling based on badge type.
|
|
22
102
|
* Uses CSS variables from theme.css for proper theming support.
|
|
@@ -764,6 +764,8 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
|
|
|
764
764
|
{} as Record<string, NavNode>
|
|
765
765
|
);
|
|
766
766
|
|
|
767
|
+
const visualiserEnabled = config?.visualiser?.enabled !== false;
|
|
768
|
+
|
|
767
769
|
const channelNodes = channels.reduce(
|
|
768
770
|
(acc, channel) => {
|
|
769
771
|
const versionedKey = `channel:${channel.data.id}:${channel.data.version}`;
|
|
@@ -796,6 +798,18 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
|
|
|
796
798
|
].filter(Boolean) as { title: string; href: string }[]
|
|
797
799
|
),
|
|
798
800
|
docsSection,
|
|
801
|
+
visualiserEnabled && {
|
|
802
|
+
type: 'group',
|
|
803
|
+
title: 'Architecture',
|
|
804
|
+
icon: 'Workflow',
|
|
805
|
+
pages: [
|
|
806
|
+
{
|
|
807
|
+
type: 'item',
|
|
808
|
+
title: 'Map',
|
|
809
|
+
href: buildUrl(`/visualiser/${channel.collection}/${channel.data.id}/${channel.data.version}`),
|
|
810
|
+
},
|
|
811
|
+
],
|
|
812
|
+
},
|
|
799
813
|
].filter(Boolean) as ChildRef[],
|
|
800
814
|
},
|
|
801
815
|
channel,
|
|
@@ -827,7 +841,6 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
|
|
|
827
841
|
{} as Record<string, NavNode>
|
|
828
842
|
);
|
|
829
843
|
|
|
830
|
-
const visualiserEnabled = config?.visualiser?.enabled !== false;
|
|
831
844
|
const rootDomainsNodes: Record<string, NavNode> = {};
|
|
832
845
|
|
|
833
846
|
if (rootDomains.length > 0) {
|
|
@@ -1,6 +1,27 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import
|
|
1
|
+
import { getCollection, type CollectionEntry } from 'astro:content';
|
|
2
|
+
import dagre from 'dagre';
|
|
3
|
+
import {
|
|
4
|
+
buildContextMenuForAgent,
|
|
5
|
+
buildContextMenuForMessage,
|
|
6
|
+
buildContextMenuForResource,
|
|
7
|
+
buildContextMenuForService,
|
|
8
|
+
calculatedNodes,
|
|
9
|
+
createDagreGraph,
|
|
10
|
+
createEdge,
|
|
11
|
+
createNode,
|
|
12
|
+
generatedIdForEdge,
|
|
13
|
+
generateIdForNode,
|
|
14
|
+
getColorFromString,
|
|
15
|
+
getEdgeLabelForMessageAsSource,
|
|
16
|
+
getEdgeLabelForServiceAsTarget,
|
|
17
|
+
getOperationFields,
|
|
18
|
+
DEFAULT_NODE_WIDTH,
|
|
19
|
+
DEFAULT_NODE_HEIGHT,
|
|
20
|
+
} from './utils/utils';
|
|
21
|
+
import { createVersionedMap, findInMap } from '@utils/collections/util';
|
|
22
|
+
import { getChannelChain, getChannels } from '@utils/collections/channels';
|
|
3
23
|
import { type Node, type Edge } from '@xyflow/react';
|
|
24
|
+
import type { CollectionMessageTypes } from '@types';
|
|
4
25
|
|
|
5
26
|
interface CollectionItem {
|
|
6
27
|
collection: string;
|
|
@@ -27,7 +48,15 @@ export const getNodesAndEdgesForChannelChain = ({
|
|
|
27
48
|
createNode({
|
|
28
49
|
id: generateIdForNode(channel),
|
|
29
50
|
type: channel.collection,
|
|
30
|
-
data: {
|
|
51
|
+
data: {
|
|
52
|
+
mode,
|
|
53
|
+
channel: { ...channel.data, ...channel },
|
|
54
|
+
contextMenu: buildContextMenuForResource({
|
|
55
|
+
collection: 'channels',
|
|
56
|
+
id: channel.data.id,
|
|
57
|
+
version: channel.data.version,
|
|
58
|
+
}),
|
|
59
|
+
},
|
|
31
60
|
position: { x: 0, y: 0 },
|
|
32
61
|
})
|
|
33
62
|
);
|
|
@@ -73,3 +102,366 @@ export const getNodesAndEdgesForChannelChain = ({
|
|
|
73
102
|
|
|
74
103
|
return { nodes, edges };
|
|
75
104
|
};
|
|
105
|
+
|
|
106
|
+
type MessageProducerOrConsumer = CollectionEntry<'services'> | CollectionEntry<'agents'>;
|
|
107
|
+
type ChannelPointer = { id: string; version?: string };
|
|
108
|
+
type ChannelRoute = CollectionEntry<'channels'>[];
|
|
109
|
+
type RoutedResource = {
|
|
110
|
+
resource: MessageProducerOrConsumer;
|
|
111
|
+
channelRoutes: ChannelRoute[];
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
type DagreGraph = any;
|
|
115
|
+
|
|
116
|
+
interface Props {
|
|
117
|
+
id: string;
|
|
118
|
+
version: string;
|
|
119
|
+
defaultFlow?: DagreGraph;
|
|
120
|
+
mode?: 'simple' | 'full';
|
|
121
|
+
layout?: boolean;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Builds the graph for a single channel: everything that publishes into it on the left,
|
|
126
|
+
* everything that consumes out of it on the right, and any channels it routes to (or is
|
|
127
|
+
* routed from) either side of it.
|
|
128
|
+
*/
|
|
129
|
+
export const getNodesAndEdges = async ({ id, version, defaultFlow, mode = 'simple', layout = true }: Props) => {
|
|
130
|
+
const flow = defaultFlow || createDagreGraph({ ranksep: 300, nodesep: 50 });
|
|
131
|
+
const nodes: any[] = [];
|
|
132
|
+
const edges: any[] = [];
|
|
133
|
+
|
|
134
|
+
const [channels, services, agents, events, commands, queries] = await Promise.all([
|
|
135
|
+
getChannels(),
|
|
136
|
+
getCollection('services'),
|
|
137
|
+
getCollection('agents'),
|
|
138
|
+
getCollection('events'),
|
|
139
|
+
getCollection('commands'),
|
|
140
|
+
getCollection('queries'),
|
|
141
|
+
]);
|
|
142
|
+
|
|
143
|
+
const channel = channels.find((c) => c.data.id === id && c.data.version === version);
|
|
144
|
+
|
|
145
|
+
// Nothing found...
|
|
146
|
+
if (!channel) {
|
|
147
|
+
return { nodes: [], edges: [] };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const allMessages = [...events, ...commands, ...queries] as CollectionEntry<CollectionMessageTypes>[];
|
|
151
|
+
const messageMap = createVersionedMap(allMessages);
|
|
152
|
+
const channelMap = createVersionedMap(channels);
|
|
153
|
+
|
|
154
|
+
const getRoutesThroughFocusedChannel = (
|
|
155
|
+
pointers: ChannelPointer[] | undefined,
|
|
156
|
+
direction: 'producers' | 'consumers'
|
|
157
|
+
): ChannelRoute[] =>
|
|
158
|
+
(pointers ?? []).flatMap((pointer) => {
|
|
159
|
+
const resolved = findInMap(channelMap, pointer.id, pointer.version) as CollectionEntry<'channels'> | undefined;
|
|
160
|
+
if (!resolved) return [];
|
|
161
|
+
|
|
162
|
+
const route =
|
|
163
|
+
direction === 'producers' ? getChannelChain(resolved, channel, channels) : getChannelChain(channel, resolved, channels);
|
|
164
|
+
|
|
165
|
+
return route.length > 0 ? [route] : [];
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const routesDirectlyToFocusedChannel = (pointers: ChannelPointer[] | undefined) =>
|
|
169
|
+
(pointers ?? []).some((pointer) => {
|
|
170
|
+
const resolved = findInMap(channelMap, pointer.id, pointer.version) as CollectionEntry<'channels'> | undefined;
|
|
171
|
+
return !!resolved && generateIdForNode(resolved) === generateIdForNode(channel);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// messageNodeId -> { message, producers, consumers }
|
|
175
|
+
const messageFlows = new Map<
|
|
176
|
+
string,
|
|
177
|
+
{
|
|
178
|
+
message: CollectionEntry<CollectionMessageTypes>;
|
|
179
|
+
producers: RoutedResource[];
|
|
180
|
+
consumers: RoutedResource[];
|
|
181
|
+
}
|
|
182
|
+
>();
|
|
183
|
+
|
|
184
|
+
const trackFlow = (
|
|
185
|
+
message: CollectionEntry<CollectionMessageTypes>,
|
|
186
|
+
resource: MessageProducerOrConsumer,
|
|
187
|
+
direction: 'producers' | 'consumers',
|
|
188
|
+
channelRoutes: ChannelRoute[]
|
|
189
|
+
) => {
|
|
190
|
+
const messageNodeId = generateIdForNode(message);
|
|
191
|
+
const flowForMessage = messageFlows.get(messageNodeId) ?? { message, producers: [], consumers: [] };
|
|
192
|
+
const alreadyTracked = flowForMessage[direction].some(
|
|
193
|
+
(existing) => existing.resource.data.id === resource.data.id && existing.resource.data.version === resource.data.version
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
if (!alreadyTracked) {
|
|
197
|
+
flowForMessage[direction].push({ resource, channelRoutes });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
messageFlows.set(messageNodeId, flowForMessage);
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
for (const resource of [...services, ...agents] as MessageProducerOrConsumer[]) {
|
|
204
|
+
const pointerGroups = [
|
|
205
|
+
{ pointers: resource.data.sends ?? [], channelKey: 'to' as const, direction: 'producers' as const },
|
|
206
|
+
{ pointers: resource.data.receives ?? [], channelKey: 'from' as const, direction: 'consumers' as const },
|
|
207
|
+
];
|
|
208
|
+
|
|
209
|
+
for (const { pointers, channelKey, direction } of pointerGroups) {
|
|
210
|
+
for (const pointer of pointers as any[]) {
|
|
211
|
+
const message = findInMap(messageMap, pointer.id, pointer.version) as CollectionEntry<CollectionMessageTypes> | undefined;
|
|
212
|
+
|
|
213
|
+
if (!message) continue;
|
|
214
|
+
|
|
215
|
+
const channelPointers = pointer[channelKey] as ChannelPointer[] | undefined;
|
|
216
|
+
const hasExplicitChannels = Array.isArray(channelPointers) && channelPointers.length > 0;
|
|
217
|
+
const effectiveChannelPointers = hasExplicitChannels
|
|
218
|
+
? channelPointers
|
|
219
|
+
: (message.data.channels as ChannelPointer[] | undefined);
|
|
220
|
+
const channelRoutes = getRoutesThroughFocusedChannel(effectiveChannelPointers, direction);
|
|
221
|
+
|
|
222
|
+
if (channelRoutes.length > 0) {
|
|
223
|
+
trackFlow(message, resource, direction, channelRoutes);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Messages whose declared channel routes into the focused channel still belong on the graph,
|
|
230
|
+
// even when no producer or consumer references them.
|
|
231
|
+
for (const message of allMessages) {
|
|
232
|
+
const messageNodeId = generateIdForNode(message);
|
|
233
|
+
const declaredRoutes = getRoutesThroughFocusedChannel(message.data.channels as ChannelPointer[] | undefined, 'producers');
|
|
234
|
+
if (declaredRoutes.length > 0 && !messageFlows.has(messageNodeId)) {
|
|
235
|
+
messageFlows.set(messageNodeId, { message, producers: [], consumers: [] });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const channelNodeId = generateIdForNode(channel);
|
|
240
|
+
const addedNodeIds = new Set<string>();
|
|
241
|
+
const addedEdgeIds = new Set<string>();
|
|
242
|
+
|
|
243
|
+
const addNode = (node: any) => {
|
|
244
|
+
if (addedNodeIds.has(node.id)) return;
|
|
245
|
+
addedNodeIds.add(node.id);
|
|
246
|
+
nodes.push(node);
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const addEdge = (edge: any) => {
|
|
250
|
+
if (addedEdgeIds.has(edge.id)) return;
|
|
251
|
+
addedEdgeIds.add(edge.id);
|
|
252
|
+
edges.push(edge);
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const addResourceNode = (resource: MessageProducerOrConsumer) => {
|
|
256
|
+
addNode(
|
|
257
|
+
createNode({
|
|
258
|
+
id: generateIdForNode(resource),
|
|
259
|
+
type: resource.collection,
|
|
260
|
+
data: {
|
|
261
|
+
mode,
|
|
262
|
+
...(resource.collection === 'agents' ? { agent: { ...resource.data } } : { service: { ...resource.data } }),
|
|
263
|
+
contextMenu:
|
|
264
|
+
resource.collection === 'agents'
|
|
265
|
+
? buildContextMenuForAgent({ id: resource.data.id, version: resource.data.version })
|
|
266
|
+
: buildContextMenuForService({
|
|
267
|
+
id: resource.data.id,
|
|
268
|
+
version: resource.data.version,
|
|
269
|
+
specifications: (resource.data as any).specifications,
|
|
270
|
+
repository: (resource.data as any).repository,
|
|
271
|
+
}),
|
|
272
|
+
},
|
|
273
|
+
position: { x: 0, y: 0 },
|
|
274
|
+
})
|
|
275
|
+
);
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const addChannelNode = (channelToAdd: CollectionEntry<'channels'>, isFocused = false) => {
|
|
279
|
+
addNode(
|
|
280
|
+
createNode({
|
|
281
|
+
id: generateIdForNode(channelToAdd),
|
|
282
|
+
type: channelToAdd.collection,
|
|
283
|
+
data: {
|
|
284
|
+
mode,
|
|
285
|
+
...(isFocused ? { isFocused: true } : {}),
|
|
286
|
+
channel: { ...channelToAdd.data },
|
|
287
|
+
contextMenu: buildContextMenuForResource({
|
|
288
|
+
collection: 'channels',
|
|
289
|
+
id: channelToAdd.data.id,
|
|
290
|
+
version: channelToAdd.data.version,
|
|
291
|
+
}),
|
|
292
|
+
},
|
|
293
|
+
position: { x: 0, y: 0 },
|
|
294
|
+
})
|
|
295
|
+
);
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
// The channel itself
|
|
299
|
+
addChannelNode(channel, true);
|
|
300
|
+
|
|
301
|
+
const addChannelRoute = (route: ChannelRoute, colorSourceId: string) => {
|
|
302
|
+
route.forEach((channelInRoute) => addChannelNode(channelInRoute, generateIdForNode(channelInRoute) === channelNodeId));
|
|
303
|
+
|
|
304
|
+
for (let index = 0; index < route.length - 1; index++) {
|
|
305
|
+
const sourceChannel = route[index];
|
|
306
|
+
const targetChannel = route[index + 1];
|
|
307
|
+
addEdge(
|
|
308
|
+
createEdge({
|
|
309
|
+
id: generatedIdForEdge(sourceChannel, targetChannel),
|
|
310
|
+
source: generateIdForNode(sourceChannel),
|
|
311
|
+
target: generateIdForNode(targetChannel),
|
|
312
|
+
label: 'routes to',
|
|
313
|
+
data: { customColor: getColorFromString(colorSourceId) },
|
|
314
|
+
})
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
// channelNodeId + consumerNodeId -> messages consumed, so each channel/consumer pair gets one edge.
|
|
320
|
+
const messagesByConsumer = new Map<
|
|
321
|
+
string,
|
|
322
|
+
{
|
|
323
|
+
sourceChannel: CollectionEntry<'channels'>;
|
|
324
|
+
consumer: MessageProducerOrConsumer;
|
|
325
|
+
messages: CollectionEntry<CollectionMessageTypes>[];
|
|
326
|
+
}
|
|
327
|
+
>();
|
|
328
|
+
|
|
329
|
+
for (const { message, producers, consumers } of messageFlows.values()) {
|
|
330
|
+
const messageNodeId = generateIdForNode(message);
|
|
331
|
+
|
|
332
|
+
addNode(
|
|
333
|
+
createNode({
|
|
334
|
+
id: messageNodeId,
|
|
335
|
+
type: message.collection,
|
|
336
|
+
data: {
|
|
337
|
+
mode,
|
|
338
|
+
message: { ...message.data, ...getOperationFields(message.data) },
|
|
339
|
+
contextMenu: buildContextMenuForMessage({
|
|
340
|
+
id: message.data.id,
|
|
341
|
+
version: message.data.version,
|
|
342
|
+
name: message.data.name,
|
|
343
|
+
collection: message.collection,
|
|
344
|
+
schemaPath: (message.data as any).schemaPath,
|
|
345
|
+
}),
|
|
346
|
+
},
|
|
347
|
+
position: { x: 0, y: 0 },
|
|
348
|
+
})
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
const producerRoutes = producers.flatMap((producer) => producer.channelRoutes);
|
|
352
|
+
const declaredRoutes = getRoutesThroughFocusedChannel(message.data.channels as ChannelPointer[] | undefined, 'producers');
|
|
353
|
+
const ingressRoutes = producerRoutes.length > 0 ? producerRoutes : declaredRoutes;
|
|
354
|
+
|
|
355
|
+
// When only a consumer declares the focused channel, the message still enters at the
|
|
356
|
+
// focused channel, matching the message graph's no-producer-channel behaviour.
|
|
357
|
+
const routesToRender = ingressRoutes.length > 0 ? ingressRoutes : [[channel]];
|
|
358
|
+
for (const route of routesToRender) {
|
|
359
|
+
addChannelRoute(route, message.data.id);
|
|
360
|
+
const firstChannel = route[0];
|
|
361
|
+
addEdge(
|
|
362
|
+
createEdge({
|
|
363
|
+
id: generatedIdForEdge(message, firstChannel),
|
|
364
|
+
source: messageNodeId,
|
|
365
|
+
target: generateIdForNode(firstChannel),
|
|
366
|
+
label: 'routes to',
|
|
367
|
+
data: { customColor: getColorFromString(message.data.id) },
|
|
368
|
+
})
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
for (const { resource: producer } of producers) {
|
|
373
|
+
addResourceNode(producer);
|
|
374
|
+
addEdge(
|
|
375
|
+
createEdge({
|
|
376
|
+
id: generatedIdForEdge(producer, message),
|
|
377
|
+
source: generateIdForNode(producer),
|
|
378
|
+
target: messageNodeId,
|
|
379
|
+
label: getEdgeLabelForServiceAsTarget(message),
|
|
380
|
+
type: 'multiline',
|
|
381
|
+
data: { customColor: getColorFromString(message.data.id) },
|
|
382
|
+
})
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
for (const { resource: consumer, channelRoutes } of consumers) {
|
|
387
|
+
for (const route of channelRoutes) {
|
|
388
|
+
addChannelRoute(route, message.data.id);
|
|
389
|
+
const sourceChannel = route[route.length - 1];
|
|
390
|
+
const consumerNodeId = generateIdForNode(consumer);
|
|
391
|
+
const groupKey = `${generateIdForNode(sourceChannel)}:${consumerNodeId}`;
|
|
392
|
+
const grouped = messagesByConsumer.get(groupKey) ?? { sourceChannel, consumer, messages: [] };
|
|
393
|
+
if (!grouped.messages.some((groupedMessage) => generateIdForNode(groupedMessage) === messageNodeId)) {
|
|
394
|
+
grouped.messages.push(message);
|
|
395
|
+
}
|
|
396
|
+
messagesByConsumer.set(groupKey, grouped);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// One edge per consumer, labelled with the message when there is only one
|
|
402
|
+
for (const { sourceChannel, consumer, messages } of messagesByConsumer.values()) {
|
|
403
|
+
addResourceNode(consumer);
|
|
404
|
+
addEdge(
|
|
405
|
+
createEdge({
|
|
406
|
+
id: generatedIdForEdge(sourceChannel, consumer),
|
|
407
|
+
source: generateIdForNode(sourceChannel),
|
|
408
|
+
target: generateIdForNode(consumer),
|
|
409
|
+
label:
|
|
410
|
+
messages.length === 1 ? getEdgeLabelForMessageAsSource(messages[0], true) : `consumes ${messages.length}\nmessages`,
|
|
411
|
+
type: 'multiline',
|
|
412
|
+
data: { customColor: getColorFromString(sourceChannel.data.id) },
|
|
413
|
+
})
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Channels this channel routes messages on to
|
|
418
|
+
for (const route of (channel.data.routes ?? []) as { id: string; version?: string }[]) {
|
|
419
|
+
const routedChannel = findInMap(channelMap, route.id, route.version) as CollectionEntry<'channels'> | undefined;
|
|
420
|
+
if (!routedChannel || generateIdForNode(routedChannel) === channelNodeId) continue;
|
|
421
|
+
|
|
422
|
+
addChannelNode(routedChannel);
|
|
423
|
+
addEdge(
|
|
424
|
+
createEdge({
|
|
425
|
+
id: generatedIdForEdge(channel, routedChannel),
|
|
426
|
+
source: channelNodeId,
|
|
427
|
+
target: generateIdForNode(routedChannel),
|
|
428
|
+
label: 'routes to',
|
|
429
|
+
data: { customColor: getColorFromString(channel.data.id) },
|
|
430
|
+
})
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Channels that route messages into this channel
|
|
435
|
+
for (const upstreamChannel of channels) {
|
|
436
|
+
if (generateIdForNode(upstreamChannel) === channelNodeId) continue;
|
|
437
|
+
if (!routesDirectlyToFocusedChannel(upstreamChannel.data.routes as ChannelPointer[] | undefined)) continue;
|
|
438
|
+
|
|
439
|
+
addChannelNode(upstreamChannel);
|
|
440
|
+
addEdge(
|
|
441
|
+
createEdge({
|
|
442
|
+
id: generatedIdForEdge(upstreamChannel, channel),
|
|
443
|
+
source: generateIdForNode(upstreamChannel),
|
|
444
|
+
target: channelNodeId,
|
|
445
|
+
label: 'routes to',
|
|
446
|
+
data: { customColor: getColorFromString(upstreamChannel.data.id) },
|
|
447
|
+
})
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
nodes.forEach((node) => {
|
|
452
|
+
flow.setNode(node.id, { width: DEFAULT_NODE_WIDTH, height: DEFAULT_NODE_HEIGHT });
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
edges.forEach((edge) => {
|
|
456
|
+
flow.setEdge(edge.source, edge.target);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
if (layout) {
|
|
460
|
+
dagre.layout(flow);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return {
|
|
464
|
+
nodes: calculatedNodes(flow, nodes),
|
|
465
|
+
edges,
|
|
466
|
+
};
|
|
467
|
+
};
|
package/package.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
},
|
|
8
8
|
"license": "SEE LICENSE IN LICENSE",
|
|
9
9
|
"type": "module",
|
|
10
|
-
"version": "4.
|
|
10
|
+
"version": "4.7.0",
|
|
11
11
|
"publishConfig": {
|
|
12
12
|
"access": "public"
|
|
13
13
|
},
|
|
@@ -121,7 +121,7 @@
|
|
|
121
121
|
"zod": "^4.3.6",
|
|
122
122
|
"@eventcatalog/linter": "1.1.11",
|
|
123
123
|
"@eventcatalog/sdk": "2.27.2",
|
|
124
|
-
"@eventcatalog/visualiser": "^4.1.
|
|
124
|
+
"@eventcatalog/visualiser": "^4.1.3"
|
|
125
125
|
},
|
|
126
126
|
"devDependencies": {
|
|
127
127
|
"@astrojs/check": "^0.9.10",
|