Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
7 changes: 7 additions & 0 deletions src/Frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/Frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"codemirror": "^6.0.1",
"hex-to-css-filter": "^6.0.0",
"lodash.debounce": "^4.0.8",
"lodash.throttle": "^4.0.8",
"lossless-json": "^4.0.2",
"memoize-one": "^6.0.0",
"moment": "^2.30.1",
Expand Down
127 changes: 74 additions & 53 deletions src/Frontend/src/components/RefreshConfig.vue
Original file line number Diff line number Diff line change
@@ -1,86 +1,107 @@
<script setup lang="ts">
import { ref } from "vue";
import OnOffSwitch from "./OnOffSwitch.vue";
import { ref, watch } from "vue";
import ListFilterSelector from "@/components/audit/ListFilterSelector.vue";

const props = defineProps<{
id: string;
initialTimeout?: number;
onManualRefresh: () => void;
}>();
const props = defineProps<{ isLoading: boolean }>();
const model = defineModel<number | null>({ required: true });
const emit = defineEmits<{ (e: "manualRefresh"): Promise<void> }>();
const autoRefreshOptionsText: [number, string][] = [
[0, "Off"],
[5000, "Every 5 seconds"],
[15000, "Every 15 seconds"],
[30000, "Every 30 seconds"],
[60000, "Every 1 minute"],
[600000, "Every 10 minute"],
[1800000, "Every 30 minute"],
[3600000, "Every 1 hour"],
];

const emit = defineEmits<{ change: [newValue: number | null]; manualRefresh: [] }>();
function extracted() {
const item = autoRefreshOptionsText.find((item) => item[0] === model.value);

const autoRefresh = ref(props.initialTimeout != null);
const refreshTimeout = ref(props.initialTimeout ?? 5);
if (item) {
return item[1];
}

function toggleRefresh() {
autoRefresh.value = !autoRefresh.value;
updateTimeout();
return "Off";
}

function updateTimeout() {
validateTimeout();
emit("change", autoRefresh.value ? refreshTimeout.value * 1000 : null);
}
const selectValue = extracted();

const selectedRefresh = ref<string>(selectValue);

watch(selectedRefresh, (newValue) => {
const item = autoRefreshOptionsText.find((item) => item[1] === newValue);

function validateTimeout() {
refreshTimeout.value = Math.max(1, Math.min(600, refreshTimeout.value));
if (item) {
if (item[0] === 0) {
model.value = null;
} else {
model.value = item[0];
}
}
});
const showSpinning = ref(false);
watch(
() => props.isLoading,
(newValue) => {
if (newValue) {
showSpinning.value = true;
window.setTimeout(() => {
showSpinning.value = false;
}, 1000);
}
}
);
async function refresh() {
await emit("manualRefresh");
}
</script>

<template>
<div class="refresh-config">
<button class="fa" title="refresh" @click="() => emit('manualRefresh')">
<i class="fa fa-lg fa-refresh" />
</button>
<span>|</span>
<label>Auto-Refresh:</label>
<div>
<OnOffSwitch :id="id" @toggle="toggleRefresh" :value="autoRefresh" />
<button class="btn btn-sm" title="refresh" @click="refresh"><i class="fa fa-refresh" :class="{ spinning: showSpinning }" /> Refresh List</button>
<div class="filter">
<div class="filter-label">Auto-Refresh:</div>
<div class="filter-component">
<ListFilterSelector :items="autoRefreshOptionsText.map((i) => i[1])" v-model="selectedRefresh" item-name="result" :can-clear="false" :show-clear="false" :show-filter="false" />
</div>
</div>
<input type="number" v-model="refreshTimeout" min="1" max="600" v-on:change="updateTimeout" />
<span class="unit">s</span>
</div>
</template>

<style scoped>
.refresh-config {
display: flex;
align-items: center;
gap: 0.5em;
}

.refresh-config .unit {
margin-left: -0.45em;
gap: 1em;
margin-bottom: 0.5em;
}

.refresh-config label {
margin: 0;
}

.refresh-config input {
width: 3.5em;
.filter {
display: flex;
align-items: center;
}

.refresh-config button {
background: none;
border: none;
width: 2em;
.filter-label {
font-weight: bold;
}

.refresh-config button .fa {
transition: all 0.15s ease-in-out;
transition: rotate 0.05s ease-in-out;
transform-origin: center;
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

.refresh-config button:hover .fa {
color: #00a3c4;
transform: scale(1.1);
.fa-refresh {
display: inline-block;
}

.refresh-config button:active .fa {
transform: rotate(25deg);
text-shadow: #929e9e 0.25px 0.25px;
/* You can add this class dynamically when needed */
.fa-refresh.spinning {
animation: spin 1s linear infinite;
}
</style>
32 changes: 27 additions & 5 deletions src/Frontend/src/components/audit/AuditList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,33 @@ import ResultsCount from "@/components/ResultsCount.vue";
import { dotNetTimespanToMilliseconds, formatDotNetTimespan } from "@/composables/formatUtils.ts";
import "@vuepic/vue-datepicker/dist/main.css";
import FiltersPanel from "@/components/audit/FiltersPanel.vue";
import { onBeforeMount, watch } from "vue";
import { onBeforeMount, onUnmounted, ref, watch } from "vue";
import RefreshConfig from "../RefreshConfig.vue";
import useAutoRefresh from "@/composables/autoRefresh.ts";
import throttle from "lodash/throttle";

const store = useAuditStore();
const { messages, totalCount, sortBy, messageFilterString, selectedEndpointName, itemsPerPage, dateRange } = storeToRefs(store);
const route = useRoute();
const router = useRouter();
const autoRefreshValue = ref<number | null>(null);
const isLoading = ref(false);

const dataRetriever = useAutoRefresh(
throttle(async () => {
isLoading.value = true;
try {
await store.refresh();
} finally {
isLoading.value = false;
}
}, 2000),
null
);

onUnmounted(() => {
dataRetriever.updateTimeout(null);
});

function statusToName(messageStatus: MessageStatus) {
switch (messageStatus) {
Expand Down Expand Up @@ -78,7 +98,7 @@ onBeforeMount(() => {

//without setTimeout, this happens before the store is properly initialised, and therefore the query route values aren't applied to the refresh
//TODO: is there a better way to achieve this?
setTimeout(async () => await Promise.all([store.refresh(), store.loadEndpoints()]), 0);
setTimeout(async () => await Promise.all([dataRetriever.executeAndResetTimer(), store.loadEndpoints()]), 0);

firstLoad = false;
});
Expand All @@ -87,7 +107,7 @@ watch(
() => router.currentRoute.value.query,
async () => {
setQuery();
await store.refresh();
await dataRetriever.executeAndResetTimer();
},
{ deep: true }
);
Expand All @@ -113,7 +133,7 @@ const watchHandle = watch([() => route.query, itemsPerPage, sortBy, messageFilte
},
});

await store.refresh();
await dataRetriever.executeAndResetTimer();
});

function setQuery() {
Expand All @@ -132,12 +152,14 @@ function setQuery() {

watchHandle.resume();
}

watch(autoRefreshValue, (newValue) => dataRetriever.updateTimeout(newValue));
</script>

<template>
<div>
<div class="header">
<RefreshConfig id="auditListRefresh" @change="store.updateRefreshTimer" @manual-refresh="store.refresh" />
<RefreshConfig v-model="autoRefreshValue" :isLoading="isLoading" @manual-refresh="dataRetriever.executeAndResetTimer()" />
<div class="row">
<FiltersPanel />
</div>
Expand Down
8 changes: 3 additions & 5 deletions src/Frontend/src/stores/AuditStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { ref } from "vue";
import type { SortInfo } from "@/components/SortInfo";
import Message from "@/resources/Message";
import { EndpointsView } from "@/resources/EndpointView.ts";
import useAutoRefresh from "@/composables/autoRefresh";

export type DateRange = [fromDate: Date, toDate: Date] | [];

Expand Down Expand Up @@ -39,7 +38,7 @@ export const useAuditStore = defineStore("AuditStore", () => {
}
}

const dataRetriever = useAutoRefresh(async () => {
async function refresh() {
try {
const [fromDate, toDate] = dateRange.value;
const from = fromDate?.toISOString() ?? "";
Expand All @@ -53,11 +52,10 @@ export const useAuditStore = defineStore("AuditStore", () => {
messages.value = [];
throw e;
}
}, null);
}

return {
refresh: dataRetriever.executeAndResetTimer,
updateRefreshTimer: dataRetriever.updateTimeout,
refresh,
loadEndpoints,
sortBy: sortByInstances,
messages,
Expand Down