Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A few component changes #2043

Merged
merged 5 commits into from
Sep 10, 2024
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
74 changes: 74 additions & 0 deletions src/Frontend/src/components/AutoRefreshDataView.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<script setup lang="ts" generic="T">
import { onMounted, onUnmounted, ref, watch } from "vue";
import { useTypedFetchFromServiceControl } from "@/composables/serviceServiceControlUrls";
import ItemsPerPage from "@/components/ItemsPerPage.vue";
import PaginationStrip from "@/components/PaginationStrip.vue";
import type DataViewPageModel from "./DataViewPageModel";

const props = withDefaults(
defineProps<{
apiUrl: string;
itemsPerPageOptions?: number[];
itemsPerPage?: number;
autoRefreshSeconds: number;
showPagination?: boolean;
showItemsPerPage?: boolean;
}>(),
{ itemsPerPageOptions: () => [20, 35, 50, 75], itemsPerPage: 50, showPagination: true, showItemsPerPage: false }
);

let refreshTimer: number | undefined;
const viewModel = defineModel<DataViewPageModel<T>>({ required: true });

const pageNumber = ref(1);
const itemsPerPage = ref(props.itemsPerPage);

watch(
() => props.autoRefreshSeconds,
() => {
stopRefreshTimer();
startRefreshTimer();
}
);

watch(itemsPerPage, () => loadData());
watch(pageNumber, () => loadData());

async function loadData() {
const [response, data] = await useTypedFetchFromServiceControl<T[]>(`${props.apiUrl}?page=${pageNumber.value}&per_page=${itemsPerPage.value}`);
if (response.ok) {
viewModel.value.totalCount = parseInt(response.headers.get("Total-Count") ?? "0");
viewModel.value.data = data;
}
}

function startRefreshTimer() {
if (props.autoRefreshSeconds) {
refreshTimer = window.setInterval(() => {
loadData();
}, props.autoRefreshSeconds * 1000);
}
}

function stopRefreshTimer() {
window.clearInterval(refreshTimer);
}

onMounted(() => {
startRefreshTimer();
loadData();
});

onUnmounted(() => {
stopRefreshTimer();
});
</script>

<template>
<slot name="data"></slot>
<div class="row">
<ItemsPerPage v-if="showItemsPerPage" v-model="itemsPerPage" :options="itemsPerPageOptions" />
<PaginationStrip v-if="showPagination" v-model="pageNumber" :totalCount="viewModel.totalCount" :itemsPerPage="itemsPerPage" />
</div>
<slot name="footer"></slot>
</template>
56 changes: 7 additions & 49 deletions src/Frontend/src/components/DataView.vue
Original file line number Diff line number Diff line change
@@ -1,74 +1,32 @@
<script setup lang="ts" generic="T">
import { onMounted, onUnmounted, ref, watch } from "vue";
import { useTypedFetchFromServiceControl } from "@/composables/serviceServiceControlUrls";
import { ref, computed, watch } from "vue";
import ItemsPerPage from "@/components/ItemsPerPage.vue";
import PaginationStrip from "@/components/PaginationStrip.vue";
import type DataViewPageModel from "./DataViewPageModel";

const props = withDefaults(
defineProps<{
apiUrl: string;
data: T[];
itemsPerPageOptions?: number[];
itemsPerPage?: number;
autoRefreshSeconds: number;
showPagination?: boolean;
showItemsPerPage?: boolean;
}>(),
{ itemsPerPageOptions: () => [20, 35, 50, 75], itemsPerPage: 50, showPagination: true, showItemsPerPage: false }
);

let refreshTimer: number | undefined;
const viewModel = defineModel<DataViewPageModel<T>>({ required: true });

const pageNumber = ref(1);
const itemsPerPage = ref(props.itemsPerPage);
const pageData = computed(() => props.data.slice((pageNumber.value - 1) * itemsPerPage.value, Math.min(pageNumber.value * itemsPerPage.value, props.data.length)));

watch(
() => props.autoRefreshSeconds,
() => {
stopRefreshTimer();
startRefreshTimer();
}
);

watch(itemsPerPage, () => loadData());
watch(pageNumber, () => loadData());

async function loadData() {
const [response, data] = await useTypedFetchFromServiceControl<T[]>(`${props.apiUrl}?page=${pageNumber.value}&per_page=${itemsPerPage.value}`);
if (response.ok) {
viewModel.value.totalCount = parseInt(response.headers.get("Total-Count") ?? "0");
viewModel.value.data = data;
}
}

function startRefreshTimer() {
if (props.autoRefreshSeconds) {
refreshTimer = window.setInterval(() => {
loadData();
}, props.autoRefreshSeconds * 1000);
}
}

function stopRefreshTimer() {
window.clearInterval(refreshTimer);
}

onMounted(() => {
startRefreshTimer();
loadData();
});
const emit = defineEmits<{ itemsPerPageChanged: [value: number] }>();

onUnmounted(() => {
stopRefreshTimer();
});
watch(itemsPerPage, () => emit("itemsPerPageChanged", itemsPerPage.value));
</script>

