Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/Frontend/src/components/audit/AuditList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const { messages, totalCount, sortBy, messageFilterString, selectedEndpointName,
const route = useRoute();
const router = useRouter();
const autoRefreshValue = ref<number | null>(null);
const { refreshNow, isRefreshing, updateInterval, start, stop } = useFetchWithAutoRefresh("audit-list", store.refresh, 3000);
const { refreshNow, isRefreshing, updateInterval, isActive, start, stop } = useFetchWithAutoRefresh("audit-list", store.refresh, 0);
const firstLoad = ref(true);

onBeforeMount(() => {
Expand Down Expand Up @@ -82,7 +82,7 @@ watch(autoRefreshValue, (newValue) => {
updateInterval(newValue || 0);
if (newValue === null || newValue === 0) {
stop();
} else {
} else if (!isActive.value) {
start();
}
});
Expand Down
248 changes: 30 additions & 218 deletions src/Frontend/src/components/failedmessages/DeletedMessageGroups.vue
Original file line number Diff line number Diff line change
@@ -1,169 +1,36 @@
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import { ref, watch } from "vue";
import { useRouter } from "vue-router";
import { useShowToast } from "../../composables/toast";
import createMessageGroupClient from "./messageGroupClient";
import { useCookies } from "vue3-cookies";
import NoData from "../NoData.vue";
import TimeSince from "../TimeSince.vue";
import LicenseNotExpired from "../../components/LicenseNotExpired.vue";
import ServiceControlAvailable from "../ServiceControlAvailable.vue";
import ConfirmDialog from "../ConfirmDialog.vue";
import routeLinks from "@/router/routeLinks";
import FailureGroupView from "@/resources/FailureGroupView";
import { TYPE } from "vue-toastification";
import MetadataItem from "@/components/MetadataItem.vue";
import ActionButton from "@/components/ActionButton.vue";
import { faArrowRotateRight, faEnvelope } from "@fortawesome/free-solid-svg-icons";
import { faClock } from "@fortawesome/free-regular-svg-icons";
import { useServiceControlStore } from "@/stores/ServiceControlStore";
const statusesForRestoreOperation = ["restorestarted", "restoreprogressing", "restorefinalizing", "restorecompleted"] as const;
type RestoreOperationStatus = (typeof statusesForRestoreOperation)[number];
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const otherStatuses = ["none", "working"] as const;
type Status = RestoreOperationStatus | (typeof otherStatuses)[number];
interface WorkflowState {
status: Status;
total?: number;
failed?: boolean;
message?: string;
}
interface ExtendedFailureGroupView extends FailureGroupView {
index: number;
need_user_acknowledgement?: boolean;
workflow_state: WorkflowState;
operation_remaining_count?: number;
hover2?: boolean;
hover3?: boolean;
operation_start_time?: string;
last_operation_completion_time?: string;
}
import { useDeletedMessageGroupsStore, statusesForRestoreOperation, ExtendedFailureGroupView, Status } from "@/stores/DeletedMessageGroupsStore";
import { useStoreAutoRefresh } from "@/composables/useAutoRefresh";
import { storeToRefs } from "pinia";
let pollingFaster = false;
const archiveGroups = ref<ExtendedFailureGroupView[]>([]);
const undismissedRestoreGroups = ref<ExtendedFailureGroupView[]>([]);
const loadingData = ref(true);
const initialLoadComplete = ref(false);
const emit = defineEmits<{
InitialLoadComplete: [];
}>();
let refreshInterval: number | undefined = undefined;
const route = useRoute();
const { autoRefresh, isRefreshing, updateInterval } = useStoreAutoRefresh("deletedMessageGroups", useDeletedMessageGroupsStore, 5000);
const { store } = autoRefresh();
const { archiveGroups, classifiers, selectedClassifier } = storeToRefs(store);
const router = useRouter();
const showRestoreGroupModal = ref(false);
const selectedGroup = ref<ExtendedFailureGroupView>();
const serviceControlStore = useServiceControlStore();
const messageGroupClient = createMessageGroupClient();
const groupRestoreSuccessful = ref<boolean | null>(null);
const selectedClassifier = ref<string | null>(null);
const classifiers = ref<string[]>([]);
async function getGroupingClassifiers() {
const [, data] = await serviceControlStore.fetchTypedFromServiceControl<string[]>("recoverability/classifiers");
classifiers.value = data;
}
function saveDefaultGroupingClassifier(classifier: string) {
const cookies = useCookies().cookies;
cookies.set("archived_groups_classification", classifier);
}
async function classifierChanged(classifier: string) {
saveDefaultGroupingClassifier(classifier);
store.setGrouping(classifier);
selectedClassifier.value = classifier;
archiveGroups.value = [];
await loadArchivedMessageGroups(classifier);
}
async function getArchiveGroups(classifier: string) {
//get all deleted message groups
const [, result] = await serviceControlStore.fetchTypedFromServiceControl<FailureGroupView[]>(`errors/groups/${classifier}`);
if (result.length === 0 && undismissedRestoreGroups.value.length > 0) {
undismissedRestoreGroups.value.forEach((deletedGroup) => {
deletedGroup.need_user_acknowledgement = true;
deletedGroup.workflow_state.status = "restorecompleted";
});
}
undismissedRestoreGroups.value.forEach((deletedGroup) => {
if (!result.find((group) => group.id === deletedGroup.id)) {
deletedGroup.need_user_acknowledgement = true;
deletedGroup.workflow_state.status = "restorecompleted";
}
});
// need a map in some ui state for controlling animations
const mappedResults = result
.filter((group) => !undismissedRestoreGroups.value.find((deletedGroup) => deletedGroup.id === group.id))
.map(initializeGroupState)
.concat(undismissedRestoreGroups.value);
let maxIndex = archiveGroups.value.reduce((currentMax, currentGroup) => Math.max(currentMax, currentGroup.index), 0);
mappedResults.forEach((serverGroup) => {
const previousGroup = archiveGroups.value.find((oldGroup) => oldGroup.id === serverGroup.id);
if (previousGroup) {
serverGroup.index = previousGroup.index;
} else {
serverGroup.index = ++maxIndex;
}
});
archiveGroups.value = mappedResults.sort((group1, group2) => {
return group1.index - group2.index;
});
}
function initializeGroupState(group: FailureGroupView): ExtendedFailureGroupView {
return {
index: 0,
workflow_state: createWorkflowState("none"),
...group,
};
}
function loadDefaultGroupingClassifier() {
const cookies = useCookies().cookies;
const cookieGrouping = cookies.get("archived_groups_classification");
if (cookieGrouping) {
return cookieGrouping;
}
return null;
}
async function loadArchivedMessageGroups(groupBy: string | null = null) {
loadingData.value = true;
if (!initialLoadComplete.value || !groupBy) {
groupBy = loadDefaultGroupingClassifier();
}
await getArchiveGroups(groupBy ?? (route.query.deletedGroupBy as string));
loadingData.value = false;
initialLoadComplete.value = true;
emit("InitialLoadComplete");
}
//create workflow state
function createWorkflowState(optionalStatus?: Status, optionalTotal?: number, optionalFailed?: boolean): WorkflowState {
if (optionalTotal && optionalTotal <= 1) {
optionalTotal = optionalTotal * 100;
}
return {
status: optionalStatus ?? "working",
total: optionalTotal ?? 0,
failed: optionalFailed ?? false,
};
await store.refresh();
}
//Restore operation
Expand All @@ -176,18 +43,14 @@ function showRestoreGroupDialog(group: ExtendedFailureGroupView) {
async function restoreGroup() {
const group = selectedGroup.value;
if (group) {
// We're starting a restore, poll more frequently
changeRefreshInterval(1000);
undismissedRestoreGroups.value.push(group);
group.workflow_state = { status: "restorestarted", message: "Restore request initiated..." };
group.operation_start_time = new Date().toUTCString();
const result = await messageGroupClient.restoreGroup(group.id);
if (messageGroupClient.isError(result)) {
const { result, errorMessage } = await store.restoreGroup(group);
if (!result) {
groupRestoreSuccessful.value = false;
useShowToast(TYPE.ERROR, "Error", `Failed to restore the group: ${result.message}`);
useShowToast(TYPE.ERROR, "Error", `Failed to restore the group: ${errorMessage}`);
} else {
// We're starting a restore, poll more frequently
pollingFaster = true;
updateInterval(1000);
groupRestoreSuccessful.value = true;
useShowToast(TYPE.INFO, "Info", "Group restore started...");
}
Expand All @@ -211,20 +74,6 @@ function getClassesForRestoreOperation(stepStatus: Status, currentStatus: Status
return getClasses(stepStatus, currentStatus, statusesForRestoreOperation);
}
const acknowledgeGroup = function (dismissedGroup: FailureGroupView) {
undismissedRestoreGroups.value.splice(
undismissedRestoreGroups.value.findIndex((group) => {
return group.id === dismissedGroup.id;
}),
1
);
archiveGroups.value.splice(
archiveGroups.value.findIndex((group) => group.id === dismissedGroup.id),
1
);
};
function isBeingRestored(status: Status) {
return (statusesForRestoreOperation as readonly Status[]).includes(status);
}
Expand All @@ -237,44 +86,17 @@ function isRestoreInProgress() {
return archiveGroups.value.some((group) => group.workflow_state.status !== "none" && group.workflow_state.status !== "restorecompleted");
}
function changeRefreshInterval(milliseconds: number) {
if (refreshInterval) {
clearInterval(refreshInterval);
}
refreshInterval = window.setInterval(() => {
// If we're currently polling at 5 seconds and there is a restore in progress, then change the polling interval to poll every 1 second
if (!pollingFaster && isRestoreInProgress()) {
changeRefreshInterval(1000);
pollingFaster = true;
} else if (pollingFaster && !isRestoreInProgress()) {
// if we're currently polling every 1 second and all restores are done, change polling frequency back to every 5 seconds
changeRefreshInterval(5000);
pollingFaster = false;
}
loadArchivedMessageGroups();
}, milliseconds);
}
onUnmounted(() => {
if (refreshInterval) {
clearInterval(refreshInterval);
watch(isRefreshing, () => {
// If we're currently polling at 5 seconds and there is a restore in progress, then change the polling interval to poll every 1 second
if (!pollingFaster && isRestoreInProgress()) {
pollingFaster = true;
updateInterval(1000);
} else if (pollingFaster && !isRestoreInProgress()) {
// if we're currently polling every 1 second and all restores are done, change polling frequency back to every 5 seconds
pollingFaster = false;
updateInterval(5000);
}
});
onMounted(async () => {
await getGroupingClassifiers();
let savedClassifier = loadDefaultGroupingClassifier();
if (!savedClassifier) {
savedClassifier = classifiers.value[0];
}
selectedClassifier.value = savedClassifier;
await loadArchivedMessageGroups();
changeRefreshInterval(5000);
});
</script>

<template>
Expand Down Expand Up @@ -307,31 +129,23 @@ onMounted(async () => {
<div>
<div class="row">
<div class="col-sm-12">
<no-data v-if="archiveGroups.length === 0 && !loadingData" title="message groups" message="There are currently no grouped message failures"></no-data>
<no-data v-if="archiveGroups.length === 0 && !isRefreshing" title="message groups" message="There are currently no grouped message failures"></no-data>
</div>
</div>

<div class="row">
<div class="col-sm-12 no-mobile-side-padding">
<div v-if="archiveGroups.length > 0">
<div
:class="`row box box-group wf-${group.workflow_state.status} repeat-modify deleted-message-group`"
v-for="(group, index) in archiveGroups"
:key="index"
:disabled="group.count == 0"
@mouseenter="group.hover2 = true"
@mouseleave="group.hover2 = false"
@click.prevent="navigateToGroup(group.id)"
>
<div :class="`row box box-group wf-${group.workflow_state.status} repeat-modify deleted-message-group`" v-for="(group, index) in archiveGroups" :key="index" :disabled="group.count == 0" @click.prevent="navigateToGroup(group.id)">
<div class="col-sm-12 no-mobile-side-padding">
<div class="row">
<div class="col-sm-12 no-side-padding">
<div class="row box-header">
<div class="col-sm-12 no-side-padding">
<p class="lead break" v-bind:class="{ 'msg-type-hover': group.hover2, 'msg-type-hover-off': group.hover3 }">{{ group.title }}</p>
<p class="lead break">{{ group.title }}</p>
<p class="metadata" v-if="!isBeingRestored(group.workflow_state.status)">
<MetadataItem :icon="faEnvelope">
{{ group.count }} message<span v-if="group.count > 1">s</span>
<span>{{ group.count }} message<span v-if="group.count > 1">s</span></span>
<span v-if="group.operation_remaining_count"> (currently restoring {{ group.operation_remaining_count }} </span>
</MetadataItem>

Expand All @@ -357,8 +171,6 @@ onMounted(async () => {
size="sm"
:icon="faArrowRotateRight"
:disabled="group.count === 0 || isBeingRestored(group.workflow_state.status)"
@mouseenter="group.hover3 = true"
@mouseleave="group.hover3 = false"
v-if="archiveGroups.length > 0"
@click.stop="showRestoreGroupDialog(group)"
>
Expand All @@ -378,7 +190,7 @@ onMounted(async () => {
</li>
<li v-if="group.workflow_state.status === 'restorecompleted'">
<div class="retry-completed bulk-retry-progress-status">Restore request completed</div>
<button type="button" class="btn btn-default btn-primary btn-xs btn-retry-dismiss" v-if="group.need_user_acknowledgement == true" @click.stop="acknowledgeGroup(group)">Dismiss</button>
<button type="button" class="btn btn-default btn-primary btn-xs btn-retry-dismiss" v-if="group.need_user_acknowledgement == true" @click.stop="store.acknowledgeGroup(group)">Dismiss</button>
</li>
</ul>
<div class="op-metadata">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ export interface ErrorResponse {
message: string;
}

class MessageGroupClient {
export class MessageGroupClient {
serviceControlStore: ServiceControlStore;
constructor() {
constructor(store?: ServiceControlStore) {
//this module is only called from within view setup or other pinia stores, so this call is lifecycle safe
this.serviceControlStore = useServiceControlStore();
this.serviceControlStore = store ?? useServiceControlStore();
}

public async getExceptionGroups(classifier: string = "") {
Expand Down
11 changes: 9 additions & 2 deletions src/Frontend/src/composables/autoRefresh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default function useFetchWithAutoRefresh(name: string, fetch: () => Promi
await fetch();
isRefreshing.value = false;
};
const { pause, resume } = useTimeoutPoll(
const { isActive, pause, resume } = useTimeoutPoll(
fetchWrapper,
interval,
{ immediate: false, immediateCallback: true } // we control first fetch manually
Expand Down Expand Up @@ -59,8 +59,15 @@ export default function useFetchWithAutoRefresh(name: string, fetch: () => Promi
};

const updateInterval = (newIntervalMs: number) => {
if (interval.value === newIntervalMs) return;

interval.value = newIntervalMs;
console.debug(`updated polling ${name} to ${newIntervalMs}ms`);
pause();
if (newIntervalMs > 0) {
resume();
}
};

return { refreshNow: fetchWrapper, isRefreshing: shallowReadonly(isRefreshing), updateInterval, start, stop };
return { refreshNow: fetchWrapper, isRefreshing: shallowReadonly(isRefreshing), updateInterval, isActive, start, stop };
}
Loading