Skip to content

Commit 97543c0

Browse files
PhilBastiandependabot[bot]johnsimons
authored
MassTransit retry functionality (#2220)
* don't show unmonitored endpoints on the monitoring screen, even if they have failed messages * show masstransit message if pending retries is enabled * Adding masstransit connector tab * Hide "Open in ServiceInsight" and "Flow diagram" in ServicePulse * replace non-production references for masstransit with early access * allow for no monitoring url in settings * Don't add proxy config for monitoring if monitoring is disabled --------- Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: John Simons <[email protected]>
1 parent d6a0b3a commit 97543c0

26 files changed

+449
-132
lines changed

src/Frontend/src/components/LicenseNotifications.vue

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,13 @@ import { useShowToast } from "@/composables/toast";
66
import { TYPE } from "vue-toastification";
77
import routeLinks from "@/router/routeLinks";
88
import { useRouter } from "vue-router";
9+
import { useConfiguration } from "@/composables/configuration";
910
1011
const router = useRouter();
1112
const { license, getOrUpdateLicenseStatus } = useLicense();
1213
14+
const configuration = useConfiguration();
15+
1316
function displayWarningMessage(licenseStatus: LicenseStatus) {
1417
const configurationRootLink = router.resolve(routeLinks.configuration.root).href;
1518
switch (licenseStatus) {
@@ -19,7 +22,9 @@ function displayWarningMessage(licenseStatus: LicenseStatus) {
1922
break;
2023
}
2124
case "ValidWithExpiringTrial": {
22-
const trialExpiring = `<div><strong>Non-production development license expiring</strong><div>Your non-production development license will expire soon. To continue using the Particular Service Platform you'll need to extend your license.</div><a href="${license.license_extension_url}" class="btn btn-warning"><i class="fa fa-external-link-alt"></i> Extend your license</a><a href="${configurationRootLink}" class="btn btn-light">View license details</a></div>`;
25+
const trialExpiring = configuration.value?.mass_transit_connector
26+
? `<div><strong>Early Access license expiring</strong><div>Your Early Access license will expire soon. To continue using the Particular Service Platform you'll need to extend your license.</div><a href="${license.license_extension_url}" class="btn btn-warning"><i class="fa fa-external-link-alt"></i> Extend your license</a><a href="${configurationRootLink}" class="btn btn-light">View license details</a></div>`
27+
: `<div><strong>Non-production development license expiring</strong><div>Your non-production development license will expire soon. To continue using the Particular Service Platform you'll need to extend your license.</div><a href="${license.license_extension_url}" class="btn btn-warning"><i class="fa fa-external-link-alt"></i> Extend your license</a><a href="${configurationRootLink}" class="btn btn-light">View license details</a></div>`;
2328
useShowToast(TYPE.WARNING, "", trialExpiring, true);
2429
break;
2530
}

src/Frontend/src/components/PageFooter.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { monitoringUrl, serviceControlUrl } from "../composables/serviceServiceC
55
import { license, licenseStatus } from "../composables/serviceLicense";
66
import { LicenseStatus } from "@/resources/LicenseInfo";
77
import routeLinks from "@/router/routeLinks";
8+
import { useConfiguration } from "@/composables/configuration";
89
910
const isMonitoringEnabled = computed(() => {
1011
return monitoringUrl.value !== "!" && monitoringUrl.value !== "" && monitoringUrl.value !== null && monitoringUrl.value !== undefined;
@@ -17,6 +18,8 @@ const scAddressTooltip = computed(() => {
1718
const scMonitoringAddressTooltip = computed(() => {
1819
return `Monitoring URL ${monitoringUrl.value}`;
1920
});
21+
22+
const configuration = useConfiguration();
2023
</script>
2124

2225
<template>
@@ -64,7 +67,7 @@ const scMonitoringAddressTooltip = computed(() => {
6467
</template>
6568
</div>
6669
</div>
67-
<template v-if="license.license_status !== LicenseStatus.Unavailable && licenseStatus.isTrialLicense">
70+
<template v-if="license.license_status !== LicenseStatus.Unavailable && !configuration?.mass_transit_connector && licenseStatus.isTrialLicense">
6871
<div class="row trialLicenseBar">
6972
<div role="status" aria-label="trial license bar information">
7073
<RouterLink :to="routeLinks.configuration.license.link">{{ license.license_type }} license</RouterLink>, non-production use only
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<script setup lang="ts">
2+
import { useConfiguration } from "@/composables/configuration";
3+
import moment from "moment";
4+
5+
const configuration = useConfiguration();
6+
// "Wed, Jan 15th 2025 10:56:21 +10:00",
7+
function formatDate(date: string) {
8+
return moment(date).local().format("LLLL"); //.format("ddd, MMM Do YYYY HH:mm:ss Z");
9+
}
10+
</script>
11+
12+
<template>
13+
<div class="box" v-if="configuration?.mass_transit_connector !== undefined">
14+
<div class="row margin-bottom-10">
15+
<h4>
16+
Connector Version: <span class="version-format">{{ configuration.mass_transit_connector.version }}</span>
17+
</h4>
18+
</div>
19+
<div class="row margin-bottom-10">
20+
<h4>List of error queues configured in the connector.</h4>
21+
<div class="queues-container">
22+
<div class="row margin-gap hover-highlight" v-for="queue in configuration.mass_transit_connector.error_queues" :key="queue.name">
23+
<div :title="queue.name">{{ queue.name }}</div>
24+
<div class="error-color" v-if="!queue.ingesting">Not ingesting</div>
25+
<div class="ok-color" v-else>Ok</div>
26+
</div>
27+
</div>
28+
</div>
29+
<div class="row">
30+
<h4>The entries below are the most recent warning and error-level events recorded on the ServiceControl Connector.</h4>
31+
<div class="logs-container">
32+
<div class="row margin-gap hover-highlight" v-for="log in [...configuration.mass_transit_connector.logs].reverse()" :key="log.date">
33+
<div class="col-2">{{ formatDate(log.date) }}</div>
34+
<div class="col-1" :class="`${log.level.toLowerCase()}-color`">{{ log.level }}</div>
35+
<div class="col-9" :class="`${log.level.toLowerCase()}-color`">
36+
<pre>{{ log.message }}</pre>
37+
</div>
38+
</div>
39+
</div>
40+
</div>
41+
</div>
42+
<div class="box" v-else>
43+
<p>MassTransit Connector for ServiceControl is not configured.</p>
44+
<p><a target="_blank" href="https://particular.net/learn-more-about-masstransit-connector">Learn more about the MassTransit Connector.</a></p>
45+
</div>
46+
</template>
47+
48+
<style scoped>
49+
.hover-highlight:hover {
50+
background-color: #ededed;
51+
}
52+
53+
.margin-gap {
54+
margin-bottom: 3px;
55+
}
56+
57+
.queues-container {
58+
max-width: 100%;
59+
width: fit-content;
60+
padding: 0.75rem;
61+
}
62+
.queues-container .row {
63+
display: grid;
64+
grid-template-columns: 5fr minmax(10em, 1fr);
65+
}
66+
.queues-container .row div {
67+
overflow-wrap: anywhere;
68+
}
69+
70+
.logs-container {
71+
padding: 0.75rem;
72+
}
73+
.version-format {
74+
font-weight: bold;
75+
}
76+
.box > .row:not(:last-child) {
77+
padding-bottom: 0.5rem;
78+
border-bottom: 1px solid #ccc;
79+
margin-bottom: 0.5rem;
80+
}
81+
82+
.logs-container pre {
83+
all: revert;
84+
margin: 0;
85+
font-size: 0.9rem;
86+
overflow-wrap: break-word;
87+
text-wrap: auto;
88+
}
89+
90+
.warning-color {
91+
color: var(--bs-warning);
92+
}
93+
.error-color {
94+
color: var(--bs-danger);
95+
}
96+
.ok-color {
97+
color: var(--bs-success);
98+
}
99+
</style>

src/Frontend/src/components/configuration/PlatformLicense.vue

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { connectionState } from "@/composables/serviceServiceControl";
66
import BusyIndicator from "../BusyIndicator.vue";
77
import ExclamationMark from "./../../components/ExclamationMark.vue";
88
import convertToWarningLevel from "@/components/configuration/convertToWarningLevel";
9+
import { useConfiguration } from "@/composables/configuration";
10+
import { typeText } from "@/resources/LicenseInfo";
911
1012
// This is needed because the ConfigurationView.vue routerView expects this event.
1113
// The event is only actually emitted on the RetryRedirects.vue component
@@ -18,6 +20,8 @@ defineEmits<{
1820
const loading = computed(() => {
1921
return !license;
2022
});
23+
24+
const configuration = useConfiguration();
2125
</script>
2226

2327
<template>
@@ -31,7 +35,7 @@ const loading = computed(() => {
3135
<div class="box">
3236
<div class="row">
3337
<div class="license-info">
34-
<div><b>Platform license type:</b> {{ license.license_type }}{{ license.licenseEdition }}</div>
38+
<div><b>Platform license type:</b> {{ typeText(license, configuration) }}{{ license.licenseEdition }}</div>
3539

3640
<template v-if="licenseStatus.isSubscriptionLicense">
3741
<div>

src/Frontend/src/components/failedmessages/DeletedMessages.vue

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup lang="ts">
2-
import { onBeforeMount, onMounted, onUnmounted, ref, watch } from "vue";
2+
import { onMounted, onUnmounted, ref, watch } from "vue";
33
import { licenseStatus } from "../../composables/serviceLicense";
44
import { connectionState } from "../../composables/serviceServiceControl";
55
import { usePatchToServiceControl, useTypedFetchFromServiceControl } from "../../composables/serviceServiceControlUrls";
@@ -13,14 +13,13 @@ import ConfirmDialog from "../ConfirmDialog.vue";
1313
import PaginationStrip from "../../components/PaginationStrip.vue";
1414
import moment from "moment";
1515
import { ExtendedFailedMessage } from "@/resources/FailedMessage";
16-
import Configuration from "@/resources/Configuration";
1716
import { TYPE } from "vue-toastification";
1817
import FailureGroup from "@/resources/FailureGroup";
18+
import { useConfiguration } from "@/composables/configuration";
1919
2020
let pollingFaster = false;
2121
let refreshInterval: number | undefined;
2222
const perPage = 50;
23-
const configuration = ref<Configuration | null>(null);
2423
2524
const route = useRoute();
2625
const groupId = ref<string>(route.params.groupId as string);
@@ -36,6 +35,7 @@ const messageList = ref<IMessageList | undefined>();
3635
const messages = ref<ExtendedFailedMessage[]>([]);
3736
3837
watch(pageNumber, () => loadMessages());
38+
const configuration = useConfiguration();
3939
4040
function loadMessages() {
4141
let startDate = new Date(0);
@@ -162,11 +162,6 @@ function periodChanged(period: PeriodOption) {
162162
loadMessages();
163163
}
164164
165-
async function getConfiguration() {
166-
const [, data] = await useTypedFetchFromServiceControl<Configuration>("configuration");
167-
configuration.value = data;
168-
}
169-
170165
function isRestoreInProgress() {
171166
return messages.value.some((message) => message.restoreInProgress);
172167
}
@@ -196,10 +191,6 @@ onBeforeRouteLeave(() => {
196191
groupName.value = "";
197192
});
198193
199-
onBeforeMount(() => {
200-
getConfiguration();
201-
});
202-
203194
onUnmounted(() => {
204195
if (refreshInterval != null) {
205196
window.clearInterval(refreshInterval);

src/Frontend/src/components/failedmessages/EditMessageHeader.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { computed, onMounted, ref, watch } from "vue";
33
44
interface MessageHeader {
55
key: string;
6-
value: string;
6+
value?: string;
77
isChanged: boolean;
88
isMarkedAsRemoved: boolean;
99
isLocked: boolean;
@@ -14,7 +14,7 @@ const settings = defineProps<{
1414
header: MessageHeader;
1515
}>();
1616
17-
let origHeaderValue: string;
17+
let origHeaderValue: string | undefined;
1818
const header = ref<MessageHeader>(settings.header);
1919
2020
const headerValue = computed(() => settings.header.value);

src/Frontend/src/components/failedmessages/EditRetryDialog.vue

Lines changed: 51 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,30 @@ const props = defineProps<{
2424
configuration: EditAndRetryConfig;
2525
}>();
2626
27-
const panel = ref();
28-
const localMessage = ref();
27+
interface LocalMessageState {
28+
isBodyChanged: boolean;
29+
isBodyEmpty: boolean;
30+
isContentTypeSupported: boolean;
31+
bodyContentType: string | undefined;
32+
bodyUnavailable: boolean;
33+
isEvent: boolean;
34+
retried: boolean;
35+
headers: HeaderWithEditing[];
36+
messageBody: string;
37+
}
38+
39+
const panel = ref(0);
40+
const localMessage = ref<LocalMessageState>({
41+
isBodyChanged: false,
42+
isBodyEmpty: false,
43+
isContentTypeSupported: false,
44+
bodyContentType: undefined,
45+
bodyUnavailable: true,
46+
isEvent: false,
47+
retried: false,
48+
headers: [],
49+
messageBody: "",
50+
});
2951
let origMessageBody: string;
3052
3153
const showEditAndRetryConfirmation = ref(false);
@@ -79,10 +101,12 @@ function findHeadersByKey(key: string) {
79101
80102
function getContentType() {
81103
const header = findHeadersByKey("NServiceBus.ContentType");
82-
return header.value;
104+
return header?.value;
83105
}
84106
85-
function isContentTypeSupported(contentType: string) {
107+
function isContentTypeSupported(contentType: string | undefined) {
108+
if (contentType === undefined) return false;
109+
86110
if (contentType.startsWith("text/")) return true;
87111
88112
const charsetUtf8 = "; charset=utf-8";
@@ -106,7 +130,7 @@ function isContentTypeSupported(contentType: string) {
106130
107131
function getMessageIntent() {
108132
const intent = findHeadersByKey("NServiceBus.MessageIntent");
109-
return intent.value;
133+
return intent?.value;
110134
}
111135
112136
function removeHeadersMarkedAsRemoved() {
@@ -127,7 +151,17 @@ async function retryEditedMessage() {
127151
128152
function initializeMessageBodyAndHeaders() {
129153
origMessageBody = props.message.messageBody;
130-
localMessage.value = props.message;
154+
localMessage.value = {
155+
isBodyChanged: false,
156+
isBodyEmpty: false,
157+
isContentTypeSupported: false,
158+
bodyContentType: undefined,
159+
bodyUnavailable: props.message.bodyUnavailable,
160+
isEvent: false,
161+
retried: props.message.retried,
162+
headers: props.message.headers.map((header: Header) => ({ ...header })) as HeaderWithEditing[],
163+
messageBody: props.message.messageBody,
164+
};
131165
localMessage.value.isBodyEmpty = false;
132166
localMessage.value.isBodyChanged = false;
133167
@@ -136,7 +170,7 @@ function initializeMessageBodyAndHeaders() {
136170
localMessage.value.isContentTypeSupported = isContentTypeSupported(contentType);
137171
138172
const messageIntent = getMessageIntent();
139-
localMessage.value.isEvent = messageIntent.value === "Publish";
173+
localMessage.value.isEvent = messageIntent === "Publish";
140174
141175
for (let index = 0; index < props.message.headers.length; index++) {
142176
const header: HeaderWithEditing = props.message.headers[index] as HeaderWithEditing;
@@ -191,15 +225,15 @@ onMounted(() => {
191225
</div>
192226
<div class="row msg-editor-content">
193227
<div class="col-sm-12 no-side-padding">
194-
<div class="alert alert-warning" v-if="localMessage?.isEvent">
228+
<div class="alert alert-warning" v-if="localMessage.isEvent">
195229
<div class="col-sm-12">
196230
<i class="fa fa-exclamation-circle"></i> This message is an event. If it was already successfully handled by other subscribers, editing it now has the risk of changing the semantic meaning of the event and may result in
197231
altering the system behavior.
198232
</div>
199233
</div>
200-
<div class="alert alert-warning" v-if="!localMessage?.isContentTypeSupported || localMessage?.bodyUnavailable">
234+
<div class="alert alert-warning" v-if="!localMessage.isContentTypeSupported || localMessage.bodyUnavailable">
201235
<div role="status" aria-label="cannot edit message body" class="col-sm-12">
202-
<i class="fa fa-exclamation-circle"></i> Message body cannot be edited because content type "{{ localMessage?.bodyContentType }}" is not supported. Only messages with content types "application/json" and "text/xml" can be
236+
<i class="fa fa-exclamation-circle"></i> Message body cannot be edited because content type "{{ localMessage.bodyContentType }}" is not supported. Only messages with content types "application/json" and "text/xml" can be
203237
edited.
204238
</div>
205239
</div>
@@ -208,16 +242,16 @@ onMounted(() => {
208242
</div>
209243
<table role="tabpanel" class="table" v-if="panel === 1">
210244
<tbody>
211-
<tr class="interactiveList" v-for="header in localMessage?.headers" :key="header.key">
245+
<tr class="interactiveList" v-for="header in localMessage.headers" :key="header.key">
212246
<MessageHeader :header="header"></MessageHeader>
213247
</tr>
214248
</tbody>
215249
</table>
216-
<div role="tabpanel" v-if="panel === 2 && !localMessage?.bodyUnavailable" style="height: calc(100% - 260px)">
217-
<textarea aria-label="message body" class="form-control" :disabled="!localMessage?.isContentTypeSupported" v-model="localMessage.messageBody"></textarea>
218-
<span class="empty-error" v-if="localMessage?.isBodyEmpty"><i class="fa fa-exclamation-triangle"></i> Message body cannot be empty</span>
219-
<span class="reset-body" v-if="localMessage?.isBodyChanged"><i class="fa fa-undo" v-tippy="`Reset changes`"></i> <a @click="resetBodyChanges()" href="javascript:void(0)">Reset changes</a></span>
220-
<div class="alert alert-info" v-if="localMessage?.panel === 2 && localMessage.bodyUnavailable">{{ localMessage.bodyUnavailable }}</div>
250+
<div role="tabpanel" v-if="panel === 2 && !localMessage.bodyUnavailable" style="height: calc(100% - 260px)">
251+
<textarea aria-label="message body" class="form-control" :disabled="!localMessage.isContentTypeSupported" v-model="localMessage.messageBody"></textarea>
252+
<span class="empty-error" v-if="localMessage.isBodyEmpty"><i class="fa fa-exclamation-triangle"></i> Message body cannot be empty</span>
253+
<span class="reset-body" v-if="localMessage.isBodyChanged"><i class="fa fa-undo" v-tippy="`Reset changes`"></i> <a @click="resetBodyChanges()" href="javascript:void(0)">Reset changes</a></span>
254+
<div class="alert alert-info" v-if="panel === 2 && localMessage.bodyUnavailable">{{ localMessage.bodyUnavailable }}</div>
221255
</div>
222256
</div>
223257
</div>
@@ -226,7 +260,7 @@ onMounted(() => {
226260
</div>
227261
<div class="modal-footer" v-if="!showEditAndRetryConfirmation && !showCancelConfirmation">
228262
<button class="btn btn-default" @click="confirmCancel()">Cancel</button>
229-
<button class="btn btn-primary" :disabled="localMessage?.isBodyEmpty || localMessage?.bodyUnavailable" @click="confirmEditAndRetry()">Retry</button>
263+
<button class="btn btn-primary" :disabled="localMessage.isBodyEmpty || localMessage.bodyUnavailable" @click="confirmEditAndRetry()">Retry</button>
230264
</div>
231265
<div class="modal-footer cancel-confirmation" v-if="showCancelConfirmation">
232266
<div>Are you sure you want to cancel? Any changes you made will be lost.</div>

0 commit comments

Comments
 (0)