Skip to content

Commit 8c59517

Browse files
authored
Sequence Diagram message selection (#2334)
* selection of messages and navigation to selection * fix rect size error * keep endpoints visible during scroll * fix sequence model creator when first message is not included * handle handler state not being known due to missing incoming message * switch to using full type name for internal identification of handlers/routes * scroll to selected element on tab load/activate
1 parent b47dc6b commit 8c59517

File tree

9 files changed

+134
-29
lines changed

9 files changed

+134
-29
lines changed

src/Frontend/src/components/messages2/SequenceDiagram.vue

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,24 @@ import Routes from "./SequenceDiagram/RoutesComponent.vue";
66
import { useSequenceDiagramStore } from "@/stores/SequenceDiagramStore";
77
import { storeToRefs } from "pinia";
88
import useTooltips from "./SequenceDiagram/tooltipOverlay.ts";
9-
import { onMounted } from "vue";
9+
import { onMounted, ref } from "vue";
1010
1111
const store = useSequenceDiagramStore();
1212
const { maxWidth, maxHeight } = storeToRefs(store);
13+
const endpointYOffset = ref(0);
1314
1415
useTooltips();
1516
1617
onMounted(() => store.refreshConversation());
1718
</script>
1819

1920
<template>
20-
<div class="outer">
21+
<div class="outer" @scroll="(ev) => (endpointYOffset = (ev.target as Element).scrollTop)">
2122
<svg class="sequence-diagram" :width="`max(100%, ${isNaN(maxWidth) ? 0 : maxWidth}px)`" :height="maxHeight + 20">
22-
<Endpoints />
2323
<Timeline />
2424
<Handlers />
2525
<Routes />
26+
<Endpoints :yOffset="endpointYOffset" />
2627
</svg>
2728
</div>
2829
</template>

src/Frontend/src/components/messages2/SequenceDiagram/EndpointsComponent.vue

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ interface EndpointSurround {
2525
const Endpoint_Gap = 30;
2626
const Endpoint_Image_Width = 20;
2727
28+
defineProps<{
29+
yOffset: number;
30+
}>();
31+
2832
const store = useSequenceDiagramStore();
2933
const { startX, endpoints } = storeToRefs(store);
3034
@@ -79,7 +83,7 @@ function setEndpointTextRef(el: SVGTextElement, index: number) {
7983
</script>
8084

8185
<template>
82-
<g v-for="(endpoint, i) in endpointItems" :key="endpoint.name" transform="translate(0,15)" :ref="(el) => (endpoint.uiRef = el as SVGElement)">
86+
<g v-for="(endpoint, i) in endpointItems" :key="endpoint.name" :transform="`translate(0,${yOffset + 15})`" :ref="(el) => (endpoint.uiRef = el as SVGElement)">
8387
<rect
8488
v-if="endpoint.surround"
8589
:x="endpoint.surround.x"

src/Frontend/src/components/messages2/SequenceDiagram/HandlerTooltip.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ function formatTime(milliseconds: number) {
1818
<label>Processing Time:</label>
1919
<span>{{ formatTime(handler.processingTime ?? 0) }}</span>
2020
<label>Processing Of:</label>
21-
<span>{{ handler.name }}</span>
21+
<span>{{ handler.friendlyName }}</span>
2222
<label v-if="handler.partOfSaga">Sagas Invoked:</label>
2323
<span v-if="handler.partOfSaga">{{ handler.partOfSaga }}</span>
2424
</div>

src/Frontend/src/components/messages2/SequenceDiagram/HandlersComponent.vue

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script setup lang="ts">
22
import { HandlerState } from "@/resources/SequenceDiagram/Handler";
3-
import { computed, ref } from "vue";
3+
import { computed, onActivated, ref, watch } from "vue";
44
import { Direction } from "@/resources/SequenceDiagram/RoutedMessage";
55
import { useSequenceDiagramStore } from "@/stores/SequenceDiagramStore";
66
import { storeToRefs } from "pinia";
@@ -10,9 +10,17 @@ const Handler_Gap = 20;
1010
const Handler_Width = 14;
1111
1212
const store = useSequenceDiagramStore();
13-
const { handlers, endpointCentrePoints, highlightId } = storeToRefs(store);
13+
const { handlers, endpointCentrePoints, highlightId, selectedId } = storeToRefs(store);
1414
1515
const messageTypeRefs = ref<SVGTextElement[]>([]);
16+
const hasMadeVisible = ref(false);
17+
const selectedElement = ref<SVGElement>();
18+
//reset values to allow scroll to element on switching back to this tab
19+
onActivated(() => {
20+
hasMadeVisible.value = false;
21+
if (selectedElement.value) scrollToIfSelected(selectedElement.value, selectedId.value);
22+
});
23+
watch(selectedId, () => (selectedElement.value = undefined));
1624
1725
const handlerItems = computed(() => {
1826
let nextY = 0;
@@ -27,7 +35,8 @@ const handlerItems = computed(() => {
2735
const fill = (() => {
2836
if (handler.id === "First") return "black";
2937
if (handler.state === HandlerState.Fail) return "var(--error)";
30-
if (handler.route?.name === highlightId.value) return "var(--highlight-background)";
38+
if (handler.route && handler.route.name === selectedId.value) return "var(--highlight)";
39+
if (handler.route && handler.route.name === highlightId.value) return "var(--highlight-background)";
3140
return "var(--gray60)";
3241
})();
3342
const icon = (() => {
@@ -51,6 +60,8 @@ const handlerItems = computed(() => {
5160
5261
return {
5362
id: handler.id,
63+
messageId: { id: handler.selectedMessage?.message_id, uniqueId: handler.selectedMessage?.id },
64+
isError: handler.state === HandlerState.Fail,
5465
endpointName: handler.endpoint.name,
5566
incomingId: handler.route?.name,
5667
left,
@@ -60,9 +71,10 @@ const handlerItems = computed(() => {
6071
fill,
6172
icon,
6273
iconSize,
63-
messageType: handler.name,
74+
messageType: handler.friendlyName,
6475
messageTypeOffset,
6576
messageTypeHighlight: handler.route?.name === highlightId.value,
77+
messageTypeSelected: handler.route?.name === selectedId.value,
6678
setUIRef: (el: SVGElement) => (handler.uiRef = el),
6779
};
6880
});
@@ -76,23 +88,46 @@ const handlerItems = computed(() => {
7688
function setMessageTypeRef(el: SVGTextElement, index: number) {
7789
if (el) messageTypeRefs.value[index] = el;
7890
}
91+
92+
function scrollToIfSelected(el: SVGElement, handlerId: string | undefined) {
93+
if (!hasMadeVisible.value && el && handlerId === selectedId.value) {
94+
hasMadeVisible.value = true;
95+
selectedElement.value = el;
96+
//can't be done immediately since the sequence diagram hasn't completed layout yet
97+
setTimeout(() => selectedElement.value!.scrollIntoView(false), 30);
98+
}
99+
}
79100
</script>
80101

81102
<template>
82-
<g v-for="(handler, i) in handlerItems" :key="`${handler.id}###${handler.endpointName}`" :transform="`translate(${handler.left}, ${handler.y})`">
103+
<g v-for="(handler, i) in handlerItems" :key="`${handler.id}###${handler.endpointName}`" :ref="(el) => scrollToIfSelected(el as SVGElement, handler.incomingId)" :transform="`translate(${handler.left}, ${handler.y})`">
83104
<!--Handler Activation Box-->
84-
<g :ref="(el) => handler.setUIRef(el as SVGElement)">
85-
<rect :width="Handler_Width" :height="handler.height" :class="handler.incomingId && 'clickable'" :fill="handler.fill" @mouseover="() => store.setHighlightId(handler.incomingId)" @mouseleave="() => store.setHighlightId()" />
105+
<g :ref="(el) => handler.setUIRef(el as SVGElement)" class="activation-box">
106+
<rect
107+
:width="Handler_Width"
108+
:height="handler.height"
109+
:class="{
110+
clickable: handler.incomingId && !handler.messageTypeSelected,
111+
}"
112+
:fill="handler.fill"
113+
@mouseover="() => store.setHighlightId(handler.incomingId)"
114+
@mouseleave="() => store.setHighlightId()"
115+
@click="handler.incomingId && !handler.messageTypeSelected && store.navigateTo(handler.messageId.uniqueId, handler.messageId.id, handler.isError)"
116+
/>
86117
<path v-if="handler.icon" :d="handler.icon" fill="white" :transform="`translate(${Handler_Width / 2 - handler.iconSize / 2}, ${handler.height / 2 - handler.iconSize / 2})`" />
87118
</g>
88119
<!--Message Type and Icon-->
89120
<g
90121
v-if="handler.messageType"
91122
:transform="`translate(${handler.messageTypeOffset}, 4)`"
92-
class="clickable"
93-
:fill="handler.messageTypeHighlight ? 'var(--highlight)' : 'var(--gray40)'"
123+
:class="{
124+
clickable: !handler.messageTypeSelected,
125+
'message-type': true,
126+
highlight: handler.messageTypeHighlight || handler.messageTypeSelected,
127+
}"
94128
@mouseover="() => store.setHighlightId(handler.incomingId)"
95129
@mouseleave="() => store.setHighlightId()"
130+
@click="handler.incomingId && !handler.messageTypeSelected && store.navigateTo(handler.messageId.uniqueId, handler.messageId.id, handler.isError)"
96131
>
97132
<path d="M9,3L9,3 9,0 0,0 0,3 4,3 4,6 0,6 0,9 4,9 4,12 0,12 0,15 9,15 9,12 5,12 5,9 9,9 9,6 5,6 5,3z" />
98133
<text x="14" y="10" alignment-baseline="middle" :ref="(el) => setMessageTypeRef(el as SVGTextElement, i)">{{ handler.messageType }}</text>
@@ -104,4 +139,16 @@ function setMessageTypeRef(el: SVGTextElement, index: number) {
104139
.clickable {
105140
cursor: pointer;
106141
}
142+
143+
.activation-box:focus {
144+
outline: none;
145+
}
146+
147+
.message-type {
148+
fill: var(--gray40);
149+
}
150+
151+
.message-type.highlight {
152+
fill: var(--highlight);
153+
}
107154
</style>

src/Frontend/src/components/messages2/SequenceDiagram/RoutesComponent.vue

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ import { Direction, RoutedMessageType } from "@/resources/SequenceDiagram/Routed
33
import { computed, ref } from "vue";
44
import { useSequenceDiagramStore } from "@/stores/SequenceDiagramStore";
55
import { storeToRefs } from "pinia";
6+
import { HandlerState } from "@/resources/SequenceDiagram/Handler";
67
78
const Arrow_Head_Width = 10;
89
const Message_Type_Margin = 4;
910
1011
const store = useSequenceDiagramStore();
11-
const { routes, handlerLocations, highlightId } = storeToRefs(store);
12+
const { selectedId, routes, handlerLocations, highlightId } = storeToRefs(store);
1213
1314
const messageTypeRefs = ref<SVGTextElement[]>([]);
1415
@@ -41,6 +42,9 @@ const arrows = computed(() =>
4142
4243
return {
4344
id: route.name,
45+
selected: route.name === selectedId.value,
46+
messageId: { uniqueId: route.fromRoutedMessage.selectedMessage.id, id: route.fromRoutedMessage.selectedMessage.message_id },
47+
isHandlerError: route.processingHandler?.state === HandlerState.Fail,
4448
fromX,
4549
y,
4650
direction,
@@ -73,25 +77,30 @@ function setMessageTypeRef(el: SVGTextElement, index: number) {
7377
<path v-if="arrow.direction === Direction.Left" :d="`M${arrow.toHandlerCentre + 1} ${arrow.y} l10,-7.5 0,15z`" fill="black" />
7478
</g>
7579
<!--Highlight Arrow-->
76-
<g v-if="arrow.highlight" :transform="`translate(${arrow.toHandlerCentre},${arrow.y})`" stroke="var(--highlight)" fill="var(--highlight)">
80+
<g v-if="arrow.highlight || arrow.selected" :transform="`translate(${arrow.toHandlerCentre},${arrow.y})`" stroke="var(--highlight)" fill="var(--highlight)">
7781
<path :d="`M0 0 v${arrow.height - 6}`" stroke-width="2" />
7882
<path :d="`M0 ${arrow.height} l-3,-6 6,0z`" />
7983
</g>
8084
<!--Message Type and Icon-->
8185
<g
82-
class="clickable message-type"
86+
:class="{
87+
clickable: !arrow.selected,
88+
'message-type': true,
89+
highlight: arrow.highlight,
90+
selected: arrow.selected,
91+
}"
8392
:transform="`translate(${arrow.messageTypeOffset}, ${arrow.y - 7.5 - Message_Type_Margin})`"
84-
:fill="arrow.highlight ? 'var(--highlight)' : 'black'"
8593
@mouseover="() => store.setHighlightId(arrow.id)"
8694
@mouseleave="() => store.setHighlightId()"
95+
@click="!arrow.selected && store.navigateTo(arrow.messageId.uniqueId, arrow.messageId.id, arrow.isHandlerError)"
8796
:ref="(el) => arrow.setUIRef(el as SVGElement)"
8897
>
8998
<!--19 is width of MessageType icon, plus a gap-->
9099
<rect
91-
v-if="arrow.highlight && arrow.messageTypeOffset"
92-
:width="arrow.highlightTextWidth + 19 + Message_Type_Margin + Message_Type_Margin"
93-
:height="arrow.highlightTextHeight + Message_Type_Margin + Message_Type_Margin"
94-
fill="var(--highlight-background)"
100+
v-if="(arrow.highlight || arrow.selected) && arrow.messageTypeOffset"
101+
:width="(arrow.highlightTextWidth ?? 0) + 19 + Message_Type_Margin + Message_Type_Margin"
102+
:height="(arrow.highlightTextHeight ?? 0) + Message_Type_Margin + Message_Type_Margin"
103+
class="border"
95104
/>
96105
<svg :x="Message_Type_Margin" :y="Message_Type_Margin" width="15" height="15" viewBox="0 0 32 32">
97106
<path
@@ -116,7 +125,32 @@ function setMessageTypeRef(el: SVGTextElement, index: number) {
116125
cursor: pointer;
117126
}
118127
128+
.message-type {
129+
fill: black;
130+
outline: none;
131+
}
132+
133+
.message-type.selected {
134+
fill: white;
135+
}
136+
137+
.message-type .border {
138+
fill: var(--highlight-background);
139+
}
140+
141+
.message-type.selected .border {
142+
fill: var(--highlight);
143+
}
144+
145+
.message-type:not(.selected).highlight {
146+
fill: var(--highlight);
147+
}
148+
119149
.message-type text::selection {
120150
fill: white;
121151
}
152+
153+
.message-type.selected text::selection {
154+
background-color: black;
155+
}
122156
</style>

src/Frontend/src/components/messages2/SequenceDiagram/tooltipOverlay.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useTippy } from "vue-tippy";
55
import EndpointTooltip from "./EndpointTooltip.vue";
66
import HandlerTooltip from "./HandlerTooltip.vue";
77
import RouteTooltip from "./RouteTooltip.vue";
8+
import { HandlerState } from "@/resources/SequenceDiagram/Handler";
89

910
export default function useTooltips() {
1011
const store = useSequenceDiagramStore();
@@ -30,7 +31,7 @@ export default function useTooltips() {
3031
() => handlers.value.map((handler) => handler.uiRef),
3132
() =>
3233
handlers.value
33-
.filter((handler) => handler.uiRef)
34+
.filter((handler) => handler.uiRef && handler.state !== HandlerState.Unknown)
3435
.forEach((handler) =>
3536
useTippy(handler.uiRef, {
3637
interactive: true,

src/Frontend/src/resources/SequenceDiagram/Handler.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { friendlyTypeName } from "./SequenceModel";
77
export interface Handler {
88
readonly id: string;
99
name?: string;
10+
friendlyName?: string;
1011
readonly endpoint: Endpoint;
1112
readonly isPartOfSaga: boolean;
1213
partOfSaga?: string;
@@ -27,6 +28,7 @@ export interface Handler {
2728
export enum HandlerState {
2829
Fail,
2930
Success,
31+
Unknown,
3032
}
3133

3234
export const ConversationStartHandlerName = "First";
@@ -60,7 +62,8 @@ export function updateProcessingHandler(handler: Handler, message: Message) {
6062
//TODO: extract logic since it's also currently used in AuditList
6163
const [hh, mm, ss] = message.processing_time.split(":");
6264
handler.processingTime = ((parseInt(hh) * 60 + parseInt(mm)) * 60 + parseFloat(ss)) * 1000;
63-
handler.name = friendlyTypeName(message.message_type);
65+
handler.name = message.message_type;
66+
handler.friendlyName = friendlyTypeName(message.message_type);
6467

6568
if ((message.invoked_sagas?.length ?? 0) > 0) {
6669
handler.partOfSaga = message.invoked_sagas!.map((saga) => friendlyTypeName(saga.saga_type)).join(", ");
@@ -85,7 +88,7 @@ class HandlerItem implements Handler {
8588
name?: string;
8689
partOfSaga?: string;
8790
inMessage?: RoutedMessage;
88-
state: HandlerState = HandlerState.Fail;
91+
state: HandlerState = HandlerState.Unknown;
8992
processedAt?: Date;
9093
processingTime?: number;
9194
route?: MessageProcessingRoute;

src/Frontend/src/resources/SequenceDiagram/SequenceModel.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,10 @@ export class ModelCreator implements ConversationModel {
7272
sendingHandler.addOutMessage(routedMessage);
7373
}
7474

75-
const start = firstOrderHandlers.find((h) => h.id === ConversationStartHandlerName);
75+
const start = firstOrderHandlers.filter((h) => h.id === ConversationStartHandlerName);
7676
const orderByHandledAt = firstOrderHandlers.filter((h) => h.id !== ConversationStartHandlerName).sort((a, b) => (a.handledAt?.getTime() ?? 0) - (b.handledAt?.getTime() ?? 0));
7777

78-
this.#handlers = [start!, ...orderByHandledAt];
78+
this.#handlers = [...start, ...orderByHandledAt];
7979
}
8080

8181
get endpoints(): Endpoint[] {

0 commit comments

Comments
 (0)