Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bugfix: auditlist was incrementing the count on every change of interval

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>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these classes are not defined anywhere. Whatever functionality they previously implemented has likely been missing since the switch from AngularJS

<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