Skip to content

Commit

Permalink
Merge pull request #2043 from Particular/john/components
Browse files Browse the repository at this point in the history
A few component changes
  • Loading branch information
johnsimons authored Sep 10, 2024
2 parents d8fe9af + bd0ae94 commit 2a28b81
Show file tree
Hide file tree
Showing 11 changed files with 149 additions and 103 deletions.
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

0 comments on commit 2a28b81

Please sign in to comment.