Skip to content
128 changes: 128 additions & 0 deletions src/Frontend/src/components/ColumnHeader.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
<script setup lang="ts">
import { computed, useSlots } from "vue";
import type { SortInfo } from "./SortInfo";

const props = withDefaults(
defineProps<{
name: string;
label: string;
unit?: string;
sortable?: boolean;
sortBy?: string;
sortState?: SortInfo;
defaultAscending?: boolean;
columnClass: string;
interactiveHelp?: boolean;
}>(),
{
sortable: false,
defaultAscending: false,
interactiveHelp: false,
}
);

const slots = useSlots();
const sortByColumn = computed(() => props.sortBy || props.name);
const activeSortColumn = defineModel<SortInfo>();
const isSortActive = computed(() => activeSortColumn?.value?.property === sortByColumn.value);
const sortIcon = computed(() => (activeSortColumn?.value?.isAscending ? "sort-up" : "sort-down"));

function toggleSort() {
activeSortColumn.value = { property: sortByColumn.value, isAscending: isSortActive.value ? !activeSortColumn?.value?.isAscending : props.defaultAscending };
}
</script>

<template>
<div role="columnheader" :aria-label="props.name" :class="props.columnClass">
<div class="box-header">
<button v-if="props.sortable" @click="toggleSort" class="column-header-button" :aria-label="props.name">
<span>
{{ props.label }}
<span v-if="props.unit" class="table-header-unit">{{ props.unit }}</span>
<span v-if="isSortActive">
<i role="img" :class="sortIcon" :aria-label="sortIcon"></i>
</span>
<tippy v-if="slots.help" max-width="400px" :interactive="props.interactiveHelp">
<i class="fa fa-sm fa-info-circle text-primary ps-1" />
<template #content>
<slot name="help" />
</template>
</tippy>
</span>
</button>
<div v-else class="column-header">
<span>
{{ props.label }}
<span v-if="props.unit" class="table-header-unit">{{ props.unit }}</span>
</span>
<tippy v-if="slots.help" max-width="400px" :interactive="props.interactiveHelp">
<i class="fa fa-sm fa-info-circle text-primary ps-1" />
<template #content>
<slot name="help" />
</template>
</tippy>
</div>
</div>
</div>
</template>

<style scoped>
.column-header {
background: none;
border: none;
padding: 0;
cursor: default;
max-width: 100%;
display: flex;
flex-wrap: wrap;
}
.column-header span,
.column-header-button span {
text-transform: uppercase;
display: inline-block;
text-align: left;
}
.column-header-button {
background: none;
border: none;
padding: 0;
cursor: pointer;
max-width: 100%;
display: flex;
flex-wrap: wrap;
align-items: end;
}

.column-header-button:hover span {
text-decoration: underline;
}

.column-header-button div {
display: inline-block;
}

.sort-up,
.sort-down {
background-position: center;
background-repeat: no-repeat;
width: 8px;
height: 14px;
padding: 0;
margin-left: 10px;
}

.sort-up {
background-image: url("@/assets/sort-up.svg");
}

.sort-down {
background: url("@/assets/sort-down.svg");
}

.sort-up,
.sort-down {
background-repeat: no-repeat;
display: inline-block;
vertical-align: middle;
}
</style>
51 changes: 14 additions & 37 deletions src/Frontend/src/components/heartbeats/EndpointInstances.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,12 @@ import { storeToRefs } from "pinia";
import { useRoute, useRouter } from "vue-router";
import { computed, onMounted, ref } from "vue";
import { EndpointStatus } from "@/resources/Heartbeat";
import SortableColumn from "@/components/SortableColumn.vue";
import ColumnHeader from "@/components/ColumnHeader.vue";
import DataView from "@/components/DataView.vue";
import OnOffSwitch from "../OnOffSwitch.vue";
import routeLinks from "@/router/routeLinks";
import { useShowToast } from "@/composables/toast";
import { TYPE } from "vue-toastification";
import { Tippy } from "vue-tippy";
import { useHeartbeatInstancesStore, ColumnNames } from "@/stores/HeartbeatInstancesStore";
import { EndpointsView } from "@/resources/EndpointView";
import endpointSettingsClient from "@/components/heartbeats/endpointSettingsClient";
Expand Down Expand Up @@ -159,41 +158,19 @@ async function toggleAlerts(instance: EndpointsView) {
<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.InstanceName" class="col-6">
<SortableColumn :sort-by="ColumnNames.InstanceName" v-model="sortByInstances" :default-ascending="true">Host Name</SortableColumn>
</div>
<div role="columnheader" :aria-label="ColumnNames.LastHeartbeat" class="col-2">
<SortableColumn :sort-by="ColumnNames.LastHeartbeat" v-model="sortByInstances">Last Heartbeat</SortableColumn>
</div>
<div role="columnheader" :aria-label="ColumnNames.MuteToggle" class="col-2 centre">
<SortableColumn :sort-by="ColumnNames.MuteToggle" v-model="sortByInstances">Mute Alerts</SortableColumn>
<tippy max-width="400px">
<i :style="{ fontSize: '1.1em', marginLeft: '0.25em' }" class="fa fa-info-circle text-primary" />
<template #content>
<span>Mute an instance when you are planning to take the instance offline to do maintenance or some other reason. This will prevent alerts on the dashboard.</span>
</template>
</tippy>
</div>
<div role="columnheader" aria-label="actions" class="col-1">
<div>
Actions
<tippy max-width="400px">
<i :style="{ fontSize: '1.1em' }" class="fa fa-info-circle text-primary" />
<template #content>
<table>
<tbody>
<tr>
<td style="padding: 3px; width: 6em; text-align: end; align-content: center">
<button type="button" class="btn btn-danger btn-sm"><i class="fa fa-trash text-white" /> Delete</button>
</td>
<td style="padding: 3px">Delete an instance when that instance has been decommissioned.</td>
</tr>
</tbody>
</table>
</template>
</tippy>
</div>
</div>
<ColumnHeader :name="ColumnNames.InstanceName" label="Host Name" columnClass="col-6" v-model="sortByInstances" sortable default-ascending />
<ColumnHeader :name="ColumnNames.LastHeartbeat" label="Last Heartbeat" columnClass="col-2" v-model="sortByInstances" sortable />
<ColumnHeader :name="ColumnNames.MuteToggle" label="Mute Alerts" columnClass="col-2 centre">
<template #help>Mute an instance when you are planning to take the instance offline to do maintenance or some other reason. This will prevent alerts on the dashboard.</template>
</ColumnHeader>
<ColumnHeader name="actions" label="Actions" columnClass="col-1" interactive-help>
<template #help>
<div class="d-flex align-items-center p-1">
<button type="button" class="btn btn-danger btn-ms text-nowrap me-3" @click="deleteAllInstances()"><i class="fa fa-trash text-white" /> Delete</button>
<span>Delete an instance when that instance has been decommissioned.</span>
</div>
</template>
</ColumnHeader>
</div>
<no-data v-if="filteredValidInstances.length === 0" message="No endpoint instances found. For untracked endpoints, disconnected instances are automatically pruned.">
<div v-if="totalValidInstances.length === 0" class="delete-all">
Expand Down
30 changes: 8 additions & 22 deletions src/Frontend/src/components/heartbeats/HeartbeatsList.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<script setup lang="ts">
import { useHeartbeatsStore, ColumnNames } from "@/stores/HeartbeatsStore";
import { storeToRefs } from "pinia";
import SortableColumn from "@/components/SortableColumn.vue";
import DataView from "@/components/DataView.vue";
import OnOffSwitch from "../OnOffSwitch.vue";
import routeLinks from "@/router/routeLinks";
Expand All @@ -11,6 +10,7 @@ import { LogicalEndpoint } from "@/resources/Heartbeat";
import { useShowToast } from "@/composables/toast";
import { TYPE } from "vue-toastification";
import LastHeartbeat from "@/components/heartbeats/LastHeartbeat.vue";
import ColumnHeader from "../ColumnHeader.vue";

defineProps<{
data: LogicalEndpoint[];
Expand Down Expand Up @@ -42,27 +42,13 @@ function endpointHealth(endpoint: LogicalEndpoint) {
<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 v-if="columns.includes(ColumnNames.Name)" role="columnheader" :aria-label="ColumnNames.Name" class="col-6">
<SortableColumn :sort-by="ColumnNames.Name" v-model="sortByInstances" :default-ascending="true">Name</SortableColumn>
</div>
<div v-if="columns.includes(ColumnNames.InstancesDown)" role="columnheader" :aria-label="ColumnNames.InstancesDown" class="col-2">
<SortableColumn :sort-by="ColumnNames.InstancesDown" v-model="sortByInstances" :default-ascending="true">Instances</SortableColumn>
</div>
<div v-if="columns.includes(ColumnNames.InstancesTotal)" role="columnheader" :aria-label="ColumnNames.InstancesTotal" class="col-2">
<SortableColumn :sort-by="ColumnNames.InstancesTotal" v-model="sortByInstances" :default-ascending="true">Instances</SortableColumn>
</div>
<div v-if="columns.includes(ColumnNames.LastHeartbeat)" role="columnheader" :aria-label="ColumnNames.LastHeartbeat" class="col-2">
<SortableColumn :sort-by="ColumnNames.LastHeartbeat" v-model="sortByInstances">Last Heartbeat</SortableColumn>
</div>
<div v-if="columns.includes(ColumnNames.Tracked)" role="columnheader" :aria-label="ColumnNames.Tracked" class="col-1 centre">
<SortableColumn :sort-by="ColumnNames.Tracked" v-model="sortByInstances">Track Instances</SortableColumn>
</div>
<div v-if="columns.includes(ColumnNames.TrackToggle)" role="columnheader" :aria-label="ColumnNames.Tracked" class="col-2 centre">
<SortableColumn :sort-by="ColumnNames.TrackToggle" v-model="sortByInstances">Track Instances</SortableColumn>
</div>
<div v-if="columns.includes(ColumnNames.Muted)" role="columnheader" :aria-label="ColumnNames.Muted" class="col-1 centre">
<SortableColumn :sort-by="ColumnNames.Muted" v-model="sortByInstances">Instances Muted</SortableColumn>
</div>
<ColumnHeader v-if="columns.includes(ColumnNames.Name)" :name="ColumnNames.Name" label="Name" columnClass="col-6" sortable v-model="sortByInstances" default-ascending />
<ColumnHeader v-if="columns.includes(ColumnNames.InstancesDown)" :name="ColumnNames.InstancesDown" label="Instances Down" columnClass="col-2" sortable v-model="sortByInstances" default-ascending />
<ColumnHeader v-if="columns.includes(ColumnNames.InstancesTotal)" :name="ColumnNames.InstancesTotal" label="Instances" columnClass="col-2" sortable v-model="sortByInstances" default-ascending />
<ColumnHeader v-if="columns.includes(ColumnNames.LastHeartbeat)" :name="ColumnNames.LastHeartbeat" label="Last Heartbeat" columnClass="col-2" sortable v-model="sortByInstances" />
<ColumnHeader v-if="columns.includes(ColumnNames.Tracked)" :name="ColumnNames.Tracked" label="Track Instances" columnClass="col-1 centre" sortable v-model="sortByInstances" />
<ColumnHeader v-if="columns.includes(ColumnNames.TrackToggle)" :name="ColumnNames.TrackToggle" label="Track Instances" columnClass="col-2 centre" sortable v-model="sortByInstances" />
<ColumnHeader v-if="columns.includes(ColumnNames.Muted)" :name="ColumnNames.Muted" label="Instances Muted" columnClass="col-1 centre" sortable v-model="sortByInstances" />
</div>
<!--Table rows-->
<DataView :data="data" :show-items-per-page="true" :items-per-page="itemsPerPage" @items-per-page-changed="store.setItemsPerPage">
Expand Down
47 changes: 14 additions & 33 deletions src/Frontend/src/components/monitoring/EndpointInstances.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import NoData from "@/components/NoData.vue";
import SmallGraph from "./SmallGraph.vue";
import type { ExtendedEndpointInstance } from "@/resources/MonitoringEndpoint";
import routeLinks from "@/router/routeLinks";
import ColumnHeader from "@/components/ColumnHeader.vue";

const isRemovingEndpointEnabled = ref<boolean>(false);
const router = useRouter();
Expand Down Expand Up @@ -57,39 +58,19 @@ onMounted(async () => {
<!-- Breakdown by instance-->
<!--headers-->
<div role="row" aria-label="instances-column-headers" class="row box box-no-click table-head-row">
<div class="col-xs-4 col-xl-8">
<div role="columnheader" aria-label="instance-name" class="row box-header">
<div class="col-xs-12">Instance Name</div>
</div>
</div>
<div class="col-xs-2 col-xl-1 no-side-padding">
<div class="row box-header">
<div role="columnheader" aria-label="throughput" class="col-xs-12 no-side-padding" v-tippy="`Throughput: The number of messages per second successfully processed by a receiving endpoint.`">
Throughput <span class="table-header-unit">(msgs/s)</span>
</div>
</div>
</div>
<div class="col-xs-2 col-xl-1 no-side-padding">
<div class="row box-header">
<div role="columnheader" aria-label="scheduled-retires" class="col-xs-12 no-side-padding" v-tippy="`Scheduled retries: The number of messages per second scheduled for retries (immediate or delayed).`">
Scheduled retries <span class="table-header-unit">(msgs/s)</span>
</div>
</div>
</div>
<div class="col-xs-2 col-xl-1 no-side-padding">
<div class="row box-header">
<div role="columnheader" aria-label="processing-time" class="col-xs-12 no-side-padding" v-tippy="`Processing time: The time taken for a receiving endpoint to successfully process a message.`">
Processing Time <span class="table-header-unit">(t)</span>
</div>
</div>
</div>
<div class="col-xs-2 col-xl-1 no-side-padding">
<div class="row box-header">
<div role="columnheader" aria-label="critical-time" class="col-xs-12 no-side-padding" v-tippy="`Critical time: The elapsed time from when a message was sent, until it was successfully processed by a receiving endpoint.`">
Critical Time <span class="table-header-unit">(t)</span>
</div>
</div>
</div>
<ColumnHeader name="instance-name" label="Instance Name" column-class="col-xs-4 col-xl-8" />
<ColumnHeader name="throughput" label="Throughput" unit="(msgs/s)" column-class="col-xs-2 col-xl-1 no-side-padding">
<template #help>Throughput: The number of messages per second successfully processed by a receiving endpoint.</template>
</ColumnHeader>
<ColumnHeader name="retires" label="Scheduled retries" unit="(msgs/s)" column-class="col-xs-2 col-xl-1 no-side-padding">
<template #help>Scheduled retries: The number of messages per second scheduled for retries (immediate or delayed).</template>
</ColumnHeader>
<ColumnHeader name="processing-time" label="Processing time" unit="(t)" column-class="col-xs-2 col-xl-1 no-side-padding">
<template #help>Processing time: The time taken for a receiving endpoint to successfully process a message.</template>
</ColumnHeader>
<ColumnHeader name="critical-time" label="Critical time" unit="(t)" column-class="col-xs-2 col-xl-1 no-side-padding">
<template #help>Critical time: The elapsed time from when a message was sent, until it was successfully processed by a receiving endpoint.</template>
</ColumnHeader>
</div>

<NoData v-if="!endpoint?.instances?.length" title="No messages" message="No messages processed in this period of time"></NoData>
Expand Down
Loading