Skip to content
Closed
Show file tree
Hide file tree
Changes from 14 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
1 change: 1 addition & 0 deletions src/Frontend/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ declare global {
service_control_url: string;
monitoring_urls: string[];
showPendingRetry: boolean;
showAllMessages: boolean;
};
}
}
2 changes: 2 additions & 0 deletions src/Frontend/src/components/PageHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ import EventsMenuItem from "@/components/events/EventsMenuItem.vue";
import DashboardMenuItem from "@/components/dashboard/DashboardMenuItem.vue";
import FeedbackButton from "@/components/FeedbackButton.vue";
import ThroughputMenuItem from "@/views/throughputreport/ThroughputMenuItem.vue";
import AuditMenuItem from "./audit/AuditMenuItem.vue";

// prettier-ignore
const menuItems = computed(
() => [
DashboardMenuItem,
HeartbeatsMenuItem,
...(useIsMonitoringEnabled() ? [MonitoringMenuItem] : []),
...(window.defaultConfig.showAllMessages ? [AuditMenuItem] : []),
FailedMessagesMenuItem,
CustomChecksMenuItem,
EventsMenuItem,
Expand Down
86 changes: 86 additions & 0 deletions src/Frontend/src/components/RefreshConfig.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<script setup lang="ts">
import { ref } from "vue";
import OnOffSwitch from "./OnOffSwitch.vue";

const props = defineProps<{
id: string;
initialTimeout?: number;
onManualRefresh: () => void;
}>();

const emit = defineEmits<{ change: [newValue: number | null]; manualRefresh: [] }>();

const autoRefresh = ref(props.initialTimeout != null);
const refreshTimeout = ref(props.initialTimeout ?? 5);

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

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

function validateTimeout() {
refreshTimeout.value = Math.max(1, Math.min(600, refreshTimeout.value));
}
</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" />
</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;
}

.refresh-config label {
margin: 0;
}

.refresh-config input {
width: 3.5em;
}

.refresh-config button {
background: none;
border: none;
width: 2em;
}

.refresh-config button .fa {
transition: all 0.15s ease-in-out;
transition: rotate 0.05s ease-in-out;
transform-origin: center;
}

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

.refresh-config button:active .fa {
transform: rotate(25deg);
text-shadow: #929e9e 0.25px 0.25px;
}
</style>
213 changes: 213 additions & 0 deletions src/Frontend/src/components/audit/AuditList.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
<script setup lang="ts">
import routeLinks from "@/router/routeLinks";
import { ColumnNames, useAuditStore } from "@/stores/AuditStore";
import { storeToRefs } from "pinia";
import { useRoute } from "vue-router";
import DataView from "../DataView.vue";
import SortableColumn from "../SortableColumn.vue";
import { MessageStatus } from "@/resources/Message";
import moment from "moment";
import { useFormatTime } from "@/composables/formatter";
import RefreshConfig from "../RefreshConfig.vue";

const route = useRoute();
const store = useAuditStore();
const { messages, sortByInstances, itemsPerPage } = storeToRefs(store);

function statusToName(messageStatus: MessageStatus) {
switch (messageStatus) {
case MessageStatus.Successful:
return "Successful";
case MessageStatus.ResolvedSuccessfully:
return "Successful after retries";
case MessageStatus.Failed:
return "Failed";
case MessageStatus.ArchivedFailure:
return "Failed message deleted";
case MessageStatus.RepeatedFailure:
return "Repeated Failures";
case MessageStatus.RetryIssued:
return "Retry requested";
}
}

function statusToIcon(messageStatus: MessageStatus) {
switch (messageStatus) {
case MessageStatus.Successful:
return "fa successful";
case MessageStatus.ResolvedSuccessfully:
return "fa resolved-successfully";
case MessageStatus.Failed:
return "fa failed";
case MessageStatus.ArchivedFailure:
return "fa archived";
case MessageStatus.RepeatedFailure:
return "fa repeated-failure";
case MessageStatus.RetryIssued:
return "fa retry-issued";
}
}

function friendlyTypeName(messageType: string) {
if (messageType == null) return null;

const typeClass = messageType.split(",")[0];
const typeName = typeClass.split(".").reverse()[0];
return typeName.replace(/\+/g, ".");
}

function formatDotNetTimespan(timespan: string) {
//assuming if we have days in the timespan then something is very, very wrong
const [hh, mm, ss] = timespan.split(":");
const time = useFormatTime(((parseInt(hh) * 60 + parseInt(mm)) * 60 + parseFloat(ss)) * 1000);
return `${time.value} ${time.unit}`;
}
</script>

<template>
<RefreshConfig id="auditListRefresh" @change="store.updateRefreshTimer" @manual-refresh="store.refresh" />
<section role="table" aria-label="endpoint-instances">
<!--Table headings-->
<div role="row" aria-label="column-headers" class="row table-head-row" :style="{ borderTop: 0 }">
<div role="columnheader" :aria-label="ColumnNames.Status" class="status">
<SortableColumn :sort-by="ColumnNames.Status" v-model="sortByInstances" :default-ascending="true">Status</SortableColumn>
</div>
<div role="columnheader" :aria-label="ColumnNames.MessageId" class="col-3">
<SortableColumn :sort-by="ColumnNames.MessageId" v-model="sortByInstances" :default-ascending="true">Message Id</SortableColumn>
</div>
<div role="columnheader" :aria-label="ColumnNames.MessageType" class="col-3">
<SortableColumn :sort-by="ColumnNames.MessageType" v-model="sortByInstances" :default-ascending="true">Type</SortableColumn>
</div>
<div role="columnheader" :aria-label="ColumnNames.TimeSent" class="col-2">
<SortableColumn :sort-by="ColumnNames.TimeSent" v-model="sortByInstances">Time Sent</SortableColumn>
</div>
<div role="columnheader" :aria-label="ColumnNames.ProcessingTime" class="col-2">
<SortableColumn :sort-by="ColumnNames.ProcessingTime" v-model="sortByInstances">Processing Time</SortableColumn>
</div>
</div>
<!--Table rows-->
<!--NOTE: currently the DataView pages on the client only: we need to make it server data aware (i.e. the total will be the count from the server, not the length of the data we have locally)-->
<DataView :data="messages" :show-items-per-page="true" :items-per-page="itemsPerPage" @items-per-page-changed="store.setItemsPerPage">
<template #data="{ pageData }">
<div role="rowgroup" aria-label="endpoints">
<div role="row" :aria-label="message.message_id" class="row grid-row" v-for="message in pageData" :key="message.id">
<div role="cell" aria-label="status" class="status" :title="statusToName(message.status)">
<div class="status-icon" :class="statusToIcon(message.status)"></div>
</div>
<div role="cell" aria-label="message-id" class="col-3 message-id">
<div class="box-header">
<tippy :aria-label="message.message_id" :delay="[700, 0]" class="no-side-padding lead righ-side-ellipsis endpoint-details-link">
<template #content>
<p :style="{ overflowWrap: 'break-word' }">{{ message.message_id }}</p>
</template>
<RouterLink class="hackToPreventSafariFromShowingTooltip" aria-label="details-link" :to="{ path: routeLinks.audit.message.link(message.id), query: { back: route.path } }">
{{ message.message_id }}
</RouterLink>
</tippy>
</div>
</div>
<div role="cell" aria-label="message-type" class="col-3 message-type">
{{ friendlyTypeName(message.message_type) }}
</div>
<div role="cell" aria-label="time-sent" class="col-2 time-sent">
{{ moment(message.time_sent).local().format("LLLL") }}
</div>
<div role="cell" aria-label="processing-time" class="col-2 processing-time">
{{ formatDotNetTimespan(message.processing_time) }}
</div>
</div>
</div>
</template>
</DataView>
</section>
</template>

<style scoped>
@import "../list.css";

.hackToPreventSafariFromShowingTooltip::after {
content: "";
display: block;
}

.instances-muted {
font-weight: bold;
}

.status {
width: 5em;
text-align: center;
}

.status-icon {
color: white;
border-radius: 0.75em;
width: 1.2em;
height: 1.2em;
}

.status-icon::before {
vertical-align: middle;
font-size: 0.85em;
}

.successful {
background: #6cc63f;
}
.successful::before {
content: "\f00c";
}

.resolved-successfully {
background: #3f881b;
}
.resolved-successfully::before {
content: "\f01e";
}

.failed {
background: #c63f3f;
}
.failed::before {
content: "\f00d";
}

.archived {
background: #000000;
}
.archived::before {
content: "\f187";
font-size: 0.85em;
}

.repeated-failure {
background: #c63f3f;
}
.repeated-failure::before {
content: "\f00d\f00d";
font-size: 0.6em;
}

.retry-issued {
background: #cccccc;
color: #000000;
}
.retry-issued::before {
content: "\f01e";
}

.grid-row {
display: flex;
position: relative;
border-top: 1px solid #eee;
border-right: 1px solid #fff;
border-bottom: 1px solid #eee;
border-left: 1px solid #fff;
background-color: #fff;
margin: 0;
}

.grid-row:nth-child(even) {
background-color: #eee;
}
</style>
16 changes: 16 additions & 0 deletions src/Frontend/src/components/audit/AuditMenuItem.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<script setup lang="ts">
import { RouterLink } from "vue-router";
import routeLinks from "@/router/routeLinks";
</script>

<template>
<RouterLink :to="routeLinks.audit.root">
<i class="fa fa-envelope icon-white" title="All Messages"></i>
<span class="navbar-label">All Messages</span>
</RouterLink>
</template>

<style scoped>
@import "@/assets/navbar.css";
@import "@/assets/header-menu-item.css";
</style>
5 changes: 5 additions & 0 deletions src/Frontend/src/components/audit/MessageView.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script setup lang="ts"></script>

<template>
<div>Hello</div>
</template>
25 changes: 23 additions & 2 deletions src/Frontend/src/composables/autoRefresh.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
export default function useAutoRefresh(refreshAction: () => Promise<void>, timeout: number) {
/**
* Enables refresh functionality, either auto or manual
* @param refreshAction The action to perform (by default) when refreshing
* @param defaultTimeout The time between refreshes in ms or null if no auto-refresh is desired
*/
export default function useAutoRefresh(refreshAction: () => Promise<void>, defaultTimeout: number | null, startImmediately = true) {
let refreshInterval: number | null = null;
const timeout = { value: defaultTimeout };

function stopTimer() {
if (refreshInterval !== null) {
Expand All @@ -9,10 +15,12 @@ export default function useAutoRefresh(refreshAction: () => Promise<void>, timeo
}

function startTimer() {
if (timeout.value === null) return;

stopTimer();
refreshInterval = window?.setTimeout(() => {
executeAndResetTimer();
}, timeout);
}, timeout.value as number);
}

async function executeAndResetTimer(overrideAction?: () => Promise<void>) {
Expand All @@ -24,7 +32,20 @@ export default function useAutoRefresh(refreshAction: () => Promise<void>, timeo
}
}

/**
* Updates the timeout interval between refreshes
* @param updatedTimeout The new time between refreshes in ms or null if no auto-refresh is desired
*/
async function updateTimeout(updatedTimeout: number | null) {
timeout.value = updatedTimeout;
await executeAndResetTimer();
}

// eslint-disable-next-line promise/catch-or-return,promise/prefer-await-to-then,promise/valid-params
if (startImmediately) executeAndResetTimer().then();

return {
executeAndResetTimer,
updateTimeout,
};
}
Loading