Add sorting by column and fix page reload issue.

This commit is contained in:
baldo 2022-06-27 13:51:01 +02:00
parent ed29b96d45
commit 6057ad5a2a
5 changed files with 228 additions and 53 deletions

View file

@ -7,12 +7,14 @@ import {
NODES_FILTER_FIELDS,
type NodesFilter,
OnlineState,
type SearchTerm,
type UnixTimestampMilliseconds,
} from "@/types";
import {computed, nextTick, onMounted, ref, watch} from "vue";
import {useConfigStore} from "@/stores/config";
interface Props {
searchTerm: SearchTerm;
filter: NodesFilter;
}
@ -41,7 +43,7 @@ const FILTER_LABELS: Record<string, string | Map<any, any>> = {
}
const emit = defineEmits<{
(e: "updateFilter", filter: NodesFilter, searchTerm?: string): void,
(e: "updateFilter", filter: NodesFilter, searchTerm: SearchTerm): void,
}>();
const props = defineProps<Props>();
@ -204,15 +206,12 @@ function buildNodesFilter(): NodesFilter {
let lastSearchTimestamp: UnixTimestampMilliseconds = 0 as UnixTimestampMilliseconds;
let searchTimeout: NodeJS.Timeout | undefined = undefined;
let lastSearchTerm = "";
let lastSearchTerm: SearchTerm = "" as SearchTerm;
function doSearch(): void {
const nodesFilter = buildNodesFilter();
lastSearchTerm = input.value.value;
let searchTerm: string | undefined = lastSearchTerm.trim();
if (!searchTerm) {
searchTerm = undefined;
}
lastSearchTerm = input.value.value as SearchTerm;
const searchTerm: SearchTerm = lastSearchTerm.trim() as SearchTerm;
emit("updateFilter", nodesFilter, searchTerm);
}
@ -258,6 +257,7 @@ function doThrottledSearch(): void {
@keyup="doThrottledSearch()"
maxlength="64"
type="search"
:value="searchTerm"
placeholder="Knoten durchsuchen..."/>
<i class="fa fa-search search" @click="doSearch()"/>
</div>

View file

@ -0,0 +1,67 @@
<script lang="ts">
import {type Component, defineComponent, type PropType} from "vue";
import {type EnumValue, SortDirection} from "@/types";
type Props<SortField> = {
field: PropType<EnumValue<SortField>>,
currentField: PropType<EnumValue<SortField>>,
currentDirection: PropType<SortDirection>,
};
type SortTH<SortField> = Component<Props<SortField>>;
function defineGenericComponent<SortField>(): SortTH<SortField> {
const props: Props<SortField> = {
field: null as unknown as PropType<EnumValue<SortField>>,
currentField: null as unknown as PropType<EnumValue<SortField>>,
currentDirection: null as unknown as PropType<SortDirection>,
};
return defineComponent({
name: "SortTH",
props,
computed: {
sortDirection: function () {
return this.field === this.currentField ? this.currentDirection : undefined;
},
isAscending: function () {
return this.sortDirection === SortDirection.ASCENDING;
},
},
emits: {
sort: (field: SortField, direction: SortDirection) => true,
},
methods: {
onClick(): void {
this.$emit(
'sort',
this.field,
this.isAscending ? SortDirection.DESCENDING : SortDirection.ASCENDING
);
}
},
});
}
const component = defineGenericComponent<unknown>();
export function SortTH<SortField>(): SortTH<SortField> {
return component as SortTH<SortField>;
}
// noinspection JSUnusedGlobalSymbols
export default component;
</script>
<template>
<th>
<a href="javascript:" title="Sortieren" @click="onClick">
<slot/>
<i v-if="sortDirection && isAscending" class="fa fa-chevron-down" />
<i v-if="sortDirection && !isAscending" class="fa fa-chevron-up" />
</a>
</th>
</template>
<style lang="scss" scoped>
@import "../../scss/variables";
</style>

View file

@ -2,7 +2,7 @@ import {createRouter, createWebHistory} from "vue-router";
import AdminDashboardView from "@/views/AdminDashboardView.vue";
import AdminNodesView from "@/views/AdminNodesView.vue";
import HomeView from "@/views/HomeView.vue";
import {isNodesFilter} from "@/types";
import {isNodesFilter, isNodeSortField, isSortDirection, type SearchTerm} from "@/types";
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
@ -34,10 +34,12 @@ const router = createRouter({
filter = {};
}
const searchTerm = route.query.q ? route.query.q as string : undefined;
const searchTerm = route.query.q ? route.query.q as SearchTerm : undefined;
return {
filter: isNodesFilter(filter) ? filter : {},
searchTerm,
sortDirection: isSortDirection(route.query.sortDir) ? route.query.sortDir : undefined,
sortField: isNodeSortField(route.query.sortField) ? route.query.sortField : undefined,
}
}
},

View file

@ -1,23 +1,53 @@
<script setup lang="ts">
import {useNodesStore} from "@/stores/nodes";
import {onMounted, ref, watch} from "vue";
import type {DomainSpecificNodeResponse, MAC, NodesFilter} from "@/types";
import {onMounted, type PropType, ref, watch} from "vue";
import type {DomainSpecificNodeResponse, MAC, NodesFilter, SearchTerm} from "@/types";
import {NodeSortField, SortDirection} from "@/types";
import Pager from "@/components/Pager.vue";
import LoadingContainer from "@/components/LoadingContainer.vue";
import NodesFilterPanel from "@/components/nodes/NodesFilterPanel.vue";
import {SortTH} from "@/components/table/SortTH.vue";
import router from "@/router";
function debug(...args: any[]): void {
console.debug("==================================================================");
console.debug("AdminNodesVue:", ...args);
console.table({
filter: JSON.stringify(props.filter),
searchTerm: props.searchTerm,
sortDirection: props.sortDirection,
sortField: props.sortField,
});
console.debug("==================================================================");
console.debug();
}
debug("init page");
const NODE_PER_PAGE = 50;
interface Props {
filter: NodesFilter;
searchTerm?: string;
}
// noinspection JSUnusedGlobalSymbols
const sth = SortTH<NodeSortField>();
const props = defineProps<Props>();
const currentFilter = ref<NodesFilter>({});
const currentSearchTerm = ref<string | undefined>(undefined);
const props = defineProps({
filter: {
type: Object as PropType<NodesFilter>,
required: true,
},
searchTerm: {
type: String as unknown as PropType<SearchTerm>,
default: "" as SearchTerm,
},
sortDirection: {
type: String as PropType<SortDirection>,
default: SortDirection.ASCENDING,
},
sortField: {
type: String as PropType<NodeSortField>,
default: NodeSortField.HOSTNAME,
},
});
type NodeRedactField = "nickname" | "email" | "token";
type NodeRedactFieldsMap = Partial<Record<NodeRedactField, boolean>>;
@ -36,10 +66,10 @@ async function refresh(page: number): Promise<void> {
await nodes.refresh(
page,
NODE_PER_PAGE,
SortDirection.ASCENDING,
NodeSortField.HOSTNAME,
currentFilter.value,
currentSearchTerm.value,
props.sortDirection,
props.sortField,
props.filter,
props.searchTerm,
);
} finally {
loading.value = false;
@ -69,37 +99,44 @@ function setRedactField(node: DomainSpecificNodeResponse, field: NodeRedactField
redactFieldsMap[field] = value;
}
async function updateFilter(filter: NodesFilter, searchTerm?: string): Promise<void> {
async function updateRouterState(
filter: NodesFilter,
searchTerm: SearchTerm,
sortDirection: SortDirection,
sortField: NodeSortField,
): Promise<void> {
const filterStr = Object.keys(filter).length > 0 ? JSON.stringify(filter) : undefined;
await router.push({
await router.replace({
path: '/admin/nodes',
query: {
q: searchTerm,
filter: filterStr
q: searchTerm || undefined,
filter: filterStr,
sortDir: sortDirection,
sortField: sortField,
}
});
currentFilter.value = filter;
currentSearchTerm.value = searchTerm;
}
async function updateFilter(filter: NodesFilter, searchTerm: SearchTerm): Promise<void> {
await updateRouterState(filter, searchTerm, props.sortDirection, props.sortField);
}
async function updateSortOrder(sortField: NodeSortField, sortDirection: SortDirection): Promise<void> {
await updateRouterState(props.filter, props.searchTerm, sortDirection, sortField);
}
onMounted(async () => {
await refresh(1);
}
async function refreshFromProps(): Promise<void> {
const needsRefresh = currentFilter.value !== props.filter || currentSearchTerm.value !== props.searchTerm;
currentFilter.value = props.filter;
currentSearchTerm.value = props.searchTerm;
if (needsRefresh) {
await refresh(1);
}
}
onMounted(refreshFromProps);
watch(props, refreshFromProps);
});
watch(props, async () => {
await refresh(1);
});
</script>
<template>
<h2>Knoten</h2>
<NodesFilterPanel :filter="props.filter" @update-filter="updateFilter"/>
<NodesFilterPanel :search-term="searchTerm" :filter="filter" @update-filter="updateFilter"/>
<Pager
:page="nodes.getPage"
@ -126,17 +163,83 @@ watch(props, refreshFromProps);
<table>
<thead>
<tr>
<th>Name</th>
<th>Besitzer*in</th>
<th>E-Mail</th>
<th>Token</th>
<th>MAC</th>
<th>VPN</th>
<th>Site</th>
<th>Domäne</th>
<th>GPS</th>
<th>Status</th>
<th>Monitoring</th>
<sth
:field="NodeSortField.HOSTNAME"
:currentField="sortField"
:currentDirection="sortDirection"
@sort="updateSortOrder">
Name
</sth>
<sth
:field="NodeSortField.NICKNAME"
:currentField="sortField"
:currentDirection="sortDirection"
@sort="updateSortOrder">
Besitzer*in
</sth>
<sth
:field="NodeSortField.EMAIL"
:currentField="sortField"
:currentDirection="sortDirection"
@sort="updateSortOrder">
E-Mail
</sth>
<sth
:field="NodeSortField.TOKEN"
:currentField="sortField"
:currentDirection="sortDirection"
@sort="updateSortOrder">
Token
</sth>
<sth
:field="NodeSortField.MAC"
:currentField="sortField"
:currentDirection="sortDirection"
@sort="updateSortOrder">
MAC
</sth>
<sth
:field="NodeSortField.KEY"
:currentField="sortField"
:currentDirection="sortDirection"
@sort="updateSortOrder">
VPN
</sth>
<sth
:field="NodeSortField.SITE"
:currentField="sortField"
:currentDirection="sortDirection"
@sort="updateSortOrder">
Site
</sth>
<sth
:field="NodeSortField.DOMAIN"
:currentField="sortField"
:currentDirection="sortDirection"
@sort="updateSortOrder">
Domäne
</sth>
<sth
:field="NodeSortField.COORDS"
:currentField="sortField"
:currentDirection="sortDirection"
@sort="updateSortOrder">
GPS
</sth>
<sth
:field="NodeSortField.ONLINE_STATE"
:currentField="sortField"
:currentDirection="sortDirection"
@sort="updateSortOrder">
Status
</sth>
<sth
:field="NodeSortField.MONITORING_STATE"
:currentField="sortField"
:currentDirection="sortDirection"
@sort="updateSortOrder">
Monitoring
</sth>
</tr>
</thead>

View file

@ -562,6 +562,9 @@ export function isNodesFilter(arg: unknown): arg is NodesFilter {
);
}
export type SearchTerm = string & { readonly __tag: unique symbol }
export const isSearchTerm = isString;
export enum MonitoringSortField {
ID = 'id',
HOSTNAME = 'hostname',