@eventcatalog/core 4.1.5 → 4.2.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-DIUAURE2.js → chunk-47LY7DY2.js} +1 -1
- package/dist/{chunk-X77TET65.js → chunk-4FWALA6J.js} +1 -1
- package/dist/{chunk-GIU3S4CI.js → chunk-J7T5HUHY.js} +1 -1
- package/dist/{chunk-G74MXXTG.js → chunk-KPDU47YJ.js} +1 -1
- package/dist/{chunk-E6IUV4GX.js → chunk-QKPPJU43.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 +5 -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/src/components/Lineage/LineageScenarioSwitcher.tsx +65 -0
- package/eventcatalog/src/content.config.ts +9 -0
- package/eventcatalog/src/pages/triggers/[type]/[id]/[version]/_index.data.ts +66 -0
- package/eventcatalog/src/pages/triggers/[type]/[id]/[version]/index.astro +201 -0
- package/eventcatalog/src/stores/sidebar-store/builders/message.ts +31 -1
- package/eventcatalog/src/stores/sidebar-store/state.ts +22 -4
- package/eventcatalog/src/utils/collections/commands.ts +1 -1
- package/eventcatalog/src/utils/collections/message-triggers.ts +118 -0
- package/eventcatalog/src/utils/node-graphs/container-node-graph.ts +2 -1
- package/eventcatalog/src/utils/node-graphs/message-lineage.ts +131 -0
- package/eventcatalog/src/utils/node-graphs/message-node-graph.ts +119 -1
- package/eventcatalog/src/utils/node-graphs/services-node-graph.ts +15 -1
- package/eventcatalog/src/utils/node-graphs/utils/utils.ts +14 -4
- package/package.json +4 -4
|
@@ -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.2.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-
|
|
3
|
+
} from "../chunk-47LY7DY2.js";
|
|
4
|
+
import "../chunk-J7T5HUHY.js";
|
|
5
5
|
import "../chunk-K2XIENVT.js";
|
|
6
|
-
import "../chunk-
|
|
6
|
+
import "../chunk-4FWALA6J.js";
|
|
7
7
|
import "../chunk-6QENHZZP.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
|
@@ -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.2.0";
|
|
148
148
|
|
|
149
149
|
// src/constants.ts
|
|
150
150
|
var VERSION = version;
|
package/dist/eventcatalog.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
2
|
log_build_default
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-47LY7DY2.js";
|
|
4
4
|
import {
|
|
5
5
|
runMigrations
|
|
6
6
|
} from "./chunk-XUAF2H54.js";
|
|
7
7
|
import "./chunk-CA4U2JP7.js";
|
|
8
8
|
import {
|
|
9
9
|
generate
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-QKPPJU43.js";
|
|
11
11
|
import {
|
|
12
12
|
logger
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-KPDU47YJ.js";
|
|
14
14
|
import {
|
|
15
15
|
resolve_catalog_dependencies_default
|
|
16
16
|
} from "./chunk-LHR4G2UO.js";
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
import {
|
|
21
21
|
watch
|
|
22
22
|
} from "./chunk-CAJUIMDJ.js";
|
|
23
|
-
import "./chunk-
|
|
23
|
+
import "./chunk-J7T5HUHY.js";
|
|
24
24
|
import "./chunk-K2XIENVT.js";
|
|
25
25
|
import {
|
|
26
26
|
catalogToAstro
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
import "./chunk-W5JQON7Z.js";
|
|
29
29
|
import {
|
|
30
30
|
VERSION
|
|
31
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-4FWALA6J.js";
|
|
32
32
|
import {
|
|
33
33
|
linkCoreNodeModules,
|
|
34
34
|
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.2.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-QKPPJU43.js";
|
|
4
|
+
import "./chunk-KPDU47YJ.js";
|
|
5
|
+
import "./chunk-4FWALA6J.js";
|
|
6
6
|
import "./chunk-6QENHZZP.js";
|
|
7
7
|
export {
|
|
8
8
|
generate
|
package/dist/utils/cli-logger.js
CHANGED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { CheckIcon, ChevronDownIcon } from '@heroicons/react/20/solid';
|
|
2
|
+
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
conditions: string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default function LineageScenarioSwitcher({ conditions }: Props) {
|
|
10
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
11
|
+
const selectedCondition = conditions[selectedIndex];
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<DropdownMenu.Root>
|
|
15
|
+
<DropdownMenu.Trigger asChild>
|
|
16
|
+
<button
|
|
17
|
+
type="button"
|
|
18
|
+
className="group inline-flex max-w-full items-center gap-1.5 rounded-md border border-[rgb(var(--ec-dropdown-border)/0.8)] bg-[rgb(var(--ec-dropdown-bg))] px-2 py-1 align-middle text-sm font-semibold text-[rgb(var(--ec-page-text))] shadow-sm transition-colors hover:border-[rgb(var(--ec-accent)/0.5)] hover:bg-[rgb(var(--ec-dropdown-hover))] focus:outline-hidden focus:ring-2 focus:ring-[rgb(var(--ec-accent)/0.3)]"
|
|
19
|
+
aria-label={`Select scenario. Scenario ${selectedIndex + 1} of ${conditions.length}: ${selectedCondition}`}
|
|
20
|
+
>
|
|
21
|
+
<span className="min-w-0 max-w-xl truncate">{selectedCondition}</span>
|
|
22
|
+
<span className="shrink-0 rounded bg-[rgb(var(--ec-accent-subtle))] px-1.5 py-0.5 text-[10px] font-semibold tabular-nums text-[rgb(var(--ec-accent-text))]">
|
|
23
|
+
{selectedIndex + 1}/{conditions.length}
|
|
24
|
+
</span>
|
|
25
|
+
<ChevronDownIcon className="h-3.5 w-3.5 shrink-0 text-[rgb(var(--ec-page-text-muted))] transition-transform group-data-[state=open]:rotate-180" />
|
|
26
|
+
</button>
|
|
27
|
+
</DropdownMenu.Trigger>
|
|
28
|
+
|
|
29
|
+
<DropdownMenu.Portal>
|
|
30
|
+
<DropdownMenu.Content
|
|
31
|
+
align="start"
|
|
32
|
+
sideOffset={6}
|
|
33
|
+
collisionPadding={12}
|
|
34
|
+
className="z-[1000] max-h-80 w-[min(36rem,calc(100vw-1.5rem))] overflow-y-auto rounded-xl border border-[rgb(var(--ec-dropdown-border)/0.8)] bg-[rgb(var(--ec-dropdown-bg))] p-1.5 shadow-[0_20px_56px_rgb(0_0_0/0.22)]"
|
|
35
|
+
>
|
|
36
|
+
<div className="px-2.5 pb-2 pt-1.5">
|
|
37
|
+
<p className="text-xs font-semibold text-[rgb(var(--ec-dropdown-text))]">Choose a scenario</p>
|
|
38
|
+
<p className="pt-0.5 text-[11px] font-normal text-[rgb(var(--ec-page-text-muted))]">
|
|
39
|
+
This message path is shared by {conditions.length} conditions.
|
|
40
|
+
</p>
|
|
41
|
+
</div>
|
|
42
|
+
<DropdownMenu.Separator className="mb-1 h-px bg-[rgb(var(--ec-dropdown-border)/0.7)]" />
|
|
43
|
+
<DropdownMenu.RadioGroup value={String(selectedIndex)} onValueChange={(value) => setSelectedIndex(Number(value))}>
|
|
44
|
+
{conditions.map((condition, index) => (
|
|
45
|
+
<DropdownMenu.RadioItem
|
|
46
|
+
key={`${condition}-${index}`}
|
|
47
|
+
value={String(index)}
|
|
48
|
+
className="relative flex cursor-pointer select-none items-start gap-2.5 rounded-lg px-2.5 py-2 pr-9 text-sm font-normal leading-5 text-[rgb(var(--ec-dropdown-text))] outline-hidden transition-colors data-[highlighted]:bg-[rgb(var(--ec-dropdown-hover))]"
|
|
49
|
+
>
|
|
50
|
+
<span className="mt-0.5 flex h-5 min-w-5 items-center justify-center rounded-md bg-[rgb(var(--ec-accent-subtle))] px-1 text-[10px] font-semibold tabular-nums text-[rgb(var(--ec-accent-text))]">
|
|
51
|
+
{index + 1}
|
|
52
|
+
</span>
|
|
53
|
+
<span>{condition}</span>
|
|
54
|
+
<DropdownMenu.ItemIndicator className="absolute right-2.5 top-2.5 text-[rgb(var(--ec-accent))]">
|
|
55
|
+
<CheckIcon className="h-4 w-4" />
|
|
56
|
+
</DropdownMenu.ItemIndicator>
|
|
57
|
+
</DropdownMenu.RadioItem>
|
|
58
|
+
))}
|
|
59
|
+
</DropdownMenu.RadioGroup>
|
|
60
|
+
<DropdownMenu.Arrow className="fill-[rgb(var(--ec-dropdown-bg))]" />
|
|
61
|
+
</DropdownMenu.Content>
|
|
62
|
+
</DropdownMenu.Portal>
|
|
63
|
+
</DropdownMenu.Root>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
@@ -87,11 +87,18 @@ const sendsPointer = z.object({
|
|
|
87
87
|
.optional(),
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
+
const triggersPointer = z.object({
|
|
91
|
+
id: z.string(),
|
|
92
|
+
version: z.string().optional().default('latest'),
|
|
93
|
+
condition: z.string().optional(),
|
|
94
|
+
});
|
|
95
|
+
|
|
90
96
|
const receivesPointer = z.object({
|
|
91
97
|
id: z.string(),
|
|
92
98
|
version: z.string().optional().default('latest'),
|
|
93
99
|
fields: z.array(z.string()).optional(),
|
|
94
100
|
group: z.string().optional(),
|
|
101
|
+
triggers: z.array(triggersPointer).optional(),
|
|
95
102
|
from: z
|
|
96
103
|
.array(
|
|
97
104
|
z.object({
|
|
@@ -355,6 +362,8 @@ const operationSchema = z
|
|
|
355
362
|
const messageDetailsPanelPropertySchema = z.object({
|
|
356
363
|
producers: detailPanelPropertySchema.optional(),
|
|
357
364
|
consumers: detailPanelPropertySchema.optional(),
|
|
365
|
+
triggers: detailPanelPropertySchema.optional(),
|
|
366
|
+
triggeredBy: detailPanelPropertySchema.optional(),
|
|
358
367
|
channels: detailPanelPropertySchema.optional(),
|
|
359
368
|
versions: detailPanelPropertySchema.optional(),
|
|
360
369
|
repository: detailPanelPropertySchema.optional(),
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { getCollection, type CollectionEntry } from 'astro:content';
|
|
2
|
+
import { getMessagesWithTriggerPaths, type MessageReceiver } from '@utils/collections/message-triggers';
|
|
3
|
+
import { isSSR } from '@utils/feature';
|
|
4
|
+
import { HybridPage } from '@utils/page-loaders/hybrid-page';
|
|
5
|
+
import type { CollectionMessageTypes } from '@types';
|
|
6
|
+
|
|
7
|
+
const MESSAGE_TYPES: CollectionMessageTypes[] = ['events', 'commands', 'queries'];
|
|
8
|
+
|
|
9
|
+
const loadTriggerPathResources = async () => {
|
|
10
|
+
const { pageDataLoader } = await import('@utils/page-loaders/page-data-loader');
|
|
11
|
+
const [messageCollections, services, agents, domains] = await Promise.all([
|
|
12
|
+
Promise.all(MESSAGE_TYPES.map((type) => pageDataLoader[type]())),
|
|
13
|
+
getCollection('services'),
|
|
14
|
+
getCollection('agents'),
|
|
15
|
+
getCollection('domains'),
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
allMessages: messageCollections.flat() as CollectionEntry<CollectionMessageTypes>[],
|
|
20
|
+
receivers: [...services, ...agents, ...domains] as MessageReceiver[],
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export class Page extends HybridPage {
|
|
25
|
+
static get prerender(): boolean {
|
|
26
|
+
return !isSSR();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
static async getStaticPaths(): Promise<Array<{ params: any; props: any }>> {
|
|
30
|
+
if (isSSR()) return [];
|
|
31
|
+
|
|
32
|
+
const { allMessages, receivers } = await loadTriggerPathResources();
|
|
33
|
+
const messagesWithTriggerPaths = getMessagesWithTriggerPaths(receivers, allMessages);
|
|
34
|
+
|
|
35
|
+
return messagesWithTriggerPaths.map((item) => ({
|
|
36
|
+
params: {
|
|
37
|
+
type: item.collection,
|
|
38
|
+
id: item.data.id,
|
|
39
|
+
version: item.data.version,
|
|
40
|
+
},
|
|
41
|
+
props: {
|
|
42
|
+
type: item.collection,
|
|
43
|
+
...item,
|
|
44
|
+
},
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
protected static async fetchData(params: any) {
|
|
49
|
+
const { type, id, version } = params;
|
|
50
|
+
if (!type || !id || !version || !MESSAGE_TYPES.includes(type)) return null;
|
|
51
|
+
|
|
52
|
+
const { allMessages, receivers } = await loadTriggerPathResources();
|
|
53
|
+
const item = allMessages.find((entry) => entry.collection === type && entry.data.id === id && entry.data.version === version);
|
|
54
|
+
if (!item) return null;
|
|
55
|
+
|
|
56
|
+
const hasTriggerPaths = getMessagesWithTriggerPaths(receivers, allMessages).includes(item);
|
|
57
|
+
return hasTriggerPaths ? { type, ...item } : null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
protected static createNotFoundResponse(): Response {
|
|
61
|
+
return new Response(null, {
|
|
62
|
+
status: 404,
|
|
63
|
+
statusText: 'Message trigger paths not found',
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { getCollection, type CollectionEntry } from 'astro:content';
|
|
3
|
+
|
|
4
|
+
import LineageScenarioSwitcher from '@components/Lineage/LineageScenarioSwitcher';
|
|
5
|
+
import AstroNodeGraph from '@components/MDX/NodeGraph/AstroNodeGraph';
|
|
6
|
+
import ResourceRef from '@components/MDX/ResourceRef/ResourceRef.astro';
|
|
7
|
+
import Footer from '@layouts/Footer.astro';
|
|
8
|
+
import VerticalSideBarLayout from '@layouts/VerticalSideBarLayout.astro';
|
|
9
|
+
import { getTriggeredByOfMessage, getTriggersOfMessage, type MessageReceiver } from '@utils/collections/message-triggers';
|
|
10
|
+
import { buildMessageLineageGraph, groupMessageLineageGraphs } from '@utils/node-graphs/message-lineage';
|
|
11
|
+
import type { CollectionMessageTypes } from '@types';
|
|
12
|
+
|
|
13
|
+
import { Page } from './_index.data';
|
|
14
|
+
|
|
15
|
+
export const prerender = Page.prerender;
|
|
16
|
+
export const getStaticPaths = Page.getStaticPaths;
|
|
17
|
+
|
|
18
|
+
const props = await Page.getData(Astro);
|
|
19
|
+
const { data } = props as CollectionEntry<CollectionMessageTypes>;
|
|
20
|
+
const currentMessage = props as CollectionEntry<CollectionMessageTypes>;
|
|
21
|
+
|
|
22
|
+
const [events, commands, queries, services, agents, domains] = await Promise.all([
|
|
23
|
+
getCollection('events'),
|
|
24
|
+
getCollection('commands'),
|
|
25
|
+
getCollection('queries'),
|
|
26
|
+
getCollection('services'),
|
|
27
|
+
getCollection('agents'),
|
|
28
|
+
getCollection('domains'),
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
const allMessages = [...events, ...commands, ...queries] as CollectionEntry<CollectionMessageTypes>[];
|
|
32
|
+
const receivers = [...services, ...agents, ...domains] as MessageReceiver[];
|
|
33
|
+
|
|
34
|
+
const triggeredByRows = groupMessageLineageGraphs(
|
|
35
|
+
getTriggeredByOfMessage(receivers, currentMessage, allMessages).map((relation) =>
|
|
36
|
+
buildMessageLineageGraph({ currentMessage, relation, direction: 'triggeredBy' })
|
|
37
|
+
)
|
|
38
|
+
);
|
|
39
|
+
const triggerRows = groupMessageLineageGraphs(
|
|
40
|
+
getTriggersOfMessage(receivers, currentMessage, allMessages).map((relation) =>
|
|
41
|
+
buildMessageLineageGraph({ currentMessage, relation, direction: 'triggers' })
|
|
42
|
+
)
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const resourceTypeByCollection = {
|
|
46
|
+
events: 'event',
|
|
47
|
+
commands: 'command',
|
|
48
|
+
queries: 'query',
|
|
49
|
+
services: 'service',
|
|
50
|
+
domains: 'domain',
|
|
51
|
+
agents: 'agent',
|
|
52
|
+
} as const;
|
|
53
|
+
const resourceType = (resource: CollectionEntry<CollectionMessageTypes> | MessageReceiver) =>
|
|
54
|
+
resourceTypeByCollection[resource.collection];
|
|
55
|
+
const conditionText = (condition: string) => condition.replace(/^when\s+/i, '');
|
|
56
|
+
const scenarioConditions = (row: (typeof triggeredByRows)[number]) =>
|
|
57
|
+
row.scenarios.map(({ condition }) => (condition ? conditionText(condition) : 'No condition specified'));
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
<VerticalSideBarLayout title={`Trigger paths | ${data.name}`} description={`Message trigger paths for ${data.name}`}>
|
|
61
|
+
<main class="w-full min-h-full bg-[rgb(var(--ec-page-bg))] py-8">
|
|
62
|
+
<header class="border-b border-[rgb(var(--ec-page-border))] py-5">
|
|
63
|
+
<div class="text-sm font-semibold text-[rgb(var(--ec-accent))]">Trigger paths</div>
|
|
64
|
+
<h1 class="pt-3 text-2xl md:text-4xl font-bold text-[rgb(var(--ec-page-text))]">{data.name}</h1>
|
|
65
|
+
<p class="pt-3 text-base text-[rgb(var(--ec-page-text-muted))] font-light">
|
|
66
|
+
Explore the direct messages that trigger this message and the messages it can trigger.
|
|
67
|
+
</p>
|
|
68
|
+
</header>
|
|
69
|
+
|
|
70
|
+
<div class="space-y-10 py-8">
|
|
71
|
+
{
|
|
72
|
+
triggeredByRows.length > 0 && (
|
|
73
|
+
<section aria-labelledby="triggered-by-heading">
|
|
74
|
+
<div class="space-y-5">
|
|
75
|
+
{triggeredByRows.map((row, index) => (
|
|
76
|
+
<article class="lineage-row overflow-hidden rounded-xl border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-card-bg))] shadow-sm">
|
|
77
|
+
<div class="border-b border-[rgb(var(--ec-page-border))] px-5 py-4">
|
|
78
|
+
<h3 class="text-sm md:text-base font-medium text-[rgb(var(--ec-page-text))]">
|
|
79
|
+
<ResourceRef type={resourceType(row.target)} version={row.target.data.version}>
|
|
80
|
+
{row.target.data.id}
|
|
81
|
+
</ResourceRef>{' '}
|
|
82
|
+
is triggered by{' '}
|
|
83
|
+
<ResourceRef type={resourceType(row.source)} version={row.source.data.version}>
|
|
84
|
+
{row.source.data.id}
|
|
85
|
+
</ResourceRef>
|
|
86
|
+
{row.scenarios.length === 1 && row.scenarios[0].condition && (
|
|
87
|
+
<>
|
|
88
|
+
{' '}
|
|
89
|
+
when <span class="font-semibold">{conditionText(row.scenarios[0].condition)}</span>
|
|
90
|
+
</>
|
|
91
|
+
)}
|
|
92
|
+
{row.scenarios.length > 1 && (
|
|
93
|
+
<>
|
|
94
|
+
{' '}
|
|
95
|
+
when <LineageScenarioSwitcher client:visible conditions={scenarioConditions(row)} />
|
|
96
|
+
</>
|
|
97
|
+
)}{' '}
|
|
98
|
+
via{' '}
|
|
99
|
+
<ResourceRef type={resourceType(row.receiver)} version={row.receiver.data.version}>
|
|
100
|
+
{row.receiver.data.id}
|
|
101
|
+
</ResourceRef>
|
|
102
|
+
</h3>
|
|
103
|
+
</div>
|
|
104
|
+
<div
|
|
105
|
+
id={`${data.id}-triggered-by-${index}-portal`}
|
|
106
|
+
class="lineage-graph h-[250px] w-full bg-[rgb(var(--ec-page-bg))]"
|
|
107
|
+
/>
|
|
108
|
+
<AstroNodeGraph
|
|
109
|
+
client:only="react"
|
|
110
|
+
id={`${data.id}-triggered-by-${index}`}
|
|
111
|
+
portalId={`${data.id}-triggered-by-${index}-portal`}
|
|
112
|
+
nodes={row.nodes}
|
|
113
|
+
edges={row.edges}
|
|
114
|
+
mode="simple"
|
|
115
|
+
linkTo="docs"
|
|
116
|
+
includeKey={false}
|
|
117
|
+
showSearch={false}
|
|
118
|
+
zoomOnScroll={false}
|
|
119
|
+
disableMessageAnimation={true}
|
|
120
|
+
/>
|
|
121
|
+
</article>
|
|
122
|
+
))}
|
|
123
|
+
</div>
|
|
124
|
+
</section>
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
{
|
|
129
|
+
triggerRows.length > 0 && (
|
|
130
|
+
<section aria-label="Messages triggered by this message">
|
|
131
|
+
<div class="space-y-5">
|
|
132
|
+
{triggerRows.map((row, index) => (
|
|
133
|
+
<article class="lineage-row overflow-hidden rounded-xl border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-card-bg))] shadow-sm">
|
|
134
|
+
<div class="border-b border-[rgb(var(--ec-page-border))] px-5 py-4">
|
|
135
|
+
<h3 class="text-sm md:text-base font-medium text-[rgb(var(--ec-page-text))]">
|
|
136
|
+
<ResourceRef type={resourceType(row.target)} version={row.target.data.version}>
|
|
137
|
+
{row.target.data.id}
|
|
138
|
+
</ResourceRef>{' '}
|
|
139
|
+
is triggered by{' '}
|
|
140
|
+
<ResourceRef type={resourceType(row.source)} version={row.source.data.version}>
|
|
141
|
+
{row.source.data.id}
|
|
142
|
+
</ResourceRef>
|
|
143
|
+
{row.scenarios.length === 1 && row.scenarios[0].condition && (
|
|
144
|
+
<>
|
|
145
|
+
{' '}
|
|
146
|
+
when <span class="font-semibold">{conditionText(row.scenarios[0].condition)}</span>
|
|
147
|
+
</>
|
|
148
|
+
)}
|
|
149
|
+
{row.scenarios.length > 1 && (
|
|
150
|
+
<>
|
|
151
|
+
{' '}
|
|
152
|
+
when <LineageScenarioSwitcher client:visible conditions={scenarioConditions(row)} />
|
|
153
|
+
</>
|
|
154
|
+
)}{' '}
|
|
155
|
+
via{' '}
|
|
156
|
+
<ResourceRef type={resourceType(row.receiver)} version={row.receiver.data.version}>
|
|
157
|
+
{row.receiver.data.id}
|
|
158
|
+
</ResourceRef>
|
|
159
|
+
</h3>
|
|
160
|
+
</div>
|
|
161
|
+
<div
|
|
162
|
+
id={`${data.id}-triggers-${index}-portal`}
|
|
163
|
+
class="lineage-graph h-[250px] w-full bg-[rgb(var(--ec-page-bg))]"
|
|
164
|
+
/>
|
|
165
|
+
<AstroNodeGraph
|
|
166
|
+
client:only="react"
|
|
167
|
+
id={`${data.id}-triggers-${index}`}
|
|
168
|
+
portalId={`${data.id}-triggers-${index}-portal`}
|
|
169
|
+
nodes={row.nodes}
|
|
170
|
+
edges={row.edges}
|
|
171
|
+
mode="simple"
|
|
172
|
+
linkTo="docs"
|
|
173
|
+
includeKey={false}
|
|
174
|
+
showSearch={false}
|
|
175
|
+
zoomOnScroll={false}
|
|
176
|
+
disableMessageAnimation={true}
|
|
177
|
+
/>
|
|
178
|
+
</article>
|
|
179
|
+
))}
|
|
180
|
+
</div>
|
|
181
|
+
</section>
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
<Footer />
|
|
187
|
+
</main>
|
|
188
|
+
</VerticalSideBarLayout>
|
|
189
|
+
|
|
190
|
+
<style is:global>
|
|
191
|
+
.lineage-row {
|
|
192
|
+
content-visibility: auto;
|
|
193
|
+
contain-intrinsic-size: 330px;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.lineage-graph .react-flow__panel.top,
|
|
197
|
+
.lineage-graph .react-flow__controls,
|
|
198
|
+
.lineage-graph .react-flow__attribution {
|
|
199
|
+
display: none;
|
|
200
|
+
}
|
|
201
|
+
</style>
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import { isVisualiserEnabled, isChangelogEnabled } from '@utils/feature';
|
|
15
15
|
import { iconFieldsForResource } from '@utils/icon';
|
|
16
16
|
import { collectionToResourceMap } from '@utils/collections/util';
|
|
17
|
+
import type { MessageTrigger } from '@utils/collections/message-triggers';
|
|
17
18
|
|
|
18
19
|
type MessageSchemaEntry = CollectionEntry<'schemas'>;
|
|
19
20
|
|
|
@@ -47,16 +48,22 @@ export const buildMessageNode = (
|
|
|
47
48
|
owners: any[],
|
|
48
49
|
context: ResourceGroupContext,
|
|
49
50
|
hasFieldUsage: boolean = false,
|
|
50
|
-
flowRefs: string[] = []
|
|
51
|
+
flowRefs: string[] = [],
|
|
52
|
+
messageTriggers: { triggers: MessageTrigger[]; triggeredBy: MessageTrigger[] } = { triggers: [], triggeredBy: [] }
|
|
51
53
|
): NavNode => {
|
|
52
54
|
const producers = message.data.producers || [];
|
|
53
55
|
const consumers = message.data.consumers || [];
|
|
54
56
|
const collection = message.collection;
|
|
57
|
+
const triggerRefs = [...new Set(messageTriggers.triggers.map(({ message }) => getProducerConsumerPageRef(message)))];
|
|
58
|
+
const triggeredByRefs = [...new Set(messageTriggers.triggeredBy.map(({ message }) => getProducerConsumerPageRef(message)))];
|
|
55
59
|
|
|
56
60
|
const renderProducers = producers.length > 0 && shouldRenderSideBarSection(message, 'producers');
|
|
57
61
|
const renderConsumers = consumers.length > 0 && shouldRenderSideBarSection(message, 'consumers');
|
|
62
|
+
const renderTriggers = triggerRefs.length > 0 && shouldRenderSideBarSection(message, 'triggers');
|
|
63
|
+
const renderTriggeredBy = triggeredByRefs.length > 0 && shouldRenderSideBarSection(message, 'triggeredBy');
|
|
58
64
|
const renderFlows = flowRefs.length > 0 && shouldRenderSideBarSection(message, 'flows');
|
|
59
65
|
const renderRepository = message.data.repository && shouldRenderSideBarSection(message, 'repository');
|
|
66
|
+
const hasTriggerPaths = messageTriggers.triggers.length > 0 || messageTriggers.triggeredBy.length > 0;
|
|
60
67
|
|
|
61
68
|
// Determine badge based on collection type
|
|
62
69
|
const badgeMap: Record<string, string> = {
|
|
@@ -124,6 +131,15 @@ export const buildMessageNode = (
|
|
|
124
131
|
title: 'Map',
|
|
125
132
|
href: buildUrl(`/visualiser/${collection}/${message.data.id}/${message.data.version}`),
|
|
126
133
|
},
|
|
134
|
+
...(hasTriggerPaths
|
|
135
|
+
? [
|
|
136
|
+
{
|
|
137
|
+
type: 'item' as const,
|
|
138
|
+
title: 'Trigger paths',
|
|
139
|
+
href: buildUrl(`/triggers/${collection}/${message.data.id}/${message.data.version}`),
|
|
140
|
+
},
|
|
141
|
+
]
|
|
142
|
+
: []),
|
|
127
143
|
],
|
|
128
144
|
},
|
|
129
145
|
hasDiagrams && {
|
|
@@ -163,6 +179,20 @@ export const buildMessageNode = (
|
|
|
163
179
|
pages: consumers.map(getProducerConsumerPageRef),
|
|
164
180
|
visible: consumers.length > 0,
|
|
165
181
|
},
|
|
182
|
+
renderTriggeredBy && {
|
|
183
|
+
type: 'group',
|
|
184
|
+
title: 'Triggered by',
|
|
185
|
+
icon: 'Mail',
|
|
186
|
+
pages: triggeredByRefs,
|
|
187
|
+
visible: triggeredByRefs.length > 0,
|
|
188
|
+
},
|
|
189
|
+
renderTriggers && {
|
|
190
|
+
type: 'group',
|
|
191
|
+
title: 'Triggers',
|
|
192
|
+
icon: 'Mail',
|
|
193
|
+
pages: triggerRefs,
|
|
194
|
+
visible: triggerRefs.length > 0,
|
|
195
|
+
},
|
|
166
196
|
renderFlows && {
|
|
167
197
|
type: 'group',
|
|
168
198
|
title: 'Appears in flows',
|
|
@@ -8,6 +8,7 @@ import { getDomains } from '@utils/collections/domains';
|
|
|
8
8
|
import { getSystems } from '@utils/collections/systems';
|
|
9
9
|
import { getServices } from '@utils/collections/services';
|
|
10
10
|
import { getMessages, pluralizeMessageType } from '@utils/collections/messages';
|
|
11
|
+
import { getTriggeredByOfMessage, getTriggersOfMessage } from '@utils/collections/message-triggers';
|
|
11
12
|
import { getOwner } from '@utils/collections/owners';
|
|
12
13
|
import { getFlows } from '@utils/collections/flows';
|
|
13
14
|
import { getUsers } from '@utils/collections/users';
|
|
@@ -451,10 +452,14 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
|
|
|
451
452
|
{} as Record<string, NavNode | string>
|
|
452
453
|
);
|
|
453
454
|
|
|
454
|
-
const rawAgents = await
|
|
455
|
+
const [rawAgents, rawServices, rawDomains] = await Promise.all([
|
|
456
|
+
getCollection('agents'),
|
|
457
|
+
getCollection('services'),
|
|
458
|
+
getCollection('domains'),
|
|
459
|
+
]);
|
|
460
|
+
const messageReceivers = [...rawServices, ...rawAgents, ...rawDomains];
|
|
455
461
|
|
|
456
462
|
// Compute channels for each service from raw sends[].to / receives[].from pointers
|
|
457
|
-
const rawServices = await getCollection('services');
|
|
458
463
|
const channelMap = createVersionedMap(channels);
|
|
459
464
|
const serviceChannelsMap = new Map<string, CollectionEntry<'channels'>[]>();
|
|
460
465
|
|
|
@@ -560,7 +565,7 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
|
|
|
560
565
|
{} as Record<string, NavNode | string>
|
|
561
566
|
);
|
|
562
567
|
|
|
563
|
-
// Build a set of message IDs that have field usage declared by any
|
|
568
|
+
// Build a set of message IDs that have field usage declared by any resource that can receive messages.
|
|
564
569
|
// We use raw collections because the hydrated resources replace
|
|
565
570
|
// sends/receives pointers with resolved message entries, which strips the fields property.
|
|
566
571
|
const messagesWithFieldUsage = new Set<string>();
|
|
@@ -580,6 +585,14 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
|
|
|
580
585
|
if (pointer.fields?.length) messagesWithFieldUsage.add(pointer.id);
|
|
581
586
|
}
|
|
582
587
|
}
|
|
588
|
+
for (const domain of rawDomains) {
|
|
589
|
+
for (const pointer of domain.data.sends || []) {
|
|
590
|
+
if (pointer.fields?.length) messagesWithFieldUsage.add(pointer.id);
|
|
591
|
+
}
|
|
592
|
+
for (const pointer of domain.data.receives || []) {
|
|
593
|
+
if (pointer.fields?.length) messagesWithFieldUsage.add(pointer.id);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
583
596
|
|
|
584
597
|
const flowRefsByMessage = buildFlowReferencesByMessage({ flows, events, commands, queries });
|
|
585
598
|
|
|
@@ -588,8 +601,13 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
|
|
|
588
601
|
const type = pluralizeMessageType(message as any);
|
|
589
602
|
const versionedKey = `${type}:${message.data.id}:${message.data.version}`;
|
|
590
603
|
const hasFieldUsage = messagesWithFieldUsage.has(message.data.id);
|
|
604
|
+
const triggers = getTriggersOfMessage(messageReceivers, message, messages);
|
|
605
|
+
const triggeredBy = getTriggeredByOfMessage(messageReceivers, message, messages);
|
|
591
606
|
acc[versionedKey] = withArchitectureDecisionsSection(
|
|
592
|
-
buildMessageNode(message, owners, context, hasFieldUsage, flowRefsByMessage.get(versionedKey) || []
|
|
607
|
+
buildMessageNode(message, owners, context, hasFieldUsage, flowRefsByMessage.get(versionedKey) || [], {
|
|
608
|
+
triggers,
|
|
609
|
+
triggeredBy,
|
|
610
|
+
}),
|
|
593
611
|
message,
|
|
594
612
|
adrs
|
|
595
613
|
);
|
|
@@ -20,7 +20,7 @@ export const getCommands = async ({ getAllVersions = true, hydrateServices = tru
|
|
|
20
20
|
const cacheKey = `${getAllVersions ? 'allVersions' : 'currentVersions'}-${hydrateServices ? 'hydrated' : 'minimal'}`;
|
|
21
21
|
|
|
22
22
|
// Check cache
|
|
23
|
-
if (memoryCache[cacheKey] && memoryCache[cacheKey].length > 0) {
|
|
23
|
+
if (memoryCache[cacheKey] && memoryCache[cacheKey].length > 0 && CACHE_ENABLED) {
|
|
24
24
|
// console.timeEnd('✅ New getCommands');
|
|
25
25
|
return memoryCache[cacheKey];
|
|
26
26
|
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { CollectionEntry } from 'astro:content';
|
|
2
|
+
import type { CollectionMessageTypes } from '@types';
|
|
3
|
+
import { getItemsFromCollectionByIdAndSemverOrLatest, versionMatches } from './util';
|
|
4
|
+
|
|
5
|
+
type Message = CollectionEntry<CollectionMessageTypes>;
|
|
6
|
+
export type MessageReceiver = CollectionEntry<'services'> | CollectionEntry<'domains'> | CollectionEntry<'agents'>;
|
|
7
|
+
|
|
8
|
+
interface TriggerPointer {
|
|
9
|
+
id: string;
|
|
10
|
+
version?: string;
|
|
11
|
+
condition?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface ReceivePointer {
|
|
15
|
+
id: string;
|
|
16
|
+
version?: string;
|
|
17
|
+
triggers?: TriggerPointer[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface MessageTrigger {
|
|
21
|
+
receiver: MessageReceiver;
|
|
22
|
+
message: Message;
|
|
23
|
+
condition?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const messageMatchesPointer = (message: Message, pointer: { id: string; version?: string }) => {
|
|
27
|
+
if (pointer.id !== message.data.id) return false;
|
|
28
|
+
|
|
29
|
+
if (!pointer.version || pointer.version === 'latest') {
|
|
30
|
+
return !message.data.latestVersion || message.data.version === message.data.latestVersion;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return versionMatches(message.data.version, pointer.version);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Returns the messages triggered when a resource receives the given message.
|
|
38
|
+
*
|
|
39
|
+
* Receivers must be raw collection entries because hydration replaces receive
|
|
40
|
+
* pointers with messages and therefore removes trigger metadata.
|
|
41
|
+
*/
|
|
42
|
+
export const getTriggersOfMessage = (
|
|
43
|
+
receivers: MessageReceiver[],
|
|
44
|
+
message: Message,
|
|
45
|
+
allMessages: Message[]
|
|
46
|
+
): MessageTrigger[] => {
|
|
47
|
+
const triggers: MessageTrigger[] = [];
|
|
48
|
+
|
|
49
|
+
for (const receiver of receivers) {
|
|
50
|
+
const receives = (receiver.data.receives as ReceivePointer[]) ?? [];
|
|
51
|
+
|
|
52
|
+
for (const receive of receives) {
|
|
53
|
+
if (!receive.triggers?.length || !messageMatchesPointer(message, receive)) continue;
|
|
54
|
+
|
|
55
|
+
for (const trigger of receive.triggers) {
|
|
56
|
+
const target = getItemsFromCollectionByIdAndSemverOrLatest(allMessages, trigger.id, trigger.version)[0];
|
|
57
|
+
if (!target) continue;
|
|
58
|
+
|
|
59
|
+
triggers.push({ receiver, message: target, condition: trigger.condition });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return triggers;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Returns the messages whose handling can trigger the given message.
|
|
69
|
+
*
|
|
70
|
+
* Receivers must be raw collection entries because hydration replaces receive
|
|
71
|
+
* pointers with messages and therefore removes trigger metadata.
|
|
72
|
+
*/
|
|
73
|
+
export const getTriggeredByOfMessage = (
|
|
74
|
+
receivers: MessageReceiver[],
|
|
75
|
+
message: Message,
|
|
76
|
+
allMessages: Message[]
|
|
77
|
+
): MessageTrigger[] => {
|
|
78
|
+
const triggeredBy: MessageTrigger[] = [];
|
|
79
|
+
|
|
80
|
+
for (const receiver of receivers) {
|
|
81
|
+
const receives = (receiver.data.receives as ReceivePointer[]) ?? [];
|
|
82
|
+
|
|
83
|
+
for (const receive of receives) {
|
|
84
|
+
for (const trigger of receive.triggers ?? []) {
|
|
85
|
+
if (!messageMatchesPointer(message, trigger)) continue;
|
|
86
|
+
|
|
87
|
+
const source = getItemsFromCollectionByIdAndSemverOrLatest(allMessages, receive.id, receive.version)[0];
|
|
88
|
+
if (!source) continue;
|
|
89
|
+
|
|
90
|
+
triggeredBy.push({ receiver, message: source, condition: trigger.condition });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return triggeredBy;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const getMessageKey = (message: Message) => `${message.collection}:${message.data.id}:${message.data.version}`;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Returns only messages that participate in at least one resolved trigger path.
|
|
102
|
+
* Both the source and target are included so empty trigger pages are never generated.
|
|
103
|
+
*/
|
|
104
|
+
export const getMessagesWithTriggerPaths = (receivers: MessageReceiver[], allMessages: Message[]): Message[] => {
|
|
105
|
+
const messageKeysWithPaths = new Set<string>();
|
|
106
|
+
|
|
107
|
+
for (const source of allMessages) {
|
|
108
|
+
const triggers = getTriggersOfMessage(receivers, source, allMessages);
|
|
109
|
+
if (triggers.length === 0) continue;
|
|
110
|
+
|
|
111
|
+
messageKeysWithPaths.add(getMessageKey(source));
|
|
112
|
+
for (const trigger of triggers) {
|
|
113
|
+
messageKeysWithPaths.add(getMessageKey(trigger.message));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return allMessages.filter((message) => messageKeysWithPaths.has(getMessageKey(message)));
|
|
118
|
+
};
|
|
@@ -143,11 +143,12 @@ export const getNodesAndEdges = async ({
|
|
|
143
143
|
targetPosition: 'left',
|
|
144
144
|
data: {
|
|
145
145
|
mode,
|
|
146
|
+
isFocused: true,
|
|
146
147
|
data: {
|
|
147
148
|
...container.data,
|
|
148
149
|
},
|
|
149
150
|
contextMenu: buildContextMenuForResource({
|
|
150
|
-
collection: '
|
|
151
|
+
collection: 'containers',
|
|
151
152
|
id: container.data.id,
|
|
152
153
|
version: container.data.version,
|
|
153
154
|
}),
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { CollectionEntry } from 'astro:content';
|
|
2
|
+
import type { Edge, Node } from '@xyflow/react';
|
|
3
|
+
import type { CollectionMessageTypes } from '@types';
|
|
4
|
+
import type { MessageTrigger } from '@utils/collections/message-triggers';
|
|
5
|
+
import {
|
|
6
|
+
createEdge,
|
|
7
|
+
createNode,
|
|
8
|
+
generateIdForNode,
|
|
9
|
+
generatedIdForEdge,
|
|
10
|
+
getColorFromString,
|
|
11
|
+
getEdgeLabelForMessageAsSource,
|
|
12
|
+
getEdgeLabelForServiceAsTarget,
|
|
13
|
+
} from './utils/utils';
|
|
14
|
+
import { createTriggerMessageNode, getTriggerReceiverNodeData } from './message-node-graph';
|
|
15
|
+
|
|
16
|
+
type Message = CollectionEntry<CollectionMessageTypes>;
|
|
17
|
+
|
|
18
|
+
export type MessageLineageDirection = 'triggeredBy' | 'triggers';
|
|
19
|
+
|
|
20
|
+
export interface MessageLineageGraph {
|
|
21
|
+
direction: MessageLineageDirection;
|
|
22
|
+
condition?: string;
|
|
23
|
+
source: Message;
|
|
24
|
+
receiver: MessageTrigger['receiver'];
|
|
25
|
+
target: Message;
|
|
26
|
+
relatedMessage: Message;
|
|
27
|
+
nodes: Node[];
|
|
28
|
+
edges: Edge[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface MessageLineageScenario {
|
|
32
|
+
condition?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface MessageLineageGroup extends Omit<MessageLineageGraph, 'condition'> {
|
|
36
|
+
scenarios: MessageLineageScenario[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const createLineageEdge = (source: any, target: any, label: string, colorMessage: Message): Edge =>
|
|
40
|
+
createEdge({
|
|
41
|
+
id: generatedIdForEdge(source, target),
|
|
42
|
+
source: generateIdForNode(source),
|
|
43
|
+
target: generateIdForNode(target),
|
|
44
|
+
label,
|
|
45
|
+
animated: false,
|
|
46
|
+
data: {
|
|
47
|
+
animated: false,
|
|
48
|
+
customColor: getColorFromString(colorMessage.data.id),
|
|
49
|
+
rootSourceAndTarget: {
|
|
50
|
+
source: { id: generateIdForNode(source), collection: source.collection },
|
|
51
|
+
target: { id: generateIdForNode(target), collection: target.collection },
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export const buildMessageLineageGraph = ({
|
|
57
|
+
currentMessage,
|
|
58
|
+
relation,
|
|
59
|
+
direction,
|
|
60
|
+
}: {
|
|
61
|
+
currentMessage: Message;
|
|
62
|
+
relation: MessageTrigger;
|
|
63
|
+
direction: MessageLineageDirection;
|
|
64
|
+
}): MessageLineageGraph => {
|
|
65
|
+
const source = direction === 'triggeredBy' ? relation.message : currentMessage;
|
|
66
|
+
const target = direction === 'triggeredBy' ? currentMessage : relation.message;
|
|
67
|
+
const mode = 'simple';
|
|
68
|
+
|
|
69
|
+
const nodes: Node[] = [
|
|
70
|
+
{
|
|
71
|
+
...createTriggerMessageNode(source, mode, source === currentMessage),
|
|
72
|
+
position: { x: 0, y: 0 },
|
|
73
|
+
},
|
|
74
|
+
createNode({
|
|
75
|
+
id: generateIdForNode(relation.receiver),
|
|
76
|
+
type: relation.receiver.collection,
|
|
77
|
+
data: getTriggerReceiverNodeData(relation.receiver, mode),
|
|
78
|
+
position: { x: 320, y: 0 },
|
|
79
|
+
}),
|
|
80
|
+
{
|
|
81
|
+
...createTriggerMessageNode(target, mode, target === currentMessage),
|
|
82
|
+
position: { x: 640, y: 0 },
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
direction,
|
|
88
|
+
condition: relation.condition,
|
|
89
|
+
source,
|
|
90
|
+
receiver: relation.receiver,
|
|
91
|
+
target,
|
|
92
|
+
relatedMessage: relation.message,
|
|
93
|
+
nodes,
|
|
94
|
+
edges: [
|
|
95
|
+
createLineageEdge(source, relation.receiver, getEdgeLabelForMessageAsSource(source), source),
|
|
96
|
+
createLineageEdge(relation.receiver, target, getEdgeLabelForServiceAsTarget(target), target),
|
|
97
|
+
],
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const getResourceKey = (resource: Message | MessageTrigger['receiver']) =>
|
|
102
|
+
`${resource.collection}:${resource.data.id}:${resource.data.version}`;
|
|
103
|
+
|
|
104
|
+
export const groupMessageLineageGraphs = (graphs: MessageLineageGraph[]): MessageLineageGroup[] => {
|
|
105
|
+
const groups = new Map<string, MessageLineageGroup>();
|
|
106
|
+
|
|
107
|
+
for (const graph of graphs) {
|
|
108
|
+
const key = [
|
|
109
|
+
graph.direction,
|
|
110
|
+
getResourceKey(graph.source),
|
|
111
|
+
getResourceKey(graph.receiver),
|
|
112
|
+
getResourceKey(graph.target),
|
|
113
|
+
].join('>');
|
|
114
|
+
const existingGroup = groups.get(key);
|
|
115
|
+
|
|
116
|
+
if (existingGroup) {
|
|
117
|
+
if (!existingGroup.scenarios.some((scenario) => scenario.condition === graph.condition)) {
|
|
118
|
+
existingGroup.scenarios.push({ condition: graph.condition });
|
|
119
|
+
}
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const { condition, ...lineage } = graph;
|
|
124
|
+
groups.set(key, {
|
|
125
|
+
...lineage,
|
|
126
|
+
scenarios: [{ condition }],
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return [...groups.values()];
|
|
131
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// import { getColor } from '@utils/colors';
|
|
2
2
|
import { getEvents } from '@utils/collections/events';
|
|
3
|
-
import type
|
|
3
|
+
import { getCollection, type CollectionEntry } from 'astro:content';
|
|
4
4
|
import dagre from 'dagre';
|
|
5
5
|
import {
|
|
6
6
|
calculatedNodes,
|
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
import { getNodesAndEdgesForChannelChain } from './channel-node-graph';
|
|
43
43
|
import { getChannelChain, isChannelsConnected } from '@utils/collections/channels';
|
|
44
44
|
import { getChannels } from '@utils/collections/channels';
|
|
45
|
+
import { getTriggeredByOfMessage, getTriggersOfMessage, type MessageReceiver } from '@utils/collections/message-triggers';
|
|
45
46
|
|
|
46
47
|
type DagreGraph = any;
|
|
47
48
|
type RoutableResource = CollectionEntry<'agents'> | CollectionEntry<'services'>;
|
|
@@ -166,6 +167,52 @@ const getDataProductNodeData = (resource: CollectionEntry<'data-products'>, mode
|
|
|
166
167
|
contextMenu: buildContextMenuForResource({ collection: 'data-products', id: resource.data.id, version: resource.data.version }),
|
|
167
168
|
});
|
|
168
169
|
|
|
170
|
+
export const getTriggerReceiverNodeData = (receiver: MessageReceiver, mode: 'simple' | 'full') => {
|
|
171
|
+
if (receiver.collection !== 'domains') return getRoutableNodeData(receiver, mode);
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
title: receiver.data.id,
|
|
175
|
+
mode,
|
|
176
|
+
domain: {
|
|
177
|
+
...receiver,
|
|
178
|
+
data: {
|
|
179
|
+
...receiver.data,
|
|
180
|
+
// Raw domain entries contain service pointers, while the visualiser's
|
|
181
|
+
// domain node expects hydrated services in full mode.
|
|
182
|
+
services: [],
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
contextMenu: buildContextMenuForResource({
|
|
186
|
+
collection: 'domains',
|
|
187
|
+
id: receiver.data.id,
|
|
188
|
+
version: receiver.data.version,
|
|
189
|
+
}),
|
|
190
|
+
};
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
export const createTriggerMessageNode = (
|
|
194
|
+
message: CollectionEntry<CollectionMessageTypes>,
|
|
195
|
+
mode: 'simple' | 'full',
|
|
196
|
+
isFocused = false
|
|
197
|
+
) =>
|
|
198
|
+
createNode({
|
|
199
|
+
id: generateIdForNode(message),
|
|
200
|
+
type: message.collection,
|
|
201
|
+
data: {
|
|
202
|
+
mode,
|
|
203
|
+
...(isFocused ? { isFocused: true } : {}),
|
|
204
|
+
message: { ...message.data, ...getOperationFields(message.data) },
|
|
205
|
+
contextMenu: buildContextMenuForMessage({
|
|
206
|
+
id: message.data.id,
|
|
207
|
+
version: message.data.version,
|
|
208
|
+
name: message.data.name,
|
|
209
|
+
collection: message.collection,
|
|
210
|
+
schemaPath: (message.data as any).schemaPath,
|
|
211
|
+
}),
|
|
212
|
+
},
|
|
213
|
+
position: { x: 0, y: 0 },
|
|
214
|
+
});
|
|
215
|
+
|
|
169
216
|
const getNodesAndEdges = async ({
|
|
170
217
|
id,
|
|
171
218
|
version,
|
|
@@ -194,6 +241,19 @@ const getNodesAndEdges = async ({
|
|
|
194
241
|
// Pre-calculate channel map for O(1) lookups
|
|
195
242
|
const channelMap = createVersionedMap(channels);
|
|
196
243
|
|
|
244
|
+
// Trigger metadata lives on raw receive pointers. Hydrated resources replace
|
|
245
|
+
// those pointers with messages, so load the raw collections for this relation.
|
|
246
|
+
const [events, commands, queries, services, agents, domains] = await Promise.all([
|
|
247
|
+
getCollection('events'),
|
|
248
|
+
getCollection('commands'),
|
|
249
|
+
getCollection('queries'),
|
|
250
|
+
getCollection('services'),
|
|
251
|
+
getCollection('agents'),
|
|
252
|
+
getCollection('domains'),
|
|
253
|
+
]);
|
|
254
|
+
const allMessages = [...events, ...commands, ...queries] as CollectionEntry<CollectionMessageTypes>[];
|
|
255
|
+
const messageReceivers = [...services, ...agents, ...domains] as MessageReceiver[];
|
|
256
|
+
|
|
197
257
|
// We always render the message itself
|
|
198
258
|
nodes.push({
|
|
199
259
|
id: generateIdForNode(message),
|
|
@@ -201,6 +261,7 @@ const getNodesAndEdges = async ({
|
|
|
201
261
|
targetPosition: 'left',
|
|
202
262
|
data: {
|
|
203
263
|
mode,
|
|
264
|
+
isFocused: true,
|
|
204
265
|
message: {
|
|
205
266
|
...message.data,
|
|
206
267
|
...getOperationFields(message.data),
|
|
@@ -564,6 +625,63 @@ const getNodesAndEdges = async ({
|
|
|
564
625
|
}
|
|
565
626
|
});
|
|
566
627
|
|
|
628
|
+
const appendNode = (node: Node) => {
|
|
629
|
+
if (!nodes.some((existingNode: Node) => existingNode.id === node.id)) nodes.push(node);
|
|
630
|
+
};
|
|
631
|
+
const appendEdge = (source: any, target: any, label: string, colorMessage: CollectionEntry<CollectionMessageTypes>) => {
|
|
632
|
+
const id = generatedIdForEdge(source, target);
|
|
633
|
+
if (edges.some((edge: Edge) => edge.id === id)) return;
|
|
634
|
+
|
|
635
|
+
edges.push(
|
|
636
|
+
createEdge({
|
|
637
|
+
id,
|
|
638
|
+
source: generateIdForNode(source),
|
|
639
|
+
target: generateIdForNode(target),
|
|
640
|
+
label,
|
|
641
|
+
data: {
|
|
642
|
+
customColor: getColorFromString(colorMessage.data.id),
|
|
643
|
+
rootSourceAndTarget: {
|
|
644
|
+
source: { id: generateIdForNode(source), collection: source.collection },
|
|
645
|
+
target: { id: generateIdForNode(target), collection: target.collection },
|
|
646
|
+
},
|
|
647
|
+
},
|
|
648
|
+
})
|
|
649
|
+
);
|
|
650
|
+
};
|
|
651
|
+
const appendReceiver = (receiver: MessageReceiver) => {
|
|
652
|
+
const receiverId = generateIdForNode(receiver);
|
|
653
|
+
if (nodes.some((node: Node) => node.id === receiverId)) return;
|
|
654
|
+
|
|
655
|
+
appendNode(
|
|
656
|
+
createNode({
|
|
657
|
+
id: receiverId,
|
|
658
|
+
type: receiver.collection,
|
|
659
|
+
data: getTriggerReceiverNodeData(receiver, mode),
|
|
660
|
+
position: { x: 0, y: 0 },
|
|
661
|
+
})
|
|
662
|
+
);
|
|
663
|
+
|
|
664
|
+
if (receiver.collection !== 'domains') {
|
|
665
|
+
appendAgentToolNodesAndEdges({ agent: receiver, nodes, edges, mode });
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
// Complete both sides of a trigger chain around the focused message:
|
|
670
|
+
// triggering message -> receiver -> triggered message.
|
|
671
|
+
for (const relation of getTriggeredByOfMessage(messageReceivers, message, allMessages)) {
|
|
672
|
+
appendNode(createTriggerMessageNode(relation.message, mode));
|
|
673
|
+
appendReceiver(relation.receiver);
|
|
674
|
+
appendEdge(relation.message, relation.receiver, getEdgeLabelForMessageAsSource(relation.message), relation.message);
|
|
675
|
+
appendEdge(relation.receiver, message, getEdgeLabelForServiceAsTarget(message), message);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
for (const relation of getTriggersOfMessage(messageReceivers, message, allMessages)) {
|
|
679
|
+
appendReceiver(relation.receiver);
|
|
680
|
+
appendNode(createTriggerMessageNode(relation.message, mode));
|
|
681
|
+
appendEdge(message, relation.receiver, getEdgeLabelForMessageAsSource(message), message);
|
|
682
|
+
appendEdge(relation.receiver, relation.message, getEdgeLabelForServiceAsTarget(relation.message), relation.message);
|
|
683
|
+
}
|
|
684
|
+
|
|
567
685
|
nodes.forEach((node: any) => {
|
|
568
686
|
flow.setNode(node.id, { width: DEFAULT_NODE_WIDTH, height: DEFAULT_NODE_HEIGHT });
|
|
569
687
|
});
|
|
@@ -347,7 +347,10 @@ export const getNodesAndEdges = async ({
|
|
|
347
347
|
id: generateIdForNode(service),
|
|
348
348
|
sourcePosition: 'right',
|
|
349
349
|
targetPosition: 'left',
|
|
350
|
-
data:
|
|
350
|
+
data: {
|
|
351
|
+
...getResourceNodeData(service, mode),
|
|
352
|
+
isFocused: service.collection === 'services',
|
|
353
|
+
},
|
|
351
354
|
type: service.collection,
|
|
352
355
|
});
|
|
353
356
|
|
|
@@ -606,6 +609,17 @@ export const getNodesAndEdges = async ({
|
|
|
606
609
|
uniqueNodesById.set(node.id, node);
|
|
607
610
|
}
|
|
608
611
|
});
|
|
612
|
+
|
|
613
|
+
// Inbound message processing may create the service node before the primary
|
|
614
|
+
// node is added. Mark the surviving copy after deduplication so the service
|
|
615
|
+
// currently being viewed always retains its focus treatment.
|
|
616
|
+
if (service.collection === 'services') {
|
|
617
|
+
const focusedServiceNode = uniqueNodesById.get(generateIdForNode(service));
|
|
618
|
+
if (focusedServiceNode) {
|
|
619
|
+
focusedServiceNode.data = { ...focusedServiceNode.data, isFocused: true };
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
609
623
|
const uniqueNodes = Array.from(uniqueNodesById.values());
|
|
610
624
|
|
|
611
625
|
uniqueNodes.forEach((node: any) => {
|
|
@@ -162,7 +162,10 @@ export const buildContextMenuForMessage = ({
|
|
|
162
162
|
collection: string;
|
|
163
163
|
schemaPath?: string;
|
|
164
164
|
}): ContextMenuItem[] => {
|
|
165
|
-
const items: ContextMenuItem[] = [
|
|
165
|
+
const items: ContextMenuItem[] = [
|
|
166
|
+
{ label: 'Read documentation', href: buildUrl(`/docs/${collection}/${id}/${version}`) },
|
|
167
|
+
{ label: 'Focus node', href: buildUrl(`/visualiser/${collection}/${id}/${version}`) },
|
|
168
|
+
];
|
|
166
169
|
|
|
167
170
|
if (schemaPath) {
|
|
168
171
|
items.push({
|
|
@@ -250,7 +253,10 @@ export const buildContextMenuForService = ({
|
|
|
250
253
|
specifications?: unknown;
|
|
251
254
|
repository?: { url: string };
|
|
252
255
|
}): ContextMenuItem[] => {
|
|
253
|
-
const items: ContextMenuItem[] = [
|
|
256
|
+
const items: ContextMenuItem[] = [
|
|
257
|
+
{ label: 'Read documentation', href: buildUrl(`/docs/services/${id}/${version}`) },
|
|
258
|
+
{ label: 'Focus node', href: buildUrl(`/visualiser/services/${id}/${version}`) },
|
|
259
|
+
];
|
|
254
260
|
|
|
255
261
|
const specItems = getSpecMenuItems(specifications, id, version);
|
|
256
262
|
items.push(...specItems);
|
|
@@ -283,7 +289,10 @@ export const buildContextMenuForAgent = ({
|
|
|
283
289
|
version: string;
|
|
284
290
|
repository?: { url: string };
|
|
285
291
|
}): ContextMenuItem[] => {
|
|
286
|
-
const items: ContextMenuItem[] = [
|
|
292
|
+
const items: ContextMenuItem[] = [
|
|
293
|
+
{ label: 'Read documentation', href: buildUrl(`/docs/agents/${id}/${version}`) },
|
|
294
|
+
{ label: 'Focus node', href: buildUrl(`/visualiser/agents/${id}/${version}`) },
|
|
295
|
+
];
|
|
287
296
|
|
|
288
297
|
if (repository?.url) {
|
|
289
298
|
items.push({
|
|
@@ -315,6 +324,7 @@ export const buildContextMenuForResource = ({
|
|
|
315
324
|
}): ContextMenuItem[] => {
|
|
316
325
|
return [
|
|
317
326
|
{ label: 'Read documentation', href: buildUrl(`/docs/${collection}/${id}/${version}`) },
|
|
327
|
+
{ label: 'Focus node', href: buildUrl(`/visualiser/${collection}/${id}/${version}`) },
|
|
318
328
|
{
|
|
319
329
|
label: 'Read changelog',
|
|
320
330
|
href: buildUrl(`/docs/${collection}/${id}/${version}/changelog`),
|
|
@@ -331,7 +341,7 @@ export const buildContextMenuForResource = ({
|
|
|
331
341
|
export const buildContextMenuForSystem = ({ id, version }: { id: string; version: string }): ContextMenuItem[] => {
|
|
332
342
|
return [
|
|
333
343
|
{ label: 'Read documentation', href: buildUrl(`/docs/systems/${id}/${version}`) },
|
|
334
|
-
{ label: '
|
|
344
|
+
{ label: 'Focus node', href: buildUrl(`/visualiser/systems/${id}/${version}`) },
|
|
335
345
|
{ label: 'View context', href: buildUrl(`/visualiser/systems/${id}/${version}/context`) },
|
|
336
346
|
];
|
|
337
347
|
};
|
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.2.0",
|
|
11
11
|
"publishConfig": {
|
|
12
12
|
"access": "public"
|
|
13
13
|
},
|
|
@@ -116,9 +116,9 @@
|
|
|
116
116
|
"update-notifier": "^7.3.1",
|
|
117
117
|
"uuid": "^10.0.0",
|
|
118
118
|
"zod": "^4.3.6",
|
|
119
|
-
"@eventcatalog/linter": "1.1.
|
|
120
|
-
"@eventcatalog/sdk": "2.
|
|
121
|
-
"@eventcatalog/visualiser": "^4.0
|
|
119
|
+
"@eventcatalog/linter": "1.1.5",
|
|
120
|
+
"@eventcatalog/sdk": "2.26.0",
|
|
121
|
+
"@eventcatalog/visualiser": "^4.1.0"
|
|
122
122
|
},
|
|
123
123
|
"devDependencies": {
|
|
124
124
|
"@astrojs/check": "^0.9.9",
|