Skip to content

Commit 557e4e8

Browse files
committed
It is working
1 parent df6a848 commit 557e4e8

File tree

4 files changed

+214
-156
lines changed

4 files changed

+214
-156
lines changed

src/Frontend/src/components/messages2/FlowDiagram.vue renamed to src/Frontend/src/components/messages2/FlowDiagram/FlowDiagram.vue

Lines changed: 164 additions & 154 deletions
Original file line numberDiff line numberDiff line change
@@ -1,191 +1,201 @@
11
<script setup lang="ts">
2-
import { onMounted, ref } from "vue";
3-
import { type DefaultEdge, MarkerType, type Node, type Styles, VueFlow } from "@vue-flow/core";
4-
import TimeSince from "../TimeSince.vue";
5-
import routeLinks from "@/router/routeLinks";
6-
import Message, { MessageStatus } from "@/resources/Message";
7-
import { NServiceBusHeaders } from "@/resources/Header";
2+
import { onMounted, ref, nextTick } from "vue";
3+
import { type DefaultEdge, MarkerType, type Node, type Styles, useVueFlow, VueFlow, XYPosition } from "@vue-flow/core";
4+
import TimeSince from "../../TimeSince.vue";
5+
import routeLinks from "@/router/routeLinks.ts";
6+
import Message, { MessageIntent, MessageStatus, SagaInfo } from "@/resources/Message.ts";
7+
import { NServiceBusHeaders } from "@/resources/Header.ts";
88
import { ControlButton, Controls } from "@vue-flow/controls";
9-
import { useMessageStore } from "@/stores/MessageStore";
9+
import { useMessageStore } from "@/stores/MessageStore.ts";
1010
import LoadingSpinner from "@/components/LoadingSpinner.vue";
1111
import { storeToRefs } from "pinia";
1212
import EndpointDetails from "@/resources/EndpointDetails.ts";
1313
import { hexToCSSFilter } from "hex-to-css-filter";
1414
import TextEllipses from "@/components/TextEllipses.vue";
15+
import { useLayout } from "@/components/messages2/FlowDiagram/useLayout.ts";
1516
1617
enum MessageType {
1718
Event = "Event message",
1819
Timeout = "Timeout message",
1920
Command = "Command message",
2021
}
2122
22-
interface MappedMessage {
23-
nodeName: string;
23+
const store = useMessageStore();
24+
const { state } = storeToRefs(store);
25+
26+
async function getConversation(conversationId: string) {
27+
await store.loadConversation(conversationId);
28+
29+
return store.conversationData.data;
30+
}
31+
32+
class SagaInvocation {
2433
id: string;
34+
sagaType: string;
35+
isSagaCompleted: boolean;
36+
isSagaInitiated: boolean;
37+
38+
constructor(saga: SagaInfo, message: Message) {
39+
const sagaIdHeader = getHeaderByKey(message, NServiceBusHeaders.SagaId);
40+
const originatedSagaIdHeader = getHeaderByKey(message, NServiceBusHeaders.OriginatingSagaId);
41+
this.id = saga.saga_id;
42+
this.sagaType = saga.saga_type;
43+
this.isSagaCompleted = saga.change_status === "Completed";
44+
this.isSagaInitiated = sagaIdHeader === undefined && originatedSagaIdHeader !== undefined;
45+
}
46+
}
47+
48+
interface NodeData {
49+
label: string;
50+
timeSent: string;
2551
messageId: string;
2652
sendingEndpoint: EndpointDetails;
2753
receivingEndpoint: EndpointDetails;
28-
parentId: string;
29-
parentEndpoint: string;
30-
type: MessageType;
3154
isError: boolean;
32-
sagaName: string;
33-
link: {
34-
name: string;
35-
nodeName: string;
36-
};
37-
timeSent: string;
38-
level: number;
39-
width: number;
40-
XPos: number;
55+
sagaInvocations: SagaInvocation[];
56+
isPublished: boolean;
57+
isTimeout: boolean;
58+
isEvent: boolean;
59+
isCommand: boolean;
60+
message: Message;
61+
type: MessageType;
4162
}
4263
43-
const nodeSpacingX = 300;
44-
const nodeSpacingY = 200;
64+
class MessageNode implements Node<NodeData> {
65+
readonly id: string;
66+
readonly type: string;
67+
readonly data: NodeData;
68+
readonly position: XYPosition;
69+
readonly draggable: boolean;
70+
71+
constructor(message: Message) {
72+
this.id = message.id;
73+
this.type = "message";
74+
this.position = { x: 0, y: 0 };
75+
this.draggable = false;
76+
77+
const isPublished = message.message_intent === MessageIntent.Publish;
78+
const isTimeout = getHeaderByKey(message, NServiceBusHeaders.IsSagaTimeoutMessage)?.toLowerCase() === "true";
79+
this.data = {
80+
label: message.message_type,
81+
timeSent: message.time_sent,
82+
messageId: message.message_id,
83+
sendingEndpoint: message.sending_endpoint,
84+
receivingEndpoint: message.receiving_endpoint,
85+
isError: message.status !== MessageStatus.Successful && message.status !== MessageStatus.ResolvedSuccessfully,
86+
sagaInvocations: message.invoked_sagas?.map((saga) => new SagaInvocation(saga, message)) || [],
87+
isPublished,
88+
isTimeout,
89+
isEvent: isPublished && isTimeout,
90+
isCommand: !isPublished && isTimeout,
91+
message,
92+
type: isPublished ? MessageType.Event : isTimeout ? MessageType.Timeout : MessageType.Command,
93+
};
94+
}
95+
}
4596
46-
const store = useMessageStore();
47-
const { state } = storeToRefs(store);
97+
function constructNodes(messages: Message[]): Node<NodeData>[] {
98+
const messageMap = new Map();
4899
49-
async function getConversation(conversationId: string) {
50-
await store.loadConversation(conversationId);
100+
messages.forEach((message) => {
101+
if (!messageMap.has(message.id)) {
102+
messageMap.set(message.id, new MessageNode(message));
103+
}
104+
});
51105
52-
return store.conversationData.data;
106+
return Array.from(messageMap.values());
53107
}
54108
55-
function mapMessage(message: Message): MappedMessage {
56-
let parentId = "",
57-
parentEndpoint = "",
58-
sagaName = "";
59-
const header = message.headers.find((header) => header.key === NServiceBusHeaders.RelatedTo);
60-
if (header) {
61-
parentId = header.value ?? "";
62-
parentEndpoint = message.headers.find((h) => h.key === "NServiceBus.OriginatingEndpoint")?.value ?? "";
63-
}
109+
function getHeaderByKey(message: Message, key: NServiceBusHeaders) {
110+
return message.headers.find((header) => header.key === key)?.value;
111+
}
112+
113+
function constructEdges(nodes: Node<NodeData>[]): DefaultEdge[] {
114+
const edges: DefaultEdge[] = [];
115+
116+
for (const node of nodes) {
117+
const message = node.data?.message;
118+
if (message === undefined) continue;
119+
120+
const relatedTo = getHeaderByKey(message, NServiceBusHeaders.RelatedTo);
121+
if (!relatedTo && relatedTo !== message.message_id) {
122+
continue;
123+
}
124+
125+
let parentMessages = nodes.filter((n) => {
126+
const m = n.data?.message;
127+
if (m === undefined) return false;
128+
return m.receiving_endpoint !== undefined && m.sending_endpoint !== undefined && m.message_id === relatedTo && m.receiving_endpoint.name === message.sending_endpoint.name;
129+
});
130+
131+
if (parentMessages.length === 0) {
132+
parentMessages = nodes.filter((n) => {
133+
const m = n.data?.message;
134+
if (m === undefined) return false;
135+
return m.receiving_endpoint !== undefined && m.sending_endpoint !== undefined && m.message_id === relatedTo && m.message_intent !== MessageIntent.Publish;
136+
});
64137
65-
const sagaHeader = message.headers.find((header) => header.key === NServiceBusHeaders.OriginatingSagaType);
66-
if (sagaHeader) {
67-
sagaName = sagaHeader.value?.split(", ")[0] ?? "";
138+
if (parentMessages.length === 0) {
139+
console.log(`Fall back to match only on RelatedToMessageId for message with Id '${message.message_id}' matched but link could be invalid.`);
140+
}
141+
}
142+
143+
switch (parentMessages.length) {
144+
case 0:
145+
console.log(
146+
`No parent could be resolved for the message with Id '${message.message_id}' which has RelatedToMessageId set. This can happen if the parent has been purged due to retention expiration, an ServiceControl node to be unavailable, or because the parent message not been stored (yet).`
147+
);
148+
break;
149+
case 1:
150+
// Log nothing, this is what it should be
151+
break;
152+
default:
153+
console.log(`Multiple parents matched for message id '${message.message_id}' possibly due to more-than-once processing, linking to all as it is unknown which processing attempt generated the message.`);
154+
break;
155+
}
156+
157+
for (const parentMessage of parentMessages) {
158+
edges.push(addConnection(parentMessage, node));
159+
}
68160
}
69161
70-
const type = (() => {
71-
if (message.headers.find((header) => header.key === NServiceBusHeaders.MessageIntent)?.value === "Publish") return MessageType.Event;
72-
else if (message.headers.find((header) => header.key === NServiceBusHeaders.IsSagaTimeoutMessage)?.value?.toLowerCase() === "true") return MessageType.Timeout;
73-
return MessageType.Command;
74-
})();
162+
return edges;
163+
}
75164
165+
function addConnection(parentMessage: Node<NodeData>, childMessage: Node<NodeData>): DefaultEdge {
76166
return {
77-
nodeName: message.message_type,
78-
id: message.id,
79-
messageId: message.message_id,
80-
sendingEndpoint: message.sending_endpoint,
81-
receivingEndpoint: message.receiving_endpoint,
82-
parentId,
83-
parentEndpoint,
84-
type,
85-
isError: message.status !== MessageStatus.Successful && message.status !== MessageStatus.ResolvedSuccessfully,
86-
sagaName,
87-
level: 0,
88-
width: 0,
89-
XPos: 0,
90-
link: {
91-
name: `Link ${message.id}`,
92-
nodeName: message.id,
93-
},
94-
timeSent: message.time_sent,
167+
id: `${parentMessage.id}##${childMessage.id}`,
168+
source: `${parentMessage.id}`,
169+
target: `${childMessage.id}`,
170+
markerEnd: MarkerType.ArrowClosed,
171+
style: {
172+
"stroke-dasharray": childMessage.data?.isEvent && "5, 3",
173+
} as Styles,
95174
};
96175
}
97176
98-
function constructNodes(mappedMessages: MappedMessage[]): Node<MappedMessage>[] {
99-
return (
100-
mappedMessages
101-
//group by level
102-
.reduce((groups: MappedMessage[][], message: MappedMessage) => {
103-
groups[message.level] = [...(groups[message.level] ?? []), message];
104-
return groups;
105-
}, [])
106-
//ensure each level has their items in the same "grouped" order as the level above
107-
.map((group, level, messagesByLevel) => {
108-
const previousLevel = level > 0 ? messagesByLevel[level - 1] : null;
109-
return group.sort(
110-
(a, b) =>
111-
(previousLevel?.findIndex((plMessage) => a.parentId === plMessage.messageId && a.parentEndpoint === plMessage.receivingEndpoint.name) ?? 1) -
112-
(previousLevel?.findIndex((plMessage) => b.parentId === plMessage.messageId && b.parentEndpoint === plMessage.receivingEndpoint.name) ?? 1)
113-
);
114-
})
115-
//flatten to actual flow diagram nodes, with positioning based on parent node/level
116-
.flatMap((group, level, messagesByLevel) => {
117-
const previousLevel = level > 0 ? messagesByLevel[level - 1] : null;
118-
return group.reduce(
119-
({ result, currentWidth, previousParent }, message) => {
120-
//position on current level needs to be based on parent Node, so see if one exists
121-
const parentMessage = previousLevel?.find((plMessage) => message.parentId === plMessage.messageId && message.parentEndpoint === plMessage.receivingEndpoint.name) ?? null;
122-
//if the current parent node is the same as the previous parent node, then the current position needs to be to the right of siblings
123-
const currentParentWidth = previousParent === parentMessage ? currentWidth : 0;
124-
const startX = parentMessage == null ? 0 : parentMessage.XPos - parentMessage.width / 2;
125-
//store the position of the node against the message, so child nodes can use it to determine their start position
126-
message.XPos = startX + (currentParentWidth + message.width / 2);
127-
return {
128-
result: [
129-
...result,
130-
{
131-
id: `${message.messageId}##${message.receivingEndpoint.name}`,
132-
type: "message",
133-
data: message,
134-
label: message.nodeName,
135-
position: { x: message.XPos * nodeSpacingX, y: message.level * nodeSpacingY },
136-
},
137-
],
138-
currentWidth: currentParentWidth + message.width,
139-
previousParent: parentMessage,
140-
};
141-
},
142-
{ result: [] as Node[], currentWidth: 0, previousParent: null as MappedMessage | null }
143-
).result;
144-
})
145-
);
146-
}
147-
148-
function constructEdges(mappedMessages: MappedMessage[]): DefaultEdge[] {
149-
return mappedMessages
150-
.filter((message) => message.parentId)
151-
.map((message) => ({
152-
id: `${message.parentId}##${message.messageId}`,
153-
source: `${message.parentId}##${message.parentEndpoint}`,
154-
target: `${message.messageId}##${message.receivingEndpoint.name}`,
155-
markerEnd: MarkerType.ArrowClosed,
156-
style: {
157-
"stroke-dasharray": message.type === MessageType.Event && "5, 3",
158-
} as Styles,
159-
}));
160-
}
161-
162-
const elements = ref<(Node | DefaultEdge)[]>([]);
177+
const nodes = ref<Node[]>([]);
178+
const edges = ref<DefaultEdge[]>([]);
179+
const { layout } = useLayout();
180+
const { fitView } = useVueFlow();
163181
164182
onMounted(async () => {
165183
if (!state.value.data.conversation_id) return;
166184
167185
const messages = await getConversation(state.value.data.conversation_id);
168-
const mappedMessages = messages.map(mapMessage);
169-
170-
const assignDescendantLevelsAndWidth = (message: MappedMessage, level = 0) => {
171-
message.level = level;
172-
const children = mappedMessages.filter((mm) => mm.parentId === message.messageId && mm.parentEndpoint === message.receivingEndpoint.name);
173-
message.width =
174-
children.length === 0
175-
? 1 //leaf node
176-
: children.map((child) => (child.width === 0 ? assignDescendantLevelsAndWidth(child, level + 1) : child)).reduce((sum, { width }) => sum + width, 0);
177-
return message;
178-
};
179-
for (const root of mappedMessages.filter((message) => !message.parentId)) {
180-
assignDescendantLevelsAndWidth(root);
181-
}
182-
183-
const nodes = constructNodes(mappedMessages);
184-
const edges = constructEdges(nodes.map((n) => n.data as MappedMessage));
185186
186-
elements.value = [...nodes, ...edges];
187+
nodes.value = constructNodes(messages);
188+
edges.value = constructEdges(nodes.value);
187189
});
188190
191+
async function layoutGraph() {
192+
nodes.value = layout(nodes.value, edges.value);
193+
194+
await nextTick(() => {
195+
fitView();
196+
});
197+
}
198+
189199
function typeIcon(type: MessageType) {
190200
switch (type) {
191201
case MessageType.Timeout:
@@ -211,32 +221,32 @@ const greenColor = hexToCSSFilter("#00c468").filter;
211221
<div v-if="store.conversationData.failed_to_load" class="alert alert-info">FlowDiagram data is unavailable.</div>
212222
<LoadingSpinner v-else-if="store.conversationData.loading" />
213223
<div v-else id="tree-container">
214-
<VueFlow v-model="elements" :min-zoom="0.1" :fit-view-on-init="true" :only-render-visible-elements="true">
224+
<VueFlow :nodes="nodes" :edges="edges" :min-zoom="0.1" :fit-view-on-init="true" :only-render-visible-elements="true" @nodes-initialized="layoutGraph">
215225
<Controls position="top-left" class="controls">
216226
<ControlButton v-tippy="showAddress ? `Hide endpoints` : `Show endpoints`" @click="toggleAddress">
217227
<i class="fa pa-flow-endpoint" :style="{ filter: showAddress ? greenColor : blackColor }"></i>
218228
</ControlButton>
219229
</Controls>
220-
<template #node-message="{ data }: { data: MappedMessage }">
230+
<template #node-message="{ id, data }: { id: string; data: NodeData }">
221231
<div v-if="showAddress">
222232
<TextEllipses class="address" :text="`${data.sendingEndpoint.name}@${data.sendingEndpoint.host}`" />
223233
</div>
224-
<div class="node" :class="{ error: data.isError, 'current-message': data.id === store.state.data.id }">
234+
<div class="node" :class="{ error: data.isError, 'current-message': id === store.state.data.id }">
225235
<div class="node-text">
226236
<i v-if="data.isError" class="fa pa-flow-failed" />
227237
<i class="fa" :class="typeIcon(data.type)" v-tippy="data.type" />
228238
<div class="lead">
229239
<strong>
230-
<RouterLink v-if="data.isError" :to="{ path: routeLinks.messages.failedMessage.link(data.id) }"><TextEllipses style="width: 204px" :text="data.nodeName" ellipses-style="LeftSide" /></RouterLink>
231-
<RouterLink v-else :to="{ path: routeLinks.messages.successMessage.link(data.messageId, data.id) }"><TextEllipses style="width: 204px" :text="data.nodeName" ellipses-style="LeftSide" /></RouterLink>
240+
<RouterLink v-if="data.isError" :to="{ path: routeLinks.messages.failedMessage.link(id) }"><TextEllipses style="width: 204px" :text="data.label" ellipses-style="LeftSide" /></RouterLink>
241+
<RouterLink v-else :to="{ path: routeLinks.messages.successMessage.link(data.messageId, id) }"><TextEllipses style="width: 204px" :text="data.label" ellipses-style="LeftSide" /></RouterLink>
232242
</strong>
233243
</div>
234244
<div class="time-sent">
235245
<time-since class="time-since" :date-utc="data.timeSent" />
236246
</div>
237-
<template v-if="data.sagaName">
247+
<template v-for="saga in data.sagaInvocations" :key="saga.id">
238248
<i class="fa pa-flow-saga" />
239-
<div class="saga lead"><TextEllipses style="width: 182px" :text="data.sagaName" ellipses-style="LeftSide" /></div>
249+
<div class="saga lead"><TextEllipses style="width: 182px" :text="saga.sagaType" ellipses-style="LeftSide" /></div>
240250
</template>
241251
</div>
242252
</div>
@@ -255,7 +265,7 @@ const greenColor = hexToCSSFilter("#00c468").filter;
255265
</style>
256266

257267
<style scoped>
258-
@import "../list.css";
268+
@import "../../list.css";
259269
260270
.controls {
261271
display: flex;

0 commit comments

Comments
 (0)