Add sorting by column and fix page reload issue.
This commit is contained in:
parent
ed29b96d45
commit
6057ad5a2a
5 changed files with 228 additions and 53 deletions
|
@ -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>
|
||||
|
|
67
frontend/src/components/table/SortTH.vue
Normal file
67
frontend/src/components/table/SortTH.vue
Normal 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>
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Reference in a new issue