<template>
<slot name="data"></slot>
<slot name="data" :pageData="pageData" />
<div class="row">
<ItemsPerPage v-if="showItemsPerPage" v-model="itemsPerPage" :options="itemsPerPageOptions" />
<PaginationStrip v-if="showPagination" v-model="pageNumber" :totalCount="viewModel.totalCount" :itemsPerPage="itemsPerPage" />
<PaginationStrip v-if="showPagination" v-model="pageNumber" :totalCount="data.length" :itemsPerPage="itemsPerPage" />
</div>
<slot name="footer"></slot>
</template>
2 changes: 1 addition & 1 deletion src/Frontend/src/components/EventItemShort.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import DataView from "@/components/DataView.vue";
import DataView from "@/components/AutoRefreshDataView.vue";
import EventLogItem from "@/components/EventLogItem.vue";
import type EventLogItemType from "@/resources/EventLogItem";
import { ref } from "vue";
Expand Down
14 changes: 14 additions & 0 deletions src/Frontend/src/components/LicenseNotExpired.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<script setup lang="ts">
import ConditionalRender from "@/components/ConditionalRender.vue";
import { licenseStatus } from "@/composables/serviceLicense";
import LicenseExpired from "@/components/LicenseExpired.vue";
</script>

<template>
<ConditionalRender :supported="!licenseStatus.isExpired">
<template #unsupported>
<LicenseExpired />
</template>
<slot />
</ConditionalRender>
</template>
4 changes: 2 additions & 2 deletions src/Frontend/src/components/OnOffSwitch.vue
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
<script setup lang="ts">
defineProps<{
id: string;
value: boolean | null;
}>();

const model = defineModel<boolean | null>({ required: true });
const emit = defineEmits<{ toggle: [] }>();
</script>

<template>
<div class="onoffswitch">
<input type="checkbox" :id="`onoffswitch${id}`" :name="`onoffswitch${id}`" class="onoffswitch-checkbox" @click="emit('toggle')" v-model="model" />
<input type="checkbox" :id="`onoffswitch${id}`" :name="`onoffswitch${id}`" class="onoffswitch-checkbox" @click="emit('toggle')" :checked="value ?? false" />
<label class="onoffswitch-label" :for="`onoffswitch${id}`">
<span class="onoffswitch-inner"></span>
<span class="onoffswitch-switch"></span>
Expand Down
1 change: 1 addition & 0 deletions src/Frontend/src/components/SortableColumn.vue
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ function toggleSort() {
max-width: 100%;
display: flex;
flex-wrap: wrap;
align-items: end;
}

.column-header-button span {
Expand Down
5 changes: 2 additions & 3 deletions src/Frontend/src/components/TimeSince.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import moment from "moment";

const emptyDate = "0001-01-01T00:00:00";

const props = withDefaults(defineProps<{ dateUtc?: string }>(), { dateUtc: emptyDate });
const props = withDefaults(defineProps<{ dateUtc?: string; defaultTextOnFailure?: string }>(), { dateUtc: emptyDate, defaultTextOnFailure: "n/a" });

let interval: number | undefined = undefined;

Expand All @@ -17,8 +17,7 @@ function updateText() {
text.value = m.fromNow();
title.value = m.local().format("LLLL") + " (local)\n" + m.utc().format("LLLL") + " (UTC)";
} else {
text.value = "n/a";
title.value = "n/a";
text.value = title.value = props.defaultTextOnFailure;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ onMounted(async () => {
<div class="col-12 no-side-padding">
<div class="row">
<div class="col-1">
<OnOffSwitch id="emailNotifications" @toggle="toggleEmailNotifications" v-model="emailNotifications.enabled" />
<OnOffSwitch id="emailNotifications" @toggle="toggleEmailNotifications" :value="emailNotifications.enabled" />
<div>
<span class="connection-test connection-failed">
<template v-if="emailToggleSuccessful === false"> <i class="fa fa-exclamation-triangle"></i> Update failed </template>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const sortedEndpoints = computed<Endpoint[]>(() =>
<div class="col-sm-12 no-side-padding">
<div class="row">
<div class="col-sm-2 col-lg-1">
<OnOffSwitch :id="endpoint.id" @toggle="store.toggleEndpointMonitor(endpoint)" v-model="endpoint.monitor_heartbeat" />
<OnOffSwitch :id="endpoint.id" @toggle="store.toggleEndpointMonitor(endpoint)" :value="endpoint.monitor_heartbeat" />
</div>
<div class="col-sm-10 col-lg-11">
<div class="row box-header">
Expand Down
2 changes: 1 addition & 1 deletion src/Frontend/src/views/EventsView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { licenseStatus } from "@/composables/serviceLicense";
import { connectionState } from "@/composables/serviceServiceControl";
import LicenseExpired from "@/components/LicenseExpired.vue";
import DataView from "@/components/DataView.vue";
import DataView from "@/components/AutoRefreshDataView.vue";
import EventLogItem from "@/components/EventLogItem.vue";
import ServiceControlNotAvailable from "@/components/ServiceControlNotAvailable.vue";
import type EventLogItemType from "@/resources/EventLogItem";
Expand Down
90 changes: 45 additions & 45 deletions src/Frontend/src/views/HeartbeatsView.vue
Original file line number Diff line number Diff line change
@@ -1,70 +1,70 @@
<script setup lang="ts">
import { RouterLink, RouterView } from "vue-router";
import { licenseStatus } from "../composables/serviceLicense";
import { connectionState } from "../composables/serviceServiceControl";
import LicenseExpired from "../components/LicenseExpired.vue";
import routeLinks from "@/router/routeLinks";
import isRouteSelected from "@/composables/isRouteSelected";
import { DisplayType, sortOptions, useHeartbeatsStore } from "@/stores/HeartbeatsStore";
import { storeToRefs } from "pinia";
import OrderBy from "@/components/OrderBy.vue";
import LicenseNotExpired from "@/components/LicenseNotExpired.vue";
import ServiceControlAvailable from "@/components/ServiceControlAvailable.vue";

const store = useHeartbeatsStore();
const { inactiveEndpoints, activeEndpoints, selectedDisplay, filterString } = storeToRefs(store);
</script>

<template>
<LicenseExpired />
<template v-if="!licenseStatus.isExpired">
<div class="container">
<div class="row">
<div class="col-12">
<h1>Endpoint Heartbeats</h1>
<LicenseNotExpired>
<ServiceControlAvailable>
<div class="container">
<div class="row">
<div class="col-12">
<h1>Endpoint Heartbeats</h1>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<div class="tabs">
<div>
<!--Inactive Endpoints-->
<h5 :class="{ active: isRouteSelected(routeLinks.heartbeats.inactive.link), disabled: !connectionState.connected && !connectionState.connectedRecently }">
<RouterLink :to="routeLinks.heartbeats.inactive.link"> Inactive Endpoints ({{ inactiveEndpoints.length }}) </RouterLink>
</h5>
<div class="row">
<div class="col-sm-12">
<div class="tabs">
<div>
<!--Inactive Endpoints-->
<h5 :class="{ active: isRouteSelected(routeLinks.heartbeats.inactive.link) }">
<RouterLink :to="routeLinks.heartbeats.inactive.link"> Inactive Endpoints ({{ inactiveEndpoints.length }}) </RouterLink>
</h5>

<!--Active Endpoints-->
<h5 v-if="!licenseStatus.isExpired" :class="{ active: isRouteSelected(routeLinks.heartbeats.active.link), disabled: !connectionState.connected && !connectionState.connectedRecently }">
<RouterLink :to="routeLinks.heartbeats.active.link"> Active Endpoints ({{ activeEndpoints.length }}) </RouterLink>
</h5>
<!--Active Endpoints-->
<h5 :class="{ active: isRouteSelected(routeLinks.heartbeats.active.link) }">
<RouterLink :to="routeLinks.heartbeats.active.link"> Active Endpoints ({{ activeEndpoints.length }}) </RouterLink>
</h5>

<!--Configuration-->
<h5 v-if="!licenseStatus.isExpired" :class="{ active: isRouteSelected(routeLinks.heartbeats.configuration.link), disabled: !connectionState.connected && !connectionState.connectedRecently }">
<RouterLink :to="routeLinks.heartbeats.configuration.link"> Configuration </RouterLink>
</h5>
</div>
<div class="filter-group">
<div class="msg-group-menu dropdown" v-if="!isRouteSelected(routeLinks.heartbeats.configuration.link)">
<label class="control-label">Display:</label>
<button type="button" class="btn btn-default dropdown-toggle sp-btn-menu" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{ selectedDisplay }}
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li v-for="displayType in DisplayType" :key="displayType">
<a @click.prevent="store.setSelectedDisplay(displayType)">{{ displayType }}</a>
</li>
</ul>
<!--Configuration-->
<h5 :class="{ active: isRouteSelected(routeLinks.heartbeats.configuration.link) }">
<RouterLink :to="routeLinks.heartbeats.configuration.link"> Configuration </RouterLink>
</h5>
</div>
<OrderBy @sort-updated="store.setSelectedSort" :sort-options="sortOptions" />
<div class="filter-input">
<input type="text" placeholder="Filter by name..." aria-label="filter by name" class="form-control-static filter-input" v-model="filterString" />
<div class="filter-group">
<div class="msg-group-menu dropdown" v-if="!isRouteSelected(routeLinks.heartbeats.configuration.link)">
<label class="control-label">Display:</label>
<button type="button" class="btn btn-default dropdown-toggle sp-btn-menu" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{ selectedDisplay }}
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li v-for="displayType in DisplayType" :key="displayType">
<a @click.prevent="store.setSelectedDisplay(displayType)">{{ displayType }}</a>
</li>
</ul>
</div>
<OrderBy @sort-updated="store.setSelectedSort" :sort-options="sortOptions" />
<div class="filter-input">
<input type="text" placeholder="Filter by name..." aria-label="filter by name" class="form-control-static filter-input" v-model="filterString" />
</div>
</div>
</div>
</div>
</div>
<RouterView />
</div>
<RouterView />
</div>
</template>
</ServiceControlAvailable>
</LicenseNotExpired>
</template>

<style scoped>
Expand Down