Skip to content

Commit 0a77661

Browse files
committed
Merge branch 'master' into john/common_message_view
2 parents 26219fb + 1440e61 commit 0a77661

19 files changed

+1171
-108
lines changed

src/Frontend/package-lock.json

Lines changed: 104 additions & 89 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Frontend/package.json

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@
2323
"@tinyhttp/content-disposition": "^2.2.2",
2424
"@vue-flow/controls": "^1.1.2",
2525
"@vue-flow/core": "^1.42.5",
26-
"bootstrap": "^5.3.3",
26+
"bootstrap": "^5.3.5",
2727
"bootstrap-icons": "^1.11.3",
2828
"codemirror": "^6.0.1",
29+
"lodash.debounce": "^4.0.8",
2930
"lossless-json": "^4.0.2",
3031
"memoize-one": "^6.0.0",
3132
"moment": "^2.30.1",
@@ -40,20 +41,21 @@
4041
"xml-formatter": "^3.6.5"
4142
},
4243
"devDependencies": {
43-
"@eslint/js": "^9.22.0",
44+
"@eslint/js": "^9.23.0",
4445
"@pinia/testing": "^1.0.0",
4546
"@testing-library/dom": "^10.4.0",
4647
"@testing-library/jest-dom": "^6.6.3",
4748
"@testing-library/user-event": "^14.6.1",
4849
"@testing-library/vue": "^8.1.0",
4950
"@tsconfig/node18": "^18.2.4",
5051
"@types/bootstrap": "^5.2.10",
51-
"@types/node": "^22.13.10",
52+
"@types/lodash": "^4.17.16",
53+
"@types/node": "^22.14.0",
5254
"@vitejs/plugin-vue": "^5.2.3",
53-
"@vitest/coverage-v8": "^3.0.9",
55+
"@vitest/coverage-v8": "^3.1.1",
5456
"@vue/tsconfig": "^0.7.0",
5557
"cross-env": "^7.0.3",
56-
"eslint": "^9.22.0",
58+
"eslint": "^9.23.0",
5759
"eslint-config-prettier": "^10.1.1",
5860
"eslint-plugin-prettier": "^5.2.6",
5961
"eslint-plugin-promise": "^7.2.1",
@@ -63,9 +65,9 @@
6365
"jsdom": "^26.0.0",
6466
"msw": "^2.7.3",
6567
"prettier": "^3.5.3",
66-
"typescript": "^5.8.2",
68+
"typescript": "^5.8.3",
6769
"typescript-eslint": "^8.29.0",
68-
"vite": "^6.2.4",
70+
"vite": "^6.2.5",
6971
"vite-plugin-checker": "^0.9.1",
7072
"vitest": "^3.0.9",
7173
"vue-tsc": "^2.2.8"

src/Frontend/src/components/FilterInput.vue

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
11
<script setup lang="ts">
2+
import { ref, watch } from "vue";
3+
import debounce from "lodash/debounce";
4+
25
const model = defineModel<string>({ required: true });
3-
defineProps({
4-
placeholder: {
5-
type: String,
6-
default: "Filter by name...", // Default value
7-
},
6+
const props = withDefaults(defineProps<{ placeholder?: string; ariaLabel?: string }>(), { placeholder: "Filter by name...", ariaLabel: "filter by name" });
7+
const localInput = ref<string>(model.value);
8+
9+
const debounceUpdateModel = debounce((value: string) => {
10+
model.value = value;
11+
}, 600);
12+
13+
watch(localInput, (newValue) => {
14+
debounceUpdateModel(newValue);
815
});
916
</script>
1017

1118
<template>
1219
<div role="search" aria-label="filter" class="filter-input">
13-
<input type="search" :placeholder="placeholder" aria-label="filter by name" class="form-control-static filter-input" v-model="model" />
20+
<input type="search" :placeholder="props.placeholder" :aria-label="props.ariaLabel" class="form-control-static filter-input" v-model="localInput" />
1421
</div>
1522
</template>
1623

src/Frontend/src/components/messages/HeadersView.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const filteredHeaders = computed(() => {
2828
<div class="text-search-container">
2929
<div class="text-search">
3030
<div class="filter-group">
31-
<FilterInput v-model="searchTerm" aria-label="Filter by name" :placeholder="'Search for a header key or value...'" />
31+
<FilterInput v-model="searchTerm" :aria-label="`Search for a header key or value`" :placeholder="'Search for a header key or value...'" />
3232
</div>
3333
</div>
3434
</div>

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: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
<script setup lang="ts">
2+
import { Endpoint } from "@/resources/SequenceDiagram/Endpoint";
3+
import { Endpoint_Width, 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_Gap = 30;
26+
const Endpoint_Image_Width = 20;
27+
28+
const store = useSequenceDiagramStore();
29+
const { startX, endpoints } = storeToRefs(store);
30+
31+
const epRefs = ref<SVGTextElement[]>([]);
32+
const endpointItems = computed(() =>
33+
endpoints.value.map((x, index) => {
34+
const endpoint = x as EndpointWithLocation;
35+
const el = epRefs.value[index];
36+
if (el) {
37+
const bounds = el.getBBox();
38+
const previousEndpoint = index > 0 ? endpointItems.value[index - 1] : undefined;
39+
endpoint.width = Math.max(Endpoint_Width, bounds.width);
40+
endpoint.textWidth = bounds.width;
41+
endpoint.x = (previousEndpoint?.x ?? startX.value) + (previousEndpoint?.width ?? 0) + Endpoint_Gap;
42+
43+
if (!endpoint.surround && el.isConnected) {
44+
const style = getComputedStyle(el);
45+
const padding_top = parseInt(style.getPropertyValue("padding-top"));
46+
const padding_left = parseInt(style.getPropertyValue("padding-left"));
47+
const padding_right = parseInt(style.getPropertyValue("padding-right"));
48+
const padding_bottom = parseInt(style.getPropertyValue("padding-bottom"));
49+
endpoint.surround = {
50+
x: endpoint.x - endpoint.width / 2 - padding_left,
51+
y: bounds.y - padding_top,
52+
width: endpoint.width + padding_left + padding_right,
53+
height: bounds.height + padding_top + padding_bottom,
54+
fill: style.getPropertyValue("background-color"),
55+
rx: style.getPropertyValue("border-radius"),
56+
strokeWidth: style.getPropertyValue("border-top-width"),
57+
stroke: style.getPropertyValue("border-top-color"),
58+
};
59+
}
60+
}
61+
return endpoint;
62+
})
63+
);
64+
65+
watch(endpointItems, () => {
66+
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));
67+
const lastEndpoint = endpointItems.value[endpointItems.value.length - 1];
68+
store.setMaxWidth((lastEndpoint.x ?? 0) + lastEndpoint.width);
69+
});
70+
71+
watch(startX, () => {
72+
epRefs.value = [];
73+
endpoints.value.forEach((endpoint) => ((endpoint as EndpointWithLocation).surround = undefined));
74+
});
75+
76+
function setEndpointRef(el: SVGTextElement, index: number) {
77+
if (el) epRefs.value[index] = el;
78+
}
79+
</script>
80+
81+
<template>
82+
<g v-for="(endpoint, i) in endpointItems" :key="endpoint.name" transform="translate(0,15)">
83+
<rect
84+
v-if="endpoint.surround"
85+
:x="endpoint.surround.x"
86+
:y="endpoint.surround.y"
87+
:width="endpoint.surround.width"
88+
:height="endpoint.surround.height"
89+
:fill="endpoint.surround.fill"
90+
:rx="endpoint.surround.rx"
91+
:stroke-width="endpoint.surround.strokeWidth"
92+
:stroke="endpoint.surround.stroke"
93+
></rect>
94+
<g :transform="`translate(${(endpoint.x ?? Endpoint_Width / 2) - ((endpoint.textWidth ?? 0) + Endpoint_Image_Width) / 2}, 0)`">
95+
<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>
96+
<text :x="Endpoint_Image_Width" y="10" alignment-baseline="middle" text-anchor="start" :ref="(el) => setEndpointRef(el as SVGTextElement, i)">{{ endpoint.name }}</text>
97+
</g>
98+
</g>
99+
</template>
100+
101+
<style scoped>
102+
text {
103+
background: var(--gray90);
104+
border-radius: 5px;
105+
padding: 0.5em;
106+
}
107+
</style>
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
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+
//determine which side of the handler to render the messageType on. If it's the left side (for a right arrow) then we apply a negative offset
45+
const messageTypeOffset = handler.direction === Direction.Right ? ((messageTypeElement?.getBBox().width ?? 0) + 24) * -1 : 20;
46+
if (messageTypeOffset < 0) {
47+
store.setStartX(-1 * messageTypeOffset);
48+
}
49+
50+
return {
51+
id: handler.id,
52+
endpointName: handler.endpoint.name,
53+
incomingId: handler.route?.name,
54+
left: (endpoint?.centre ?? 0) - Handler_Width / 2,
55+
right: (endpoint?.centre ?? 0) + Handler_Width / 2,
56+
y,
57+
height,
58+
fill,
59+
icon,
60+
iconSize,
61+
messageType: handler.name,
62+
messageTypeOffset,
63+
messageTypeHighlight: handler.route?.name === highlightId.value,
64+
};
65+
});
66+
67+
store.setMaxHeight(nextY);
68+
store.setHandlerLocations(result.map((handler) => ({ id: handler.id, endpointName: handler.endpointName, left: handler.left, right: handler.right, y: handler.y, height: handler.height })));
69+
70+
return result;
71+
});
72+
73+
function setMessageTypeRef(el: SVGTextElement, index: number) {
74+
if (el) messageTypeRefs.value[index] = el;
75+
}
76+
</script>
77+
78+
<template>
79+
<g v-for="(handler, i) in handlerItems" :key="handler.id" :transform="`translate(${handler.left}, ${handler.y})`">
80+
<!--Handler Activation Box-->
81+
<rect :width="Handler_Width" :height="handler.height" :class="handler.incomingId && 'clickable'" :fill="handler.fill" @mouseover="() => store.setHighlightId(handler.incomingId)" @mouseleave="() => store.setHighlightId()" />
82+
<path v-if="handler.icon" :d="handler.icon" fill="white" :transform="`translate(${Handler_Width / 2 - handler.iconSize / 2}, ${handler.height / 2 - handler.iconSize / 2})`" />
83+
<!--Message Type and Icon-->
84+
<g
85+
v-if="handler.messageType"
86+
:transform="`translate(${handler.messageTypeOffset}, 4)`"
87+
class="clickable"
88+
:fill="handler.messageTypeHighlight ? 'var(--highlight)' : 'var(--gray40)'"
89+
@mouseover="() => store.setHighlightId(handler.incomingId)"
90+
@mouseleave="() => store.setHighlightId()"
91+
>
92+
<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" />
93+
<text x="14" y="10" alignment-baseline="middle" :ref="(el) => setMessageTypeRef(el as SVGTextElement, i)">{{ handler.messageType }}</text>
94+
</g>
95+
</g>
96+
</template>
97+
98+
<style scoped>
99+
.clickable {
100+
cursor: pointer;
101+
}
102+
</style>

0 commit comments

Comments
 (0)