Skip to content

Commit d71928a

Browse files
authored
Saga timeouts biderectional navigation (#2407)
1 parent cad39d7 commit d71928a

File tree

6 files changed

+141
-27
lines changed

6 files changed

+141
-27
lines changed

src/Frontend/src/components/messages2/SagaDiagram.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,7 @@ function rendercomponent({ initialState = {} }: { initialState?: { MessageStore?
255255
],
256256
stubs: {
257257
CodeEditor: true,
258+
CopyToClipboard: true,
258259
},
259260
},
260261
});

src/Frontend/src/components/messages2/SagaDiagram/SagaDiagramParser.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ export interface InitiatingMessageViewModel {
1717
FormattedMessageTimestamp: string;
1818
IsEventMessage: boolean;
1919
MessageData: SagaMessageDataItem[];
20+
HasRelatedTimeoutRequest?: boolean;
21+
MessageId: string;
2022
}
2123
export interface SagaTimeoutMessageViewModel extends SagaMessageViewModel {
2224
TimeoutFriendly: string;
@@ -57,6 +59,18 @@ export interface SagaViewModel {
5759
export function parseSagaUpdates(sagaHistory: SagaHistory | null, messagesData: SagaMessageData[]): SagaUpdateViewModel[] {
5860
if (!sagaHistory || !sagaHistory.changes || !sagaHistory.changes.length) return [];
5961

62+
const timeoutMessageIds = new Set<string>();
63+
sagaHistory.changes.forEach((update) => {
64+
if (update.outgoing_messages) {
65+
update.outgoing_messages.forEach((msg) => {
66+
const delivery_delay = msg.delivery_delay || "00:00:00";
67+
if (delivery_delay && delivery_delay !== "00:00:00") {
68+
timeoutMessageIds.add(msg.message_id);
69+
}
70+
});
71+
}
72+
});
73+
6074
const updates = sagaHistory.changes
6175
.map((update) => {
6276
const startTime = new Date(update.start_time);
@@ -107,6 +121,9 @@ export function parseSagaUpdates(sagaHistory: SagaHistory | null, messagesData:
107121

108122
const hasTimeout = outgoingTimeoutMessages.length > 0;
109123

124+
// Check if initiating message is a timeout and if so, if it has a corresponding request in the diagram
125+
const hasRelatedTimeoutRequest = update.initiating_message?.is_saga_timeout_message && timeoutMessageIds.has(update.initiating_message?.message_id);
126+
110127
return <SagaUpdateViewModel>{
111128
MessageId: update.initiating_message?.message_id || "",
112129
StartTime: startTime,
@@ -115,11 +132,13 @@ export function parseSagaUpdates(sagaHistory: SagaHistory | null, messagesData:
115132
Status: update.status,
116133
StatusDisplay: update.status === "new" ? "Saga Initiated" : "Saga Updated",
117134
InitiatingMessage: <InitiatingMessageViewModel>{
135+
MessageId: update.initiating_message?.message_id || "",
118136
MessageType: typeToName(update.initiating_message?.message_type || "Unknown Message") || "",
119137
FormattedMessageTimestamp: `${initiatingMessageTimestamp.toLocaleDateString()} ${initiatingMessageTimestamp.toLocaleTimeString()}`,
120138
MessageData: initiatingMessageData,
121139
IsEventMessage: update.initiating_message?.intent === "Publish",
122140
IsSagaTimeoutMessage: update.initiating_message?.is_saga_timeout_message || false,
141+
HasRelatedTimeoutRequest: hasRelatedTimeoutRequest,
123142
},
124143
HasTimeout: hasTimeout,
125144
IsFirstNode: update.status === "new",

src/Frontend/src/components/messages2/SagaDiagram/SagaOutgoingMessage.vue

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,29 @@ import MessageDataBox from "./MessageDataBox.vue";
33
import CommandIcon from "@/assets/command.svg";
44
import EventIcon from "@/assets/event.svg";
55
import { SagaMessageViewModel } from "./SagaDiagramParser";
6+
import { useSagaDiagramStore } from "@/stores/SagaDiagramStore";
7+
import { computed } from "vue";
68
7-
defineProps<{
9+
const shouldBeActive = computed(() => {
10+
return store.selectedMessageId === props.message.MessageId;
11+
});
12+
13+
const store = useSagaDiagramStore();
14+
15+
const props = defineProps<{
816
message: SagaMessageViewModel;
917
showMessageData?: boolean;
1018
}>();
1119
</script>
1220

1321
<template>
14-
<div class="cell-inner cell-inner-side">
22+
<div
23+
:class="{
24+
'cell-inner': true,
25+
'cell-inner-side': true,
26+
'cell-inner-side--active': shouldBeActive,
27+
}"
28+
>
1529
<img class="saga-icon saga-icon--side-cell" :src="message.IsEventMessage ? EventIcon : CommandIcon" :alt="message.IsEventMessage ? 'Event' : 'Command'" />
1630
<h2 class="message-title">{{ message.MessageFriendlyTypeName }}</h2>
1731
<div class="timestamp">{{ message.FormattedTimeSent }}</div>
@@ -63,10 +77,6 @@ defineProps<{
6377
margin-left: 1rem;
6478
}
6579
66-
.cell-inner-side--active {
67-
border: solid 2px #000000;
68-
}
69-
7080
.cell-inner-right {
7181
position: relative;
7282
min-height: 2.5rem;
@@ -145,8 +155,10 @@ defineProps<{
145155
}
146156
147157
.cell-inner-side--active {
148-
border: solid 2px #000000;
158+
border: solid 5px #0b6eef;
159+
animation: blink-border 1.8s ease-in-out;
149160
}
161+
150162
.saga-icon {
151163
display: block;
152164
float: left;
@@ -158,4 +170,18 @@ defineProps<{
158170
height: 2rem;
159171
padding: 0.23rem;
160172
}
173+
@keyframes blink-border {
174+
0%,
175+
100% {
176+
border-color: #0b6eef;
177+
}
178+
20%,
179+
60% {
180+
border-color: #cccccc;
181+
}
182+
40%,
183+
80% {
184+
border-color: #0b6eef;
185+
}
186+
}
161187
</style>

src/Frontend/src/components/messages2/SagaDiagram/SagaOutgoingTimeoutMessage.vue

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import MessageDataBox from "./MessageDataBox.vue";
44
import TimeoutIcon from "@/assets/TimeoutIcon.svg";
55
import SagaTimeoutIcon from "@/assets/SagaTimeoutIcon.svg";
66
import { useSagaDiagramStore } from "@/stores/SagaDiagramStore";
7+
import { computed, ref, watch } from "vue";
78
89
const props = defineProps<{
910
message: SagaTimeoutMessageViewModel;
@@ -12,11 +13,32 @@ const props = defineProps<{
1213
}>();
1314
1415
const store = useSagaDiagramStore();
16+
const timeoutMessageRef = ref<HTMLElement | null>(null);
17+
const shouldBeActive = computed(() => {
18+
return store.selectedMessageId === props.message.MessageId;
19+
});
1520
21+
// This sets the store with the required values so the timeout invocation node exists, it will react by scrolling to the node
1622
const navigateToTimeout = () => {
1723
// Set the selected message ID in the store
1824
store.setSelectedMessageId(props.message.MessageId);
25+
store.scrollToTimeout = true;
1926
};
27+
28+
watch(
29+
[() => store.scrollToTimeoutRequest, () => shouldBeActive.value, () => timeoutMessageRef.value !== null],
30+
([scrollRequest, shouldScroll, refExists]) => {
31+
if (scrollRequest && shouldScroll && refExists && timeoutMessageRef.value) {
32+
timeoutMessageRef.value.scrollIntoView({
33+
behavior: "smooth",
34+
block: "center",
35+
});
36+
37+
store.scrollToTimeoutRequest = false;
38+
}
39+
},
40+
{ immediate: true }
41+
);
2042
</script>
2143

2244
<template>
@@ -35,7 +57,14 @@ const navigateToTimeout = () => {
3557
</div>
3658
<div class="cell cell--side">
3759
<div class="cell-inner cell-inner-right"></div>
38-
<div class="cell-inner cell-inner-side">
60+
<div
61+
ref="timeoutMessageRef"
62+
:class="{
63+
'cell-inner': true,
64+
'cell-inner-side': true,
65+
'cell-inner-side--active': shouldBeActive,
66+
}"
67+
>
3968
<img class="saga-icon saga-icon--side-cell" :src="TimeoutIcon" alt="" />
4069
<h2 class="message-title" aria-label="timeout message type">{{ message.MessageFriendlyTypeName }}</h2>
4170
<div class="timestamp" aria-label="timeout message timestamp">{{ message.FormattedTimeSent }}</div>
@@ -96,7 +125,8 @@ const navigateToTimeout = () => {
96125
}
97126
98127
.cell-inner-side--active {
99-
border: solid 2px #000000;
128+
border: solid 5px #0b6eef;
129+
animation: blink-border 1.8s ease-in-out;
100130
}
101131
102132
.cell-inner-right {
@@ -176,4 +206,19 @@ const navigateToTimeout = () => {
176206
.saga-icon--overlap {
177207
margin-left: -1rem;
178208
}
209+
210+
@keyframes blink-border {
211+
0%,
212+
100% {
213+
border-color: #0b6eef;
214+
}
215+
20%,
216+
60% {
217+
border-color: #cccccc;
218+
}
219+
40%,
220+
80% {
221+
border-color: #0b6eef;
222+
}
223+
}
179224
</style>

src/Frontend/src/components/messages2/SagaDiagram/SagaUpdateNode.vue

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -49,27 +49,30 @@ const props = defineProps<{
4949
5050
const store = useSagaDiagramStore();
5151
const initiatingMessageRef = ref<HTMLElement | null>(null);
52-
const isActive = ref(false);
5352
const hasParsingError = ref(false);
5453
55-
// Watch for changes to selectedMessageId
56-
watch(
57-
() => store.selectedMessageId,
58-
(newMessageId) => {
59-
// Check if this node contains the selected message
60-
const isSelected = props.update.InitiatingMessage.IsSagaTimeoutMessage && newMessageId === props.update.MessageId;
54+
const shouldBeActive = computed(() => {
55+
return store.selectedMessageId === props.update.MessageId;
56+
});
6157
62-
// Update active state
63-
isActive.value = isSelected;
58+
const navigateToTimeoutRequest = () => {
59+
store.setSelectedMessageId(props.update.InitiatingMessage.MessageId);
60+
store.scrollToTimeoutRequest = true;
61+
};
6462
65-
// If this is the selected message, scroll to it
66-
if (isSelected && initiatingMessageRef.value) {
63+
watch(
64+
[() => store.scrollToTimeout, () => shouldBeActive.value, () => initiatingMessageRef.value !== null],
65+
([scrollTimeout, shouldScroll, refExists]) => {
66+
if (scrollTimeout && shouldScroll && refExists && initiatingMessageRef.value) {
6767
initiatingMessageRef.value.scrollIntoView({
6868
behavior: "smooth",
6969
block: "center",
7070
});
71+
72+
store.scrollToTimeout = false;
7173
}
72-
}
74+
},
75+
{ immediate: true }
7376
);
7477
7578
// Format a JSON value for display
@@ -155,7 +158,7 @@ const hasStateChanges = computed(() => {
155158
:class="{
156159
'cell-inner': true,
157160
'cell-inner-side': true,
158-
'cell-inner-side--active': isActive || (update.InitiatingMessage.IsSagaTimeoutMessage && update.MessageId === store.selectedMessageId),
161+
'cell-inner-side--active': shouldBeActive || (update.InitiatingMessage.IsSagaTimeoutMessage && update.MessageId === store.selectedMessageId),
159162
}"
160163
:data-message-id="update.InitiatingMessage.IsSagaTimeoutMessage ? update.MessageId : ''"
161164
>
@@ -168,7 +171,8 @@ const hasStateChanges = computed(() => {
168171
<div class="cell-inner cell-inner-center cell-inner--align-bottom">
169172
<template v-if="update.InitiatingMessage.IsSagaTimeoutMessage">
170173
<img class="saga-icon saga-icon--center-cell" :src="SagaTimeoutIcon" alt="" />
171-
<h2 class="saga-status-title saga-status-title--inline timeout-status" aria-label="timeout invoked">Timeout Invoked</h2>
174+
<a v-if="update.InitiatingMessage.HasRelatedTimeoutRequest" href="#" @click.prevent="navigateToTimeoutRequest" class="saga-status-title saga-status-title--inline timeout-status" aria-label="timeout invoked"> Timeout Invoked </a>
175+
<h2 v-else class="saga-status-title saga-status-title--inline timeout-status" aria-label="timeout invoked">Timeout Invoked</h2>
172176
<br />
173177
</template>
174178
<img class="saga-icon saga-icon--center-cell" :src="update.IsFirstNode ? SagaInitiatedIcon : SagaUpdatedIcon" alt="" />
@@ -312,7 +316,7 @@ const hasStateChanges = computed(() => {
312316
}
313317
314318
.cell-inner-side--active {
315-
border: solid 5px #00a3c4;
319+
border: solid 5px #0b6eef;
316320
animation: blink-border 1.8s ease-in-out;
317321
}
318322
@@ -408,7 +412,6 @@ const hasStateChanges = computed(() => {
408412
display: inline-block;
409413
font-size: 1rem;
410414
font-weight: 900;
411-
color: #00a3c4;
412415
}
413416
414417
/* Styles for DiffViewer integration */
@@ -456,15 +459,15 @@ const hasStateChanges = computed(() => {
456459
@keyframes blink-border {
457460
0%,
458461
100% {
459-
border-color: #00a3c4;
462+
border-color: #0b6eef;
460463
}
461464
20%,
462465
60% {
463466
border-color: #cccccc;
464467
}
465468
40%,
466469
80% {
467-
border-color: #00a3c4;
470+
border-color: #0b6eef;
468471
}
469472
}
470473
</style>

src/Frontend/src/stores/SagaDiagramStore.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { SagaHistory, SagaMessage } from "@/resources/SagaHistory";
44
import { useFetchFromServiceControl } from "@/composables/serviceServiceControlUrls";
55
import Message from "@/resources/Message";
66
import { parse } from "lossless-json";
7+
import { useMessageStore } from "@/stores/MessageStore";
78

89
const StandardKeys = ["$type", "Id", "Originator", "OriginalMessageId"];
910
export interface SagaMessageDataItem {
@@ -24,8 +25,24 @@ export const useSagaDiagramStore = defineStore("SagaDiagramStore", () => {
2425
const fetchedMessages = ref(new Set<string>());
2526
const messagesData = ref<SagaMessageData[]>([]);
2627
const selectedMessageId = ref<string | null>(null);
28+
const scrollToTimeoutRequest = ref(false);
29+
const scrollToTimeout = ref(false);
2730
const MessageBodyEndpoint = "messages/{0}/body";
2831

32+
// Get message store to watch for changes
33+
const messageStore = useMessageStore();
34+
35+
// Watch for message_id changes in the MessageStore and update selectedMessageId
36+
watch(
37+
() => messageStore.state.data.message_id,
38+
(newMessageId) => {
39+
if (newMessageId) {
40+
setSelectedMessageId(newMessageId);
41+
}
42+
},
43+
{ immediate: true }
44+
);
45+
2946
// Watch the sagaId and fetch saga history when it changes
3047
watch(sagaId, async (newSagaId) => {
3148
if (newSagaId) {
@@ -190,6 +207,7 @@ export const useSagaDiagramStore = defineStore("SagaDiagramStore", () => {
190207
fetchedMessages.value.clear();
191208
messagesData.value = [];
192209
selectedMessageId.value = null;
210+
scrollToTimeoutRequest.value = false;
193211
}
194212

195213
function formatUrl(template: string, id: string): string {
@@ -263,6 +281,8 @@ export const useSagaDiagramStore = defineStore("SagaDiagramStore", () => {
263281
showMessageData,
264282
messagesData,
265283
selectedMessageId,
284+
scrollToTimeoutRequest,
285+
scrollToTimeout,
266286
setSagaId,
267287
clearSagaHistory,
268288
toggleMessageData,

0 commit comments

Comments
 (0)