Skip to content

Commit 4b130a8

Browse files
committed
timeout bi-directional navigation v1
1 parent cad39d7 commit 4b130a8

File tree

5 files changed

+173
-19
lines changed

5 files changed

+173
-19
lines changed

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: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,35 @@ 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 { ref, watch } from "vue";
8+
const isActive = ref(false);
69
7-
defineProps<{
10+
const store = useSagaDiagramStore();
11+
12+
const props = defineProps<{
813
message: SagaMessageViewModel;
914
showMessageData?: boolean;
1015
}>();
16+
17+
watch(
18+
() => store.selectedMessageId,
19+
(newMessageId) => {
20+
// Check if this node contains the selected message
21+
isActive.value = newMessageId === props.message.MessageId;
22+
},
23+
{ immediate: true }
24+
);
1125
</script>
1226

1327
<template>
14-
<div class="cell-inner cell-inner-side">
28+
<div
29+
:class="{
30+
'cell-inner': true,
31+
'cell-inner-side': true,
32+
'cell-inner-side--active': isActive,
33+
}"
34+
>
1535
<img class="saga-icon saga-icon--side-cell" :src="message.IsEventMessage ? EventIcon : CommandIcon" :alt="message.IsEventMessage ? 'Event' : 'Command'" />
1636
<h2 class="message-title">{{ message.MessageFriendlyTypeName }}</h2>
1737
<div class="timestamp">{{ message.FormattedTimeSent }}</div>
@@ -63,10 +83,6 @@ defineProps<{
6383
margin-left: 1rem;
6484
}
6585
66-
.cell-inner-side--active {
67-
border: solid 2px #000000;
68-
}
69-
7086
.cell-inner-right {
7187
position: relative;
7288
min-height: 2.5rem;
@@ -145,8 +161,10 @@ defineProps<{
145161
}
146162
147163
.cell-inner-side--active {
148-
border: solid 2px #000000;
164+
border: solid 5px #00a3c4;
165+
animation: blink-border 1.8s ease-in-out;
149166
}
167+
150168
.saga-icon {
151169
display: block;
152170
float: left;
@@ -158,4 +176,18 @@ defineProps<{
158176
height: 2rem;
159177
padding: 0.23rem;
160178
}
179+
@keyframes blink-border {
180+
0%,
181+
100% {
182+
border-color: #00a3c4;
183+
}
184+
20%,
185+
60% {
186+
border-color: #cccccc;
187+
}
188+
40%,
189+
80% {
190+
border-color: #00a3c4;
191+
}
192+
}
161193
</style>

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

Lines changed: 58 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,43 @@ const props = defineProps<{
1213
}>();
1314
1415
const store = useSagaDiagramStore();
16+
const timeoutMessageRef = ref<HTMLElement | null>(null);
17+
const isActive = ref(false);
1518
19+
const shouldHighlightAndScroll = computed(() => {
20+
return props.message.MessageId === store.selectedMessageId;
21+
});
22+
23+
// This sets the store with the required values so the timeout invocation node exists, it will react by scrolling to the node
1624
const navigateToTimeout = () => {
1725
// Set the selected message ID in the store
1826
store.setSelectedMessageId(props.message.MessageId);
27+
store.scrollToTimeout = true;
1928
};
29+
30+
watch(
31+
[() => store.scrollToTimeoutRequest, () => shouldHighlightAndScroll.value, () => timeoutMessageRef.value !== null],
32+
([scrollRequest, shouldScroll, refExists]) => {
33+
if (scrollRequest && shouldScroll && refExists && timeoutMessageRef.value) {
34+
timeoutMessageRef.value.scrollIntoView({
35+
behavior: "smooth",
36+
block: "center",
37+
});
38+
39+
store.scrollToTimeoutRequest = false;
40+
}
41+
},
42+
{ immediate: true }
43+
);
44+
45+
watch(
46+
() => store.selectedMessageId,
47+
(newMessageId) => {
48+
// Check if this node contains the selected message
49+
isActive.value = newMessageId === props.message.MessageId;
50+
},
51+
{ immediate: true }
52+
);
2053
</script>
2154

2255
<template>
@@ -35,7 +68,14 @@ const navigateToTimeout = () => {
3568
</div>
3669
<div class="cell cell--side">
3770
<div class="cell-inner cell-inner-right"></div>
38-
<div class="cell-inner cell-inner-side">
71+
<div
72+
ref="timeoutMessageRef"
73+
:class="{
74+
'cell-inner': true,
75+
'cell-inner-side': true,
76+
'cell-inner-side--active': isActive,
77+
}"
78+
>
3979
<img class="saga-icon saga-icon--side-cell" :src="TimeoutIcon" alt="" />
4080
<h2 class="message-title" aria-label="timeout message type">{{ message.MessageFriendlyTypeName }}</h2>
4181
<div class="timestamp" aria-label="timeout message timestamp">{{ message.FormattedTimeSent }}</div>
@@ -96,7 +136,8 @@ const navigateToTimeout = () => {
96136
}
97137
98138
.cell-inner-side--active {
99-
border: solid 2px #000000;
139+
border: solid 5px #00a3c4;
140+
animation: blink-border 1.8s ease-in-out;
100141
}
101142
102143
.cell-inner-right {
@@ -176,4 +217,19 @@ const navigateToTimeout = () => {
176217
.saga-icon--overlap {
177218
margin-left: -1rem;
178219
}
220+
221+
@keyframes blink-border {
222+
0%,
223+
100% {
224+
border-color: #00a3c4;
225+
}
226+
20%,
227+
60% {
228+
border-color: #cccccc;
229+
}
230+
40%,
231+
80% {
232+
border-color: #00a3c4;
233+
}
234+
}
179235
</style>

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

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -52,24 +52,37 @@ const initiatingMessageRef = ref<HTMLElement | null>(null);
5252
const isActive = ref(false);
5353
const hasParsingError = ref(false);
5454
55-
// Watch for changes to selectedMessageId
55+
const shouldHighlightAndScroll = computed(() => {
56+
return props.update.MessageId === store.selectedMessageId;
57+
});
58+
59+
const navigateToTimeoutRequest = () => {
60+
store.setSelectedMessageId(props.update.InitiatingMessage.MessageId);
61+
store.scrollToTimeoutRequest = true;
62+
};
63+
5664
watch(
5765
() => store.selectedMessageId,
5866
(newMessageId) => {
5967
// Check if this node contains the selected message
60-
const isSelected = props.update.InitiatingMessage.IsSagaTimeoutMessage && newMessageId === props.update.MessageId;
61-
62-
// Update active state
63-
isActive.value = isSelected;
68+
isActive.value = newMessageId === props.update.MessageId;
69+
},
70+
{ immediate: true }
71+
);
6472
65-
// If this is the selected message, scroll to it
66-
if (isSelected && initiatingMessageRef.value) {
73+
watch(
74+
[() => store.scrollToTimeout, () => shouldHighlightAndScroll.value, () => initiatingMessageRef.value !== null],
75+
([scrollTimeout, shouldScroll, refExists]) => {
76+
if (scrollTimeout && shouldScroll && refExists && initiatingMessageRef.value) {
6777
initiatingMessageRef.value.scrollIntoView({
6878
behavior: "smooth",
6979
block: "center",
7080
});
81+
82+
store.scrollToTimeout = false;
7183
}
72-
}
84+
},
85+
{ immediate: true }
7386
);
7487
7588
// Format a JSON value for display
@@ -168,7 +181,11 @@ const hasStateChanges = computed(() => {
168181
<div class="cell-inner cell-inner-center cell-inner--align-bottom">
169182
<template v-if="update.InitiatingMessage.IsSagaTimeoutMessage">
170183
<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>
184+
<!-- Conditionally make the text a link based on HasRelatedTimeoutRequest -->
185+
<a v-if="update.InitiatingMessage.HasRelatedTimeoutRequest" href="#" @click.prevent="navigateToTimeoutRequest" class="saga-status-title saga-status-title--inline timeout-status timeout-status--link" aria-label="timeout invoked">
186+
Timeout Invoked
187+
</a>
188+
<h2 v-else class="saga-status-title saga-status-title--inline timeout-status" aria-label="timeout invoked">Timeout Invoked</h2>
172189
<br />
173190
</template>
174191
<img class="saga-icon saga-icon--center-cell" :src="update.IsFirstNode ? SagaInitiatedIcon : SagaUpdatedIcon" alt="" />
@@ -408,7 +425,16 @@ const hasStateChanges = computed(() => {
408425
display: inline-block;
409426
font-size: 1rem;
410427
font-weight: 900;
411-
color: #00a3c4;
428+
color: inherit; /* Default to inheriting parent color */
429+
}
430+
431+
.timeout-status--link {
432+
color: #00a3c4; /* Blue color only for links */
433+
text-decoration: none;
434+
}
435+
436+
.timeout-status--link:hover {
437+
text-decoration: underline;
412438
}
413439
414440
/* Styles for DiffViewer integration */

src/Frontend/src/stores/SagaDiagramStore.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ export const useSagaDiagramStore = defineStore("SagaDiagramStore", () => {
2424
const fetchedMessages = ref(new Set<string>());
2525
const messagesData = ref<SagaMessageData[]>([]);
2626
const selectedMessageId = ref<string | null>(null);
27+
const scrollToTimeoutRequest = ref(false);
28+
const scrollToTimeout = ref(false);
2729
const MessageBodyEndpoint = "messages/{0}/body";
2830

2931
// Watch the sagaId and fetch saga history when it changes
@@ -190,6 +192,7 @@ export const useSagaDiagramStore = defineStore("SagaDiagramStore", () => {
190192
fetchedMessages.value.clear();
191193
messagesData.value = [];
192194
selectedMessageId.value = null;
195+
scrollToTimeoutRequest.value = false;
193196
}
194197

195198
function formatUrl(template: string, id: string): string {
@@ -254,6 +257,22 @@ export const useSagaDiagramStore = defineStore("SagaDiagramStore", () => {
254257
selectedMessageId.value = messageId;
255258
}
256259

260+
watch(scrollToTimeoutRequest, (newValue) => {
261+
if (newValue) {
262+
setTimeout(() => {
263+
scrollToTimeoutRequest.value = false;
264+
}, 1000);
265+
}
266+
});
267+
268+
watch(scrollToTimeout, (newValue) => {
269+
if (newValue) {
270+
setTimeout(() => {
271+
scrollToTimeout.value = false;
272+
}, 1000);
273+
}
274+
});
275+
257276
return {
258277
sagaHistory,
259278
sagaId,
@@ -263,6 +282,8 @@ export const useSagaDiagramStore = defineStore("SagaDiagramStore", () => {
263282
showMessageData,
264283
messagesData,
265284
selectedMessageId,
285+
scrollToTimeoutRequest,
286+
scrollToTimeout,
266287
setSagaId,
267288
clearSagaHistory,
268289
toggleMessageData,

0 commit comments

Comments
 (0)