diff --git a/src/Frontend/package-lock.json b/src/Frontend/package-lock.json index 302d45f2e..eab07dd81 100644 --- a/src/Frontend/package-lock.json +++ b/src/Frontend/package-lock.json @@ -126,7 +126,6 @@ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -616,7 +615,6 @@ "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz", "integrity": "sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg==", "license": "MIT", - "peer": true, "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", @@ -629,7 +627,6 @@ "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.8.1.tgz", "integrity": "sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==", "license": "MIT", - "peer": true, "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.4.0", @@ -666,7 +663,6 @@ "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz", "integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==", "license": "MIT", - "peer": true, "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", @@ -690,7 +686,6 @@ "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.5.tgz", "integrity": "sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA==", "license": "MIT", - "peer": true, "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.35.0", @@ -702,7 +697,6 @@ "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.11.tgz", "integrity": "sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==", "license": "MIT", - "peer": true, "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", @@ -714,7 +708,6 @@ "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", "license": "MIT", - "peer": true, "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } @@ -724,7 +717,6 @@ "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.1.tgz", "integrity": "sha512-RmTOkE7hRU3OVREqFVITWHz6ocgBjv08GoePscAakgVQfciA3SGCEk7mb9IzwW61cKKmlTpHXG6DUE5Ubx+MGQ==", "license": "MIT", - "peer": true, "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", @@ -820,7 +812,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -844,7 +835,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -1529,7 +1519,6 @@ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-7.1.0.tgz", "integrity": "sha512-fNxRUk1KhjSbnbuBxlWSnBLKLBNun52ZBTcs22H/xEEzM6Ap81ZFTQ4bZBxVQGQgVY0xugKGoRcCbaKjLQ3XZA==", "license": "MIT", - "peer": true, "dependencies": { "@fortawesome/fontawesome-common-types": "7.1.0" }, @@ -2043,7 +2032,6 @@ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", "license": "MIT", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" @@ -2349,7 +2337,6 @@ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -2616,7 +2603,6 @@ "integrity": "sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.47.0", "@typescript-eslint/types": "8.47.0", @@ -3033,7 +3019,6 @@ "resolved": "https://registry.npmjs.org/@vue-flow/core/-/core-1.48.0.tgz", "integrity": "sha512-keW9HGaEZEe4SKYtrzp5E+qSGJ5/z+9i2yRDtCr3o72IUnS0Ns1qQNsIbGGz0ygpKzg6LdtbVLWeYAvl3dzLQA==", "license": "MIT", - "peer": true, "dependencies": { "@vueuse/core": "^10.5.0", "d3-drag": "^3.0.0", @@ -3186,7 +3171,6 @@ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.25.tgz", "integrity": "sha512-PUgKp2rn8fFsI++lF2sO7gwO2d9Yj57Utr5yEsDf3GNaQcowCLKL7sf+LvVFvtJDXUp/03+dC6f2+LCv5aK1ag==", "license": "MIT", - "peer": true, "dependencies": { "@babel/parser": "^7.28.5", "@vue/compiler-core": "3.5.25", @@ -3507,7 +3491,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3770,7 +3753,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", @@ -4008,7 +3990,6 @@ "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==", "license": "MIT", - "peer": true, "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/commands": "^6.0.0", @@ -4235,7 +4216,6 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -4720,7 +4700,6 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -4781,7 +4760,6 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -6839,7 +6817,6 @@ "resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz", "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==", "license": "MIT", - "peer": true, "dependencies": { "@vue/devtools-api": "^7.7.7" }, @@ -6924,7 +6901,6 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -7607,8 +7583,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/superjson": { "version": "2.2.2", @@ -7733,7 +7708,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -7883,7 +7857,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8030,7 +8003,6 @@ "integrity": "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -8346,7 +8318,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -8360,7 +8331,6 @@ "integrity": "sha512-pmW4GCKQ8t5Ko1jYjC3SqOr7TUKN7uHOHB/XGsAIb69eYu6d1ionGSsb5H9chmPf+WeXt0VE7jTXsB1IvWoNbw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/expect": "4.0.12", "@vitest/mocker": "4.0.12", @@ -8462,7 +8432,6 @@ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.25.tgz", "integrity": "sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==", "license": "MIT", - "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.25", "@vue/compiler-sfc": "3.5.25", @@ -8610,7 +8579,6 @@ "integrity": "sha512-L/G9IUjOWhBU0yun89rv8fKqmKC+T0HfhrFjlIml71WpfBv9eb4E9Bev8FMbyueBIU9vxQqbd+oOsVcDa5amGw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@volar/typescript": "2.4.23", "@vue/language-core": "3.1.5" diff --git a/src/Frontend/src/components/failedmessages/DeletedMessages.vue b/src/Frontend/src/components/failedmessages/DeletedMessages.vue index 1be80920a..cb1af3217 100644 --- a/src/Frontend/src/components/failedmessages/DeletedMessages.vue +++ b/src/Frontend/src/components/failedmessages/DeletedMessages.vue @@ -1,134 +1,31 @@ @@ -241,7 +114,7 @@ onMounted(() => { @@ -250,11 +123,12 @@ onMounted(() => {
- + +
- +
-import { onMounted, onUnmounted, ref, useTemplateRef, watch } from "vue"; +import { computed, onBeforeMount, ref, useTemplateRef, watch } from "vue"; import { useShowToast } from "../../composables/toast"; import { downloadFileFromString } from "../../composables/fileDownloadCreator"; -import { onBeforeRouteLeave, useRoute } from "vue-router"; +import { onBeforeRouteLeave } from "vue-router"; import createMessageGroupClient from "./messageGroupClient"; import LicenseNotExpired from "../../components/LicenseNotExpired.vue"; import OrderBy from "@/components/OrderBy.vue"; @@ -10,33 +10,32 @@ import ServiceControlAvailable from "../ServiceControlAvailable.vue"; import MessageList, { IMessageList } from "./MessageList.vue"; import ConfirmDialog from "../ConfirmDialog.vue"; import PaginationStrip from "../../components/PaginationStrip.vue"; -import { ExtendedFailedMessage, FailedMessageStatus } from "@/resources/FailedMessage"; -import SortOptions, { SortDirection } from "@/resources/SortOptions"; +import { FailedMessageStatus } from "@/resources/FailedMessage"; +import SortOptions from "@/resources/SortOptions"; import { TYPE } from "vue-toastification"; import GroupOperation from "@/resources/GroupOperation"; import { faArrowDownAZ, faArrowDownZA, faArrowDownShortWide, faArrowDownWideShort, faArrowRotateRight, faTrash, faDownload } from "@fortawesome/free-solid-svg-icons"; import ActionButton from "@/components/ActionButton.vue"; -import { useServiceControlStore } from "@/stores/ServiceControlStore"; import { useMessageStore } from "@/stores/MessageStore"; +import { useRecoverabilityStore } from "@/stores/RecoverabilityStore"; +import { useStoreAutoRefresh } from "@/composables/useAutoRefresh"; +import { storeToRefs } from "pinia"; +import LoadingSpinner from "../LoadingSpinner.vue"; + +const POLLING_INTERVAL_NORMAL = 5000; +const POLLING_INTERVAL_FAST = 1000; -const serviceControlStore = useServiceControlStore(); const messageStore = useMessageStore(); const messageGroupClient = createMessageGroupClient(); +const loading = ref(false); +const { autoRefresh, isRefreshing, updateInterval } = useStoreAutoRefresh("messagesStore", useRecoverabilityStore, POLLING_INTERVAL_NORMAL); +const { store } = autoRefresh(); +const { messages, groupId, groupName, totalCount, pageNumber } = storeToRefs(store); -let pollingFaster = false; -let refreshInterval: number | undefined; -let sortMethod: SortOptions | undefined; -const perPage = 50; -const route = useRoute(); -const groupId = ref(route.params.groupId as string); -const groupName = ref(""); -const pageNumber = ref(1); -const totalCount = ref(0); const showDelete = ref(false); const showConfirmRetryAll = ref(false); const showConfirmDeleteAll = ref(false); const messageList = useTemplateRef("messageList"); -const messages = ref([]); const sortOptions: SortOptions[] = [ { description: "Time of failure", @@ -50,73 +49,15 @@ const sortOptions: SortOptions[] = [ }, ]; -watch(pageNumber, () => loadMessages()); - -function sortGroups(sort: SortOptions) { - sortMethod = sort; - loadMessages(); -} - -function loadMessages() { - loadPagedMessages(groupId.value, pageNumber.value, sortMethod && sortMethod.description.replaceAll(" ", "_").toLowerCase(), sortMethod?.dir); -} - -async function loadGroupDetails(groupId: string) { - const response = await serviceControlStore.fetchFromServiceControl(`recoverability/groups/id/${groupId}`); - const data = await response.json(); - groupName.value = data.title; -} - -function loadPagedMessages(groupId: string, page: number, sortBy?: string, direction?: SortDirection) { - sortBy ??= "time_of_failure"; - direction ??= SortDirection.Descending; - - let loadGroupDetailsPromise; - if (groupId && !groupName.value) { - loadGroupDetailsPromise = loadGroupDetails(groupId); - } - - async function loadMessages() { - try { - const [response, data] = await serviceControlStore.fetchTypedFromServiceControl( - `${groupId ? `recoverability/groups/${groupId}/` : ""}errors?status=${FailedMessageStatus.Unresolved}&page=${page}&per_page=${perPage}&sort=${sortBy}&direction=${direction}` - ); - totalCount.value = parseInt(response.headers.get("Total-Count") ?? ""); - if (messages.value.length && data.length) { - // merge the previously selected messages into the new list so we can replace them - messages.value.forEach((previousMessage) => { - const receivedMessage = data.find((m) => m.id === previousMessage.id); - if (receivedMessage) { - if (previousMessage.last_modified === receivedMessage.last_modified) { - receivedMessage.retryInProgress = previousMessage.retryInProgress; - receivedMessage.deleteInProgress = previousMessage.deleteInProgress; - } - - receivedMessage.selected = previousMessage.selected; - } - }); - } - messages.value = data; - } catch (err) { - console.log(err); - const result = { - message: "error", - }; - return result; - } - } - - const loadMessagesPromise = loadMessages(); - - if (loadGroupDetailsPromise) { - return Promise.all([loadGroupDetailsPromise, loadMessagesPromise]); - } - - return loadMessagesPromise; +async function sortGroups(sort: SortOptions) { + loading.value = true; + await store.setSort(sort.description.replaceAll(" ", "_").toLowerCase(), sort.dir); + loading.value = false; } async function retryRequested(id: string) { - changeRefreshInterval(1000); + // We're starting a retry, poll more frequently + updateInterval(POLLING_INTERVAL_FAST); useShowToast(TYPE.INFO, "Info", "Message retry requested..."); await messageStore.retryMessages([id]); const message = messages.value.find((m) => m.id === id); @@ -127,7 +68,8 @@ async function retryRequested(id: string) { } async function retrySelected() { - changeRefreshInterval(1000); + // We're starting a retry, poll more frequently + updateInterval(POLLING_INTERVAL_FAST); const selectedMessages = messageList.value?.getSelectedMessages() ?? []; useShowToast(TYPE.INFO, "Info", "Retrying " + selectedMessages.length + " messages..."); await messageStore.retryMessages(selectedMessages.map((m) => m.id)); @@ -210,14 +152,12 @@ function isAnythingSelected() { } async function deleteSelectedMessages() { - changeRefreshInterval(1000); + // We're starting a delete, poll more frequently + updateInterval(POLLING_INTERVAL_FAST); const selectedMessages = messageList.value?.getSelectedMessages() ?? []; useShowToast(TYPE.INFO, "Info", "Deleting " + selectedMessages.length + " messages..."); - await serviceControlStore.patchToServiceControl( - "errors/archive", - selectedMessages.map((m) => m.id) - ); + await store.deleteById(selectedMessages.map((m) => m.id)); messageList.value?.deselectAll(); selectedMessages.forEach((m) => (m.deleteInProgress = true)); } @@ -234,47 +174,29 @@ async function deleteGroup() { messages.value.forEach((m) => (m.deleteInProgress = true)); } -function isRetryOrDeleteOperationInProgress() { - return messages.value.some((message) => { - return message.retryInProgress || message.deleteInProgress; - }); -} - -function changeRefreshInterval(milliseconds: number) { - if (refreshInterval != null) { - window.clearInterval(refreshInterval); - } - - refreshInterval = window.setInterval(() => { - // If we're currently polling at 5 seconds and there is a retry or delete in progress, then change the polling interval to poll every 1 second - if (!pollingFaster && isRetryOrDeleteOperationInProgress()) { - changeRefreshInterval(1000); - pollingFaster = true; - } else if (pollingFaster && !isRetryOrDeleteOperationInProgress()) { - // if we're currently polling every 1 second but all retries or deletes are done, change polling frequency back to every 5 seconds - changeRefreshInterval(5000); - pollingFaster = false; - } - - loadMessages(); - }, milliseconds); -} - onBeforeRouteLeave(() => { groupId.value = ""; groupName.value = ""; }); -onUnmounted(() => { - if (refreshInterval != null) { - window.clearInterval(refreshInterval); +const isRetryOrDeleteOperationInProgress = computed(() => messages.value.some((message) => message.retryInProgress || message.deleteInProgress)); +watch(isRetryOrDeleteOperationInProgress, (retryOrDeleteOperationInProgress) => { + // If there is a retry or delete in progress, then change the polling interval to poll every 1 second + if (retryOrDeleteOperationInProgress) { + updateInterval(POLLING_INTERVAL_FAST); + } else { + // if all retries or deletes are done, change polling frequency back to every 5 seconds + updateInterval(POLLING_INTERVAL_NORMAL); } }); -onMounted(() => { - loadMessages(); - - changeRefreshInterval(5000); +onBeforeMount(async () => { + loading.value = true; + //set status before mount to ensure no other controls/processes can cause extra refreshes during mount + await store.setMessageStatus(FailedMessageStatus.Unresolved); +}); +watch(isRefreshing, () => { + if (!isRefreshing.value && loading.value) loading.value = false; }); @@ -308,11 +230,12 @@ onMounted(() => {
- + +
- +
-import { onMounted, onUnmounted, ref, useTemplateRef, watch } from "vue"; +import { onBeforeMount, ref, useTemplateRef, watch } from "vue"; import { useShowToast } from "../../composables/toast"; -import { useCookies } from "vue3-cookies"; import OrderBy from "@/components/OrderBy.vue"; import LicenseNotExpired from "../../components/LicenseNotExpired.vue"; import ServiceControlAvailable from "../ServiceControlAvailable.vue"; import MessageList, { IMessageList } from "./MessageList.vue"; import ConfirmDialog from "../ConfirmDialog.vue"; import PaginationStrip from "../../components/PaginationStrip.vue"; -import { ExtendedFailedMessage, FailedMessageStatus } from "@/resources/FailedMessage"; -import SortOptions, { SortDirection } from "@/resources/SortOptions"; -import QueueAddress from "@/resources/QueueAddress"; +import { FailedMessageStatus } from "@/resources/FailedMessage"; +import SortOptions from "@/resources/SortOptions"; import { TYPE } from "vue-toastification"; import GroupOperation from "@/resources/GroupOperation"; import { faArrowDownAZ, faArrowDownZA, faArrowDownShortWide, faArrowDownWideShort, faInfoCircle, faExternalLink, faFilter, faTimes, faArrowRightRotate } from "@fortawesome/free-solid-svg-icons"; import FAIcon from "@/components/FAIcon.vue"; import ActionButton from "@/components/ActionButton.vue"; import { faCheckSquare } from "@fortawesome/free-regular-svg-icons"; -import { useServiceControlStore } from "@/stores/ServiceControlStore"; import { useConfigurationStore } from "@/stores/ConfigurationStore"; import { storeToRefs } from "pinia"; +import { useStoreAutoRefresh } from "@/composables/useAutoRefresh"; +import { RetryPeriodOption, useRecoverabilityStore } from "@/stores/RecoverabilityStore"; -const serviceControlStore = useServiceControlStore(); +const loading = ref(false); +const { autoRefresh, isRefreshing } = useStoreAutoRefresh("messagesStore", useRecoverabilityStore, 5000); +const { store } = autoRefresh(); +const { messages, totalCount, pageNumber, selectedPeriod, selectedQueue, endpoints } = storeToRefs(store); const configurationStore = useConfigurationStore(); const { isMassTransitConnected } = storeToRefs(configurationStore); -let refreshInterval: number | undefined; -let sortMethod: SortOptions | undefined; -const perPage = 50; -const cookies = useCookies().cookies; -const selectedPeriod = ref("All Pending Retries"); -const endpoints = ref([]); const messageList = useTemplateRef("messageList"); -const messages = ref([]); -const selectedQueue = ref("empty"); const showConfirmRetry = ref(false); const showConfirmResolve = ref(false); const showConfirmResolveAll = ref(false); const showCantRetryAll = ref(false); const showRetryAllConfirm = ref(false); -const pageNumber = ref(1); -const totalCount = ref(0); -const isInitialLoad = ref(true); const sortOptions: SortOptions[] = [ { description: "Time of failure", @@ -59,76 +50,6 @@ const sortOptions: SortOptions[] = [ iconDesc: faArrowDownWideShort, }, ]; -const periodOptions = ["All Pending Retries", "Retried in the last 2 Hours", "Retried in the last 1 Day", "Retried in the last 7 Days"]; - -watch(pageNumber, () => loadPendingRetryMessages()); - -async function loadEndpoints() { - const [, data] = await serviceControlStore.fetchTypedFromServiceControl("errors/queues/addresses"); - endpoints.value = data.map((endpoint) => endpoint.physical_address); -} - -function clearSelectedQueue() { - selectedQueue.value = "empty"; - loadPendingRetryMessages(); -} - -function loadPendingRetryMessages() { - let startDate = new Date(0); - const endDate = new Date(); - - switch (selectedPeriod.value) { - case "Retried in the last 2 Hours": - startDate = new Date(); - startDate.setHours(startDate.getHours() - 2); - break; - - case "Retried in the last 1 Day": - startDate = new Date(); - startDate.setHours(startDate.getHours() - 24); - break; - - case "Retried in the last 7 days": - startDate = new Date(); - startDate.setHours(startDate.getHours() - 24 * 7); - break; - } - - return loadPagedPendingRetryMessages(pageNumber.value, selectedQueue.value, startDate, endDate, sortMethod?.description.replaceAll(" ", "_").toLowerCase(), sortMethod?.dir); -} - -async function loadPagedPendingRetryMessages(page: number, searchPhrase: string, startDate: Date, endDate: Date, sortBy?: string, direction?: SortDirection) { - sortBy ??= "time_of_failure"; - direction ??= SortDirection.Descending; - if (searchPhrase === "empty") searchPhrase = ""; - - try { - const [response, data] = await serviceControlStore.fetchTypedFromServiceControl( - `errors?status=${FailedMessageStatus.RetryIssued}&page=${page}&per_page=${perPage}&sort=${sortBy}&direction=${direction}&queueaddress=${searchPhrase}&modified=${startDate.toISOString()}...${endDate.toISOString()}` - ); - totalCount.value = parseInt(response.headers.get("Total-Count") ?? "0"); - - messages.value.forEach((previousMessage: ExtendedFailedMessage) => { - const receivedMessage = data.find((m) => m.id === previousMessage.id); - if (receivedMessage) { - if (previousMessage.last_modified === receivedMessage.last_modified) { - receivedMessage.submittedForRetrial = previousMessage.submittedForRetrial; - receivedMessage.resolved = previousMessage.resolved; - } - - receivedMessage.selected = previousMessage.selected; - } - }); - - messages.value = data; - } catch (err) { - console.log(err); - const result = { - message: "error", - }; - return result; - } -} function numberDisplayed() { return messageList.value?.numberDisplayed(); @@ -150,10 +71,7 @@ async function retrySelectedMessages() { const selectedMessages = messageList.value?.getSelectedMessages() ?? []; useShowToast(TYPE.INFO, "Info", "Selected messages were submitted for retry..."); - await serviceControlStore.postToServiceControl( - "pendingretries/retry", - selectedMessages.map((m) => m.id) - ); + await store.retryById(selectedMessages.map((m) => m.id)); messageList.value?.deselectAll(); selectedMessages.forEach((m) => (m.submittedForRetrial = true)); @@ -163,30 +81,20 @@ async function resolveSelectedMessages() { const selectedMessages = messageList.value?.getSelectedMessages() ?? []; useShowToast(TYPE.INFO, "Info", "Selected messages were marked as resolved."); - await serviceControlStore.patchToServiceControl("pendingretries/resolve", { uniquemessageids: selectedMessages.map((m) => m.id) }); + await store.resolveById(selectedMessages.map((m) => m.id)); messageList.value?.deselectAll(); selectedMessages.forEach((m) => (m.resolved = true)); } async function resolveAllMessages() { useShowToast(TYPE.INFO, "Info", "All filtered messages were marked as resolved."); - await serviceControlStore.patchToServiceControl("pendingretries/resolve", { from: new Date(0).toISOString(), to: new Date().toISOString() }); + await store.resolveAll(); messageList.value?.deselectAll(); messageList.value?.resolveAll(); } async function retryAllMessages() { - let url = "pendingretries/retry"; - const data: { from: string; to: string; queueaddress?: string } = { - from: new Date(0).toISOString(), - to: new Date(0).toISOString(), - }; - if (selectedQueue.value !== "empty") { - url = "pendingretries/queues/retry"; - data.queueaddress = selectedQueue.value; - } - - await serviceControlStore.postToServiceControl(url, data); + await store.retryAll(); messages.value.forEach((message) => { message.selected = false; message.submittedForRetrial = true; @@ -202,44 +110,25 @@ function retryAllClicked() { } } -function sortGroups(sort: SortOptions) { - sortMethod = sort; - - if (!isInitialLoad.value) { - loadPendingRetryMessages(); - } +async function sortGroups(sort: SortOptions) { + loading.value = true; + await store.setSort(sort.description.replaceAll(" ", "_").toLowerCase(), sort.dir); + loading.value = false; } -function periodChanged(period: string) { - selectedPeriod.value = period; - cookies.set("pending_retries_period", period); - - loadPendingRetryMessages(); +async function periodChanged(period: RetryPeriodOption) { + loading.value = true; + await store.setPeriod(period); + loading.value = false; } -onUnmounted(() => { - if (refreshInterval != null) { - window.clearInterval(refreshInterval); - } +onBeforeMount(async () => { + loading.value = true; + //set status before mount to ensure no other controls/processes can cause extra refreshes during mount + await store.setMessageStatus(FailedMessageStatus.RetryIssued); }); - -onMounted(() => { - let cookiePeriod = cookies.get("pending_retries_period"); - if (!cookiePeriod) { - cookiePeriod = periodOptions[0]; //default All Pending Retries - } - - selectedPeriod.value = cookiePeriod; - - loadEndpoints(); - - loadPendingRetryMessages(); - - refreshInterval = window.setInterval(() => { - loadPendingRetryMessages(); - }, 5000); - - isInitialLoad.value = false; +watch(isRefreshing, () => { + if (!isRefreshing.value && loading.value) loading.value = false; }); @@ -263,14 +152,14 @@ onMounted(() => {
- - +
@@ -283,7 +172,7 @@ onMounted(() => { @@ -307,7 +196,7 @@ onMounted(() => {
- +
1) { - days = parseInt(parts[0], 10); - timeComponent = parts[1]; - } - - const [hours, minutes, seconds] = timeComponent.split(":").map(Number); - return { days, hours, minutes, seconds }; -} +import { parseTimeSpan } from "./formatter"; function getFriendly(time: number, text: string): string { return time > 0 ? `${time}${text}` : ""; } export function getTimeoutFriendly(delivery_delay: string): string { - const { days, hours, minutes, seconds } = parseDeliveryDelay(delivery_delay); + const { days, hours, minutes, seconds } = parseTimeSpan(delivery_delay); return `${getFriendly(days, "d")}${getFriendly(hours, "h")}${getFriendly(minutes, "m")}${getFriendly(seconds, "s")}`; } diff --git a/src/Frontend/src/composables/formatter.ts b/src/Frontend/src/composables/formatter.ts index 12a01ef5b..a535ef869 100644 --- a/src/Frontend/src/composables/formatter.ts +++ b/src/Frontend/src/composables/formatter.ts @@ -76,3 +76,24 @@ function formatTimeValue(timeValue: number, displayTwoDigits = false) { const strValue = Math.floor(timeValue); return `${displayTwoDigits ? ("0" + strValue).slice(-2) : strValue.toLocaleString()}`; } + +export function parseTimeSpan(timeSpan: string) { + // Split on period first to handle multi-digit days + const parts = timeSpan.split("."); + let days = 0; + let timeComponent = timeSpan; + + if (parts.length > 1) { + days = parseInt(parts[0], 10); + timeComponent = parts[1]; + } + + const [hours, minutes, seconds] = timeComponent.split(":").map(Number); + return { days, hours, minutes, seconds }; +} + +export function timeSpanToDuration(timeSpan: string | undefined) { + if (!timeSpan) return dayjs.duration("PT0S"); + + return dayjs.duration(parseTimeSpan(timeSpan)); +} diff --git a/src/Frontend/src/stores/ConnectionsAndStatsStore.ts b/src/Frontend/src/stores/ConnectionsAndStatsStore.ts index 48086baf8..cea10048f 100644 --- a/src/Frontend/src/stores/ConnectionsAndStatsStore.ts +++ b/src/Frontend/src/stores/ConnectionsAndStatsStore.ts @@ -18,7 +18,7 @@ export const useConnectionsAndStatsStore = defineStore("ConnectionsAndStatsStore const { count: requiresFullFailureDetailsSubscriberCount, inc, dec } = useCounter(0); function requiresFullFailureDetails() { - onMounted(() => inc()); + onMounted(() => inc()); //NOTE: not forcing a refresh here since we expect the view utilising this store to also setup a refresh on mount onUnmounted(() => dec()); } diff --git a/src/Frontend/src/stores/MessageStore.ts b/src/Frontend/src/stores/MessageStore.ts index 36050ce6d..1c79009fb 100644 --- a/src/Frontend/src/stores/MessageStore.ts +++ b/src/Frontend/src/stores/MessageStore.ts @@ -14,6 +14,7 @@ import { EditAndRetryConfig } from "@/resources/Configuration"; import EditRetryResponse from "@/resources/EditRetryResponse"; import { EditedMessage } from "@/resources/EditMessage"; import useEnvironmentAndVersionsAutoRefresh from "@/composables/useEnvironmentAndVersionsAutoRefresh"; +import { timeSpanToDuration } from "@/composables/formatter"; interface Model { id?: string; @@ -79,7 +80,7 @@ export const useMessageStore = defineStore("MessageStore", () => { const areSimpleHeadersSupported = environmentStore.serviceControlIsGreaterThan("5.2.0"); const { configuration } = storeToRefs(configStore); - const error_retention_period = computed(() => dayjs.duration(configuration.value?.data_retention?.error_retention_period ?? "PT0S").asHours()); + const error_retention_period = computed(() => timeSpanToDuration(configuration.value?.data_retention?.error_retention_period).asHours()); watch(serviceControlUrl, loadConfig, { immediate: true }); async function loadConfig() { diff --git a/src/Frontend/src/stores/RecoverabilityStore.ts b/src/Frontend/src/stores/RecoverabilityStore.ts new file mode 100644 index 000000000..9260865bb --- /dev/null +++ b/src/Frontend/src/stores/RecoverabilityStore.ts @@ -0,0 +1,281 @@ +import { acceptHMRUpdate, defineStore, storeToRefs } from "pinia"; +import { computed, ref, watch, shallowReadonly } from "vue"; +import { useServiceControlStore } from "./ServiceControlStore"; +import { useCookies } from "vue3-cookies"; +import { useRoute } from "vue-router"; +import { ExtendedFailedMessage, FailedMessageStatus } from "@/resources/FailedMessage"; +import { SortDirection } from "@/resources/SortOptions"; +import dayjs from "@/utils/dayjs"; +import { useConfigurationStore } from "./ConfigurationStore"; +import FailureGroup from "@/resources/FailureGroup"; +import QueueAddress from "@/resources/QueueAddress"; +import { timeSpanToDuration } from "@/composables/formatter"; + +const deletedPeriodOptions = ["All Deleted", "Deleted in the last 2 Hours", "Deleted in the last 1 Day", "Deleted in the last 7 days"] as const; +const retryPeriodOptions = ["All Pending Retries", "Retried in the last 2 Hours", "Retried in the last 1 Day", "Retried in the last 7 Days"] as const; +export type DeletedPeriodOption = (typeof deletedPeriodOptions)[number]; +export type RetryPeriodOption = (typeof retryPeriodOptions)[number]; + +export const useRecoverabilityStore = defineStore("RecoverabilityStore", () => { + const route = useRoute(); + const groupId = ref(route.params.groupId as string); + const groupName = ref(""); + const pageNumber = ref(1); + const totalCount = ref(0); + const messages = ref([]); + let messageStatus: FailedMessageStatus | null = null; + const perPage = 50; + const sortBy = ref(""); + const sortDirection = ref(SortDirection.Descending); + const startDate = ref(new Date(0)); + const endDate = ref(new Date()); + const dateRange = computed(() => `${startDate.value.toISOString()}...${endDate.value.toISOString()}`); + const selectedPeriod = ref("Deleted in the last 7 days"); + const selectedQueue = ref("empty"); + const endpoints = ref([]); + + const serviceControlStore = useServiceControlStore(); + const configurationStore = useConfigurationStore(); + const { configuration } = storeToRefs(configurationStore); + + const cookies = useCookies(); + + //keep track of first load after a status change, so that loading of subcomponents that initiate a refresh (e.g. OrderBy) don't cause a double refresh on "page" load + let loaded = false; + + watch(pageNumber, () => refresh()); + + let controller: AbortController | null; + async function refresh() { + try { + if (!messageStatus) return; + if (messageStatus === FailedMessageStatus.Archived || messageStatus === FailedMessageStatus.RetryIssued) { + endDate.value = new Date(); + const newStartDate = new Date(); + + switch (selectedPeriod.value) { + case "All Deleted": + case "All Pending Retries": + newStartDate.setHours(newStartDate.getHours() - 24 * 365); + break; + case "Deleted in the last 2 Hours": + case "Retried in the last 2 Hours": + newStartDate.setHours(newStartDate.getHours() - 2); + break; + case "Deleted in the last 1 Day": + case "Retried in the last 1 Day": + newStartDate.setHours(newStartDate.getHours() - 24); + break; + case "Deleted in the last 7 days": + case "Retried in the last 7 Days": + newStartDate.setHours(newStartDate.getHours() - 24 * 7); + break; + } + startDate.value = newStartDate; + } + const additionalQuery = (() => { + switch (messageStatus) { + case FailedMessageStatus.Archived: + return `&modified=${dateRange.value}`; + case FailedMessageStatus.RetryIssued: { + const searchPhrase = selectedQueue.value === "empty" ? "" : selectedQueue.value; + return `&queueaddress=${searchPhrase}&modified=${dateRange.value}`; + } + default: + return ""; + } + })(); + + controller = new AbortController(); + if (groupId.value && !groupName.value) loadGroupDetails(groupId.value); + const [response, data] = await serviceControlStore.fetchTypedFromServiceControl( + `${groupId.value ? `recoverability/groups/${groupId.value}/` : ""}errors?status=${messageStatus}&page=${pageNumber.value}&per_page=${perPage}&sort=${sortBy.value}&direction=${sortDirection.value}${additionalQuery}`, + controller.signal + ); + controller = null; + + totalCount.value = parseInt(response.headers.get("Total-Count") ?? "0"); + + if (messages.value.length && data.length) { + // merge the previously selected messages into the new list so we can replace them + messages.value.forEach((previousMessage) => { + const receivedMessage = data.find((m) => m.id === previousMessage.id); + if (receivedMessage) { + if (previousMessage.last_modified === receivedMessage.last_modified) { + receivedMessage.retryInProgress = previousMessage.retryInProgress; + receivedMessage.deleteInProgress = previousMessage.deleteInProgress; + receivedMessage.restoreInProgress = previousMessage.restoreInProgress; + receivedMessage.submittedForRetrial = previousMessage.submittedForRetrial; + receivedMessage.resolved = previousMessage.resolved; + } + + receivedMessage.selected = previousMessage.selected; + } + }); + } + + messages.value = updateMessages(data); + loaded = true; + } catch (err) { + if (err instanceof Error && err.name === "AbortError") return; + console.error(err); + } + } + + async function loadGroupDetails(groupId: string) { + const [, data] = await serviceControlStore.fetchTypedFromServiceControl(`${messageStatus === FailedMessageStatus.Archived ? "archive" : "recoverability"}/groups/id/${groupId}`, controller?.signal); + groupName.value = data.title; + } + + function updateMessages(messages: ExtendedFailedMessage[]) { + switch (messageStatus) { + case FailedMessageStatus.Archived: + //check deletion time + messages.forEach((message) => { + message.error_retention_period = timeSpanToDuration(configuration.value?.data_retention.error_retention_period).asHours(); + const countdown = dayjs(message.last_modified).add(message.error_retention_period, "hours"); + message.delete_soon = countdown < dayjs(); + message.deleted_in = countdown.format(); + }); + return messages; + default: + return messages; + } + } + + async function setMessageStatus(status: FailedMessageStatus) { + if (controller) { + // need to cancel any existing fetch which otherwise will set messages of the incorrect status + controller.abort(); + } + loaded = false; + messageStatus = status; + messages.value = []; + //reset all the paging variables + switch (messageStatus) { + case FailedMessageStatus.Archived: { + sortBy.value = ""; + let deletedMessagePeriod = cookies.cookies.get("all_deleted_messages_period") as DeletedPeriodOption; + if (!deletedMessagePeriod) { + deletedMessagePeriod = deletedPeriodOptions[deletedPeriodOptions.length - 1]; //default is last 7 days + } + selectedPeriod.value = deletedMessagePeriod; + break; + } + case FailedMessageStatus.RetryIssued: + { + sortBy.value = "time_of_failure"; + let retryMessagePeriod = cookies.cookies.get("pending_retries_period") as RetryPeriodOption; + if (!retryMessagePeriod) { + retryMessagePeriod = retryPeriodOptions[0]; //default All Pending Retries + } + selectedPeriod.value = retryMessagePeriod; + const [, data] = await serviceControlStore.fetchTypedFromServiceControl("errors/queues/addresses"); + endpoints.value = data.map((endpoint) => endpoint.physical_address); + } + break; + default: + sortBy.value = "time_of_failure"; + } + + groupId.value = route.params.groupId as string; + + //Refresh is handled by the autoRefresh setup on the respective views, so doing it here also would cause a double refresh + //await refresh(); + } + + async function setSort(sort: string, direction?: SortDirection) { + sortBy.value = sort; + sortDirection.value = direction ?? SortDirection.Descending; + if (!loaded) return; + if (controller) { + // need to cancel any existing fetch which otherwise will set messages of the incorrect sort + controller.abort(); + } + messages.value = []; + await refresh(); + } + + async function setPeriod(period: DeletedPeriodOption | RetryPeriodOption) { + selectedPeriod.value = period; + cookies.cookies.set(messageStatus === FailedMessageStatus.Archived ? "all_deleted_messages_period" : "pending_retries_period", period); + if (!loaded) return; + if (controller) { + // need to cancel any existing fetch which otherwise will set messages of the incorrect period + controller.abort(); + } + messages.value = []; + await refresh(); + } + + async function deleteById(ids: string[]) { + await serviceControlStore.patchToServiceControl("errors/archive", ids); + } + + async function restoreById(ids: string[]) { + await serviceControlStore.patchToServiceControl("errors/unarchive", ids); + } + + async function retryById(ids: string[]) { + await serviceControlStore.postToServiceControl("pendingretries/retry", ids); + } + + async function resolveById(ids: string[]) { + await serviceControlStore.patchToServiceControl("pendingretries/resolve", { uniquemessageids: ids }); + } + + async function retryAll() { + let url = "pendingretries/retry"; + const data: { from: string; to: string; queueaddress?: string } = { + from: new Date(0).toISOString(), + to: new Date(0).toISOString(), + }; + if (selectedQueue.value !== "empty") { + url = "pendingretries/queues/retry"; + data.queueaddress = selectedQueue.value; + } + + await serviceControlStore.postToServiceControl(url, data); + } + + async function resolveAll() { + await serviceControlStore.patchToServiceControl("pendingretries/resolve", { from: new Date(0).toISOString(), to: new Date().toISOString() }); + } + + async function clearSelectedQueue() { + selectedQueue.value = "empty"; + await refresh(); + } + + return { + refresh, + messages, + perPage, + pageNumber, + totalCount, + groupId, + groupName, + sortDirection, + deletedPeriodOptions, + retryPeriodOptions, + selectedPeriod, + selectedQueue: shallowReadonly(selectedQueue), + endpoints: shallowReadonly(endpoints), + setSort, + setPeriod, + setMessageStatus, + deleteById, + restoreById, + retryById, + resolveById, + retryAll, + resolveAll, + clearSelectedQueue, + }; +}); + +if (import.meta.hot) { + import.meta.hot.accept(acceptHMRUpdate(useRecoverabilityStore, import.meta.hot)); +} + +export type RecoverabilityStore = ReturnType; diff --git a/src/Frontend/src/stores/ServiceControlStore.ts b/src/Frontend/src/stores/ServiceControlStore.ts index f4dfe3954..a48307be4 100644 --- a/src/Frontend/src/stores/ServiceControlStore.ts +++ b/src/Frontend/src/stores/ServiceControlStore.ts @@ -44,8 +44,8 @@ export const useServiceControlStore = defineStore("ServiceControlStore", () => { return await fetch(`${getServiceControlUrl()}${suffix}`, requestOptions); } - async function fetchTypedFromServiceControl(suffix: string): Promise<[Response, T]> { - const response = await fetch(`${getServiceControlUrl()}${suffix}`); + async function fetchTypedFromServiceControl(suffix: string, signal?: AbortSignal): Promise<[Response, T]> { + const response = await fetch(`${getServiceControlUrl()}${suffix}`, { signal }); if (!response.ok) throw new Error(response.statusText ?? "No response"); const data = await response.json();