Skip to content

Commit 27d4787

Browse files
authored
Sequence diagram first part (#2302)
sequence diagram for forward flow scenarios only
1 parent bf10b6f commit 27d4787

File tree

13 files changed

+998
-3
lines changed

13 files changed

+998
-3
lines changed

src/Frontend/src/components/messages/MessageView.vue

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import TimeSince from "../TimeSince.vue";
1010
import moment from "moment";
1111
import ConfirmDialog from "../ConfirmDialog.vue";
1212
import FlowDiagram from "./FlowDiagram.vue";
13+
import SequenceDiagram from "./SequenceDiagram.vue";
1314
import EditRetryDialog from "../failedmessages/EditRetryDialog.vue";
1415
import routeLinks from "@/router/routeLinks";
1516
import { EditAndRetryConfig } from "@/resources/Configuration";
@@ -43,6 +44,7 @@ const showEditRetryModal = ref(false);
4344
4445
const configuration = useConfiguration();
4546
const isMassTransitConnected = useIsMassTransitConnected();
47+
const showAllMessages = window.defaultConfig.showAllMessages;
4648
4749
async function loadFailedMessage() {
4850
const response = await useFetchFromServiceControl(`errors/last/${id.value}`);
@@ -71,7 +73,7 @@ async function loadFailedMessage() {
7173
}
7274
7375
updateMessageDeleteDate(message);
74-
await downloadHeadersAndBody(message);
76+
await fetchMessageDetails(message);
7577
failedMessage.value = message;
7678
}
7779
@@ -115,7 +117,7 @@ async function retryMessage() {
115117
}
116118
}
117119
118-
async function downloadHeadersAndBody(message: ExtendedFailedMessage) {
120+
async function fetchMessageDetails(message: ExtendedFailedMessage) {
119121
if (isError(message)) return;
120122
121123
try {
@@ -349,11 +351,13 @@ onUnmounted(() => {
349351
<h5 :class="{ active: panel === 2 }" class="nav-item" @click.prevent="togglePanel(2)"><a href="#">Message body</a></h5>
350352
<h5 :class="{ active: panel === 3 }" class="nav-item" @click.prevent="togglePanel(3)"><a href="#">Headers</a></h5>
351353
<h5 v-if="!isMassTransitConnected" :class="{ active: panel === 4 }" class="nav-item" @click.prevent="togglePanel(4)"><a href="#">Flow Diagram</a></h5>
354+
<h5 v-if="showAllMessages" :class="{ active: panel === 5 }" class="nav-item" @click.prevent="togglePanel(5)"><a href="#">Sequence Diagram</a></h5>
352355
</div>
353356
<StackTraceView v-if="panel === 1 && failedMessage.exception?.stack_trace" :message="failedMessage" />
354357
<BodyView v-if="panel === 2" :message="failedMessage" />
355358
<HeadersView v-if="panel === 3" :message="failedMessage" />
356359
<FlowDiagram v-if="panel === 4" :message="failedMessage" />
360+
<SequenceDiagram v-if="showAllMessages && panel === 5" />
357361
</div>
358362
</div>
359363

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<script setup lang="ts">
2+
import Endpoints from "./SequenceDiagram/EndpointsComponent.vue";
3+
import Timeline from "./SequenceDiagram/TimelineComponent.vue";
4+
import Handlers from "./SequenceDiagram/HandlersComponent.vue";
5+
import Routes from "./SequenceDiagram/RoutesComponent.vue";
6+
import { useSequenceDiagramStore } from "@/stores/SequenceDiagramStore";
7+
import { storeToRefs } from "pinia";
8+
9+
const store = useSequenceDiagramStore();
10+
store.setConversationId("39907d51-12e5-4202-82c3-b2b30077ebd4");
11+
12+
const { maxWidth, maxHeight } = storeToRefs(store);
13+
</script>
14+
15+
<template>
16+
<div class="outer">
17+
<svg class="sequence-diagram" :width="`max(100%, ${isNaN(maxWidth) ? 0 : maxWidth}px)`" :height="maxHeight + 20">
18+
<Endpoints />
19+
<Timeline />
20+
<Handlers />
21+
<Routes />
22+
</svg>
23+
</div>
24+
</template>
25+
26+
<style scoped>
27+
.outer {
28+
max-width: 100%;
29+
max-height: calc(100vh - 27em);
30+
overflow: auto;
31+
}
32+
33+
.sequence-diagram {
34+
--error: red;
35+
--gray20: #333333;
36+
--gray30: #444444;
37+
--gray40: #666666;
38+
--gray60: #999999;
39+
--gray80: #cccccc;
40+
--gray90: #e6e6e6;
41+
--gray95: #b3b3b3;
42+
--highlight: #0b6eef;
43+
--highlight-background: #c5dee9;
44+
background: white;
45+
}
46+
</style>
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<script setup lang="ts">
2+
import { Endpoint } from "@/resources/SequenceDiagram/Endpoint";
3+
import { EndpointCentrePoint, useSequenceDiagramStore } from "@/stores/SequenceDiagramStore";
4+
import { storeToRefs } from "pinia";
5+
import { computed, ref, watch } from "vue";
6+
7+
interface EndpointWithLocation extends Endpoint {
8+
width: number;
9+
textWidth: number;
10+
x?: number;
11+
surround: EndpointSurround;
12+
}
13+
14+
interface EndpointSurround {
15+
x: number;
16+
y: number;
17+
width: number;
18+
height: number;
19+
fill: string;
20+
rx: string;
21+
strokeWidth: string;
22+
stroke: string;
23+
}
24+
25+
const Endpoint_Width = 260;
26+
const Endpoint_Gap = 30;
27+
const Endpoint_Image_Width = 20;
28+
29+
const store = useSequenceDiagramStore();
30+
const { endpoints } = storeToRefs(store);
31+
32+
const epRefs = ref<SVGTextElement[]>([]);
33+
const endpointItems = computed(() =>
34+
endpoints.value.map((x, index) => {
35+
const endpoint = x as EndpointWithLocation;
36+
const el = epRefs.value[index];
37+
if (el) {
38+
const bounds = el.getBBox();
39+
const previousEndpoint = index > 0 ? endpointItems.value[index - 1] : undefined;
40+
endpoint.width = Math.max(Endpoint_Width, bounds.width);
41+
endpoint.textWidth = bounds.width;
42+
endpoint.x = (previousEndpoint?.x ?? Endpoint_Width / 2) + (previousEndpoint?.width ?? 0) + Endpoint_Gap;
43+
44+
if (!endpoint.surround && el.isConnected) {
45+
const style = getComputedStyle(el);
46+
const padding_top = parseInt(style.getPropertyValue("padding-top"));
47+
const padding_left = parseInt(style.getPropertyValue("padding-left"));
48+
const padding_right = parseInt(style.getPropertyValue("padding-right"));
49+
const padding_bottom = parseInt(style.getPropertyValue("padding-bottom"));
50+
endpoint.surround = {
51+
x: endpoint.x - endpoint.width / 2 - padding_left,
52+
y: bounds.y - padding_top,
53+
width: endpoint.width + padding_left + padding_right,
54+
height: bounds.height + padding_top + padding_bottom,
55+
fill: style.getPropertyValue("background-color"),
56+
rx: style.getPropertyValue("border-radius"),
57+
strokeWidth: style.getPropertyValue("border-top-width"),
58+
stroke: style.getPropertyValue("border-top-color"),
59+
};
60+
}
61+
}
62+
return endpoint;
63+
})
64+
);
65+
66+
watch(endpointItems, () => {
67+
store.setEndpointCentrePoints(endpointItems.value.map((endpoint) => ({ name: endpoint.name, centre: endpoint.x ?? 0, top: (endpoint.surround?.y ?? 0) + (endpoint.surround?.height ?? 0) + 15 }) as EndpointCentrePoint));
68+
const lastEndpoint = endpointItems.value[endpointItems.value.length - 1];
69+
store.setMaxWidth((lastEndpoint.x ?? 0) + lastEndpoint.width);
70+
});
71+
72+
function setEndpointRef(el: SVGTextElement, index: number) {
73+
if (el) epRefs.value[index] = el;
74+
}
75+
</script>
76+
77+
<template>
78+
<g v-for="(endpoint, i) in endpointItems" :key="endpoint.name" transform="translate(0,15)">
79+
<rect
80+
v-if="endpoint.surround"
81+
:x="endpoint.surround.x"
82+
:y="endpoint.surround.y"
83+
:width="endpoint.surround.width"
84+
:height="endpoint.surround.height"
85+
:fill="endpoint.surround.fill"
86+
:rx="endpoint.surround.rx"
87+
:stroke-width="endpoint.surround.strokeWidth"
88+
:stroke="endpoint.surround.stroke"
89+
></rect>
90+
<g :transform="`translate(${(endpoint.x ?? Endpoint_Width / 2) - ((endpoint.textWidth ?? 0) + Endpoint_Image_Width) / 2}, 0)`">
91+
<path fill="var(--gray40)" d="M 0,0 M 18,18 M 0,2 v 14 h 14 v -4 h -6 v -6 h 6 v -4 h -14 M 9,7 v 4 h 9 v -4"></path>
92+
<text :x="Endpoint_Image_Width" y="10" alignment-baseline="middle" text-anchor="start" :ref="(el) => setEndpointRef(el as SVGTextElement, i)">{{ endpoint.name }}</text>
93+
</g>
94+
</g>
95+
</template>
96+
97+
<style scoped>
98+
text {
99+
background: var(--gray90);
100+
border-radius: 5px;
101+
padding: 0.5em;
102+
}
103+
</style>
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<script setup lang="ts">
2+
import { HandlerState } from "@/resources/SequenceDiagram/Handler";
3+
import { computed, ref } from "vue";
4+
import { Direction } from "@/resources/SequenceDiagram/RoutedMessage";
5+
import { useSequenceDiagramStore } from "@/stores/SequenceDiagramStore";
6+
import { storeToRefs } from "pinia";
7+
8+
const Height_Per_Out = 40;
9+
const Handler_Gap = 20;
10+
const Handler_Width = 14;
11+
12+
const store = useSequenceDiagramStore();
13+
const { handlers, endpointCentrePoints, highlightId } = storeToRefs(store);
14+
15+
const messageTypeRefs = ref<SVGTextElement[]>([]);
16+
17+
const handlerItems = computed(() => {
18+
let nextY = 0;
19+
const result = handlers.value.map((handler, index) => {
20+
const endpoint = endpointCentrePoints.value.find((cp) => cp.name === handler.endpoint.name)!;
21+
const messageTypeElement = messageTypeRefs.value[index];
22+
const count = handler.outMessages.length;
23+
const height = (count === 0 ? 1 : count) * Height_Per_Out;
24+
if (nextY === 0) nextY += Handler_Gap + (endpoint?.top ?? 0);
25+
const y = nextY;
26+
nextY += height + Handler_Gap;
27+
const fill = (() => {
28+
if (handler.id === "First") return "black";
29+
if (handler.state === HandlerState.Fail) return "var(--error)";
30+
if (handler.route?.name === highlightId.value) return "var(--highlight-background)";
31+
return "var(--gray60)";
32+
})();
33+
const icon = (() => {
34+
if (handler.id === "First") return "M0,0L8,4 0,8z";
35+
if (handler.state === HandlerState.Fail) return "M6,0L0,6 6,12 12,6 6,0z M7,9L5,9 5,8 7,8 7,9z M5,7L5,3 7,3 7,7 5,7z";
36+
return null;
37+
})();
38+
const iconSize = (() => {
39+
if (handler.id === "First") return 8;
40+
if (handler.state === HandlerState.Fail) return 12;
41+
return 0;
42+
})();
43+
44+
return {
45+
id: handler.id,
46+
incomingId: handler.route?.name,
47+
left: (endpoint?.centre ?? 0) - Handler_Width / 2,
48+
right: (endpoint?.centre ?? 0) + Handler_Width / 2,
49+
y,
50+
height,
51+
fill,
52+
icon,
53+
iconSize,
54+
messageType: handler.name,
55+
messageTypeOffset: handler.direction === Direction.Right ? ((messageTypeElement?.getBBox().width ?? 0) + 24) * -1 : 20,
56+
messageTypeHighlight: handler.route?.name === highlightId.value,
57+
};
58+
});
59+
60+
store.setMaxHeight(nextY);
61+
store.setHandlerLocations(result.map((handler) => ({ id: handler.id, left: handler.left, right: handler.right, y: handler.y, height: handler.height })));
62+
63+
return result;
64+
});
65+
66+
function setMessageTypeRef(el: SVGTextElement, index: number) {
67+
if (el) messageTypeRefs.value[index] = el;
68+
}
69+
</script>
70+
71+
<template>
72+
<g v-for="(handler, i) in handlerItems" :key="handler.id" :transform="`translate(${handler.left}, ${handler.y})`">
73+
<!--Handler Activation Box-->
74+
<rect :width="Handler_Width" :height="handler.height" :class="handler.incomingId && 'clickable'" :fill="handler.fill" @mouseover="() => store.setHighlightId(handler.incomingId)" @mouseleave="() => store.setHighlightId()" />
75+
<path v-if="handler.icon" :d="handler.icon" fill="white" :transform="`translate(${Handler_Width / 2 - handler.iconSize / 2}, ${handler.height / 2 - handler.iconSize / 2})`" />
76+
<!--Message Type and Icon-->
77+
<g
78+
v-if="handler.messageType"
79+
:transform="`translate(${handler.messageTypeOffset}, 4)`"
80+
class="clickable"
81+
:fill="handler.messageTypeHighlight ? 'var(--highlight)' : 'var(--gray40)'"
82+
@mouseover="() => store.setHighlightId(handler.incomingId)"
83+
@mouseleave="() => store.setHighlightId()"
84+
>
85+
<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" />
86+
<text x="14" y="10" alignment-baseline="middle" :ref="(el) => setMessageTypeRef(el as SVGTextElement, i)">{{ handler.messageType }}</text>
87+
</g>
88+
</g>
89+
</template>
90+
91+
<style scoped>
92+
.clickable {
93+
cursor: pointer;
94+
}
95+
</style>

0 commit comments

Comments
 (0)