Apply filters to nodes list.

This commit is contained in:
baldo 2022-06-12 14:10:00 +02:00
parent 5b703917b5
commit cce665d149
6 changed files with 96 additions and 77 deletions

View file

@ -1,37 +1,24 @@
<script setup lang="ts"> <script setup lang="ts">
import {computed, defineProps} from "vue"; import {computed, defineProps} from "vue";
import type {NodesFilter} from "@/types";
const props = defineProps({ interface Props {
title: { title: string;
type: String, icon: string;
required: true, variant: string;
}, value: number;
icon: { link: string;
type: String, filter?: NodesFilter;
required: true, }
},
variant: { const props = defineProps<Props>();
type: String,
required: true,
},
value: {
type: Number,
required: true,
},
link: {
type: String,
required: true,
},
filter: {
type: Object,
required: false,
},
});
const linkTarget = computed(() => { const linkTarget = computed(() => {
if (props.filter) { if (props.filter) {
const json = JSON.stringify(props.filter); return {
return `${props.link}?search=${encodeURIComponent(json)}`; path: props.link,
query: props.filter,
}
} else { } else {
return props.link; return props.link;
} }

View file

@ -1,7 +1,8 @@
import { createRouter, createWebHistory } from "vue-router"; import {createRouter, createWebHistory} from "vue-router";
import AdminDashboardView from "@/views/AdminDashboardView.vue"; import AdminDashboardView from "@/views/AdminDashboardView.vue";
import AdminNodesView from "@/views/AdminNodesView.vue"; import AdminNodesView from "@/views/AdminNodesView.vue";
import HomeView from "@/views/HomeView.vue"; import HomeView from "@/views/HomeView.vue";
import {isNodesFilter} from "@/types";
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
@ -20,6 +21,9 @@ const router = createRouter({
path: "/admin/nodes", path: "/admin/nodes",
name: "admin-nodes", name: "admin-nodes",
component: AdminNodesView, component: AdminNodesView,
props: route => ({
filter: isNodesFilter(route.query) ? route.query : {}
})
}, },
], ],
}); });

View file

@ -1,5 +1,5 @@
import {defineStore} from "pinia"; import {defineStore} from "pinia";
import {isEnhancedNode, type EnhancedNode} from "@/types"; import {type EnhancedNode, isEnhancedNode, type NodesFilter} from "@/types";
import {internalApi} from "@/utils/Api"; import {internalApi} from "@/utils/Api";
interface NodesStoreState { interface NodesStoreState {
@ -37,12 +37,13 @@ export const useNodesStore = defineStore({
}, },
}, },
actions: { actions: {
async refresh(page: number, nodesPerPage: number): Promise<void> { async refresh(page: number, nodesPerPage: number, filter: NodesFilter): Promise<void> {
const result = await internalApi.getPagedList<EnhancedNode>( const result = await internalApi.getPagedList<EnhancedNode>(
"nodes", "nodes",
isEnhancedNode, isEnhancedNode,
page, page,
nodesPerPage, nodesPerPage,
filter,
); );
this.nodes = result.entries; this.nodes = result.entries;
this.totalNodes = result.total; this.totalNodes = result.total;

View file

@ -1,5 +1,4 @@
import type {TypeGuard} from "@/types/shared"; import {toIsArray, type TypeGuard} from "@/types";
import {toIsArray} from "@/types/shared";
import type {Headers} from "request"; import type {Headers} from "request";
import {parseInteger} from "@/utils/Numbers"; import {parseInteger} from "@/utils/Numbers";
@ -63,10 +62,12 @@ class Api {
isT: TypeGuard<T>, isT: TypeGuard<T>,
page: number, page: number,
itemsPerPage: number, itemsPerPage: number,
filter?: object,
): Promise<PagedListResult<T>> { ): Promise<PagedListResult<T>> {
const response = await this.doGet(path, toIsArray(isT), { const response = await this.doGet(path, toIsArray(isT), {
_page: page, _page: page,
_perPage: itemsPerPage, _perPage: itemsPerPage,
...filter,
}); });
const totalStr = response.headers.get("x-total-count"); const totalStr = response.headers.get("x-total-count");
const total = parseInteger(totalStr, 10); const total = parseInteger(totalStr, 10);

View file

@ -1,12 +1,18 @@
<script setup lang="ts"> <script setup lang="ts">
import {useNodesStore} from "@/stores/nodes"; import {useNodesStore} from "@/stores/nodes";
import {onMounted, ref} from "vue"; import {onMounted, ref} from "vue";
import type {EnhancedNode, MAC} from "@/types"; import type {EnhancedNode, MAC, NodesFilter} from "@/types";
import Pager from "@/components/Pager.vue"; import Pager from "@/components/Pager.vue";
import LoadingContainer from "@/components/LoadingContainer.vue"; import LoadingContainer from "@/components/LoadingContainer.vue";
const NODE_PER_PAGE = 50; const NODE_PER_PAGE = 50;
interface Props {
filter: NodesFilter;
}
const props = defineProps<Props>();
type NodeRedactField = "nickname" | "email" | "token"; type NodeRedactField = "nickname" | "email" | "token";
type NodeRedactFieldsMap = Partial<Record<NodeRedactField, boolean>>; type NodeRedactFieldsMap = Partial<Record<NodeRedactField, boolean>>;
@ -21,7 +27,7 @@ async function refresh(page: number): Promise<void> {
loading.value = true; loading.value = true;
redactAllFields(true); redactAllFields(true);
try { try {
await nodes.refresh(page, NODE_PER_PAGE); await nodes.refresh(page, NODE_PER_PAGE, props.filter);
} finally { } finally {
loading.value = false; loading.value = false;
} }

View file

@ -23,6 +23,14 @@ export function isString(arg: unknown): arg is string {
return typeof arg === "string" return typeof arg === "string"
} }
export function isNumber(arg: unknown): arg is number {
return typeof arg === "number"
}
export function isBoolean(arg: unknown): arg is boolean {
return typeof arg === "boolean"
}
export function toIsArray<T>(isT: TypeGuard<T>): TypeGuard<T[]> { export function toIsArray<T>(isT: TypeGuard<T>): TypeGuard<T[]> {
return (arg): arg is T[] => isArray(arg, isT); return (arg): arg is T[] => isArray(arg, isT);
} }
@ -31,6 +39,10 @@ export function toIsEnum<E>(enumDef: E): TypeGuard<E> {
return (arg): arg is E => Object.values(enumDef).includes(arg as [keyof E]); return (arg): arg is E => Object.values(enumDef).includes(arg as [keyof E]);
} }
export function isOptional<T>(arg: unknown, isT: TypeGuard<T>): arg is (T | undefined) {
return arg === undefined || isT(arg);
}
export type Version = string; export type Version = string;
// Should be good enough for now. // Should be good enough for now.
@ -51,14 +63,13 @@ export function isNodeStatistics(arg: unknown): arg is NodeStatistics {
return false; return false;
} }
const stats = arg as NodeStatistics; const stats = arg as NodeStatistics;
// noinspection SuspiciousTypeOfGuard
return ( return (
typeof stats.registered === "number" && isNumber(stats.registered) &&
typeof stats.withVPN === "number" && isNumber(stats.withVPN) &&
typeof stats.withCoords === "number" && isNumber(stats.withCoords) &&
typeof stats.monitoring === "object" && isObject(stats.monitoring) &&
typeof stats.monitoring.active === "number" && isNumber(stats.monitoring.active) &&
typeof stats.monitoring.pending === "number" isNumber(stats.monitoring.pending)
); );
} }
@ -85,11 +96,10 @@ export function isCommunityConfig(arg: unknown): arg is CommunityConfig {
return false; return false;
} }
const cfg = arg as CommunityConfig; const cfg = arg as CommunityConfig;
// noinspection SuspiciousTypeOfGuard
return ( return (
typeof cfg.name === "string" && isString(cfg.name) &&
typeof cfg.domain === "string" && isString(cfg.domain) &&
typeof cfg.contactEmail === "string" && isString(cfg.contactEmail) &&
isArray(cfg.sites, isString) && isArray(cfg.sites, isString) &&
isArray(cfg.domains, isString) isArray(cfg.domains, isString)
); );
@ -107,10 +117,9 @@ export function isLegalConfig(arg: unknown): arg is LegalConfig {
return false; return false;
} }
const cfg = arg as LegalConfig; const cfg = arg as LegalConfig;
// noinspection SuspiciousTypeOfGuard
return ( return (
(cfg.privacyUrl === undefined || typeof cfg.privacyUrl === "string") && isOptional(cfg.privacyUrl, isString) &&
(cfg.imprintUrl === undefined || typeof cfg.imprintUrl === "string") isOptional(cfg.imprintUrl, isString)
); );
} }
@ -125,8 +134,7 @@ export function isClientMapConfig(arg: unknown): arg is ClientMapConfig {
return false; return false;
} }
const cfg = arg as ClientMapConfig; const cfg = arg as ClientMapConfig;
// noinspection SuspiciousTypeOfGuard return isString(cfg.mapUrl);
return typeof cfg.mapUrl === "string";
} }
export class MonitoringConfig { export class MonitoringConfig {
@ -140,8 +148,7 @@ export function isMonitoringConfig(arg: unknown): arg is MonitoringConfig {
return false; return false;
} }
const cfg = arg as MonitoringConfig; const cfg = arg as MonitoringConfig;
// noinspection SuspiciousTypeOfGuard return isBoolean(cfg.enabled);
return typeof cfg.enabled === "boolean";
} }
export class Coords { export class Coords {
@ -156,10 +163,9 @@ export function isCoords(arg: unknown): arg is Coords {
return false; return false;
} }
const coords = arg as Coords; const coords = arg as Coords;
// noinspection SuspiciousTypeOfGuard
return ( return (
typeof coords.lat === "number" && isNumber(coords.lat) &&
typeof coords.lng === "number" isNumber(coords.lng)
); );
} }
@ -177,11 +183,10 @@ export function isCoordsSelectorConfig(arg: unknown): arg is CoordsSelectorConfi
return false; return false;
} }
const cfg = arg as CoordsSelectorConfig; const cfg = arg as CoordsSelectorConfig;
// noinspection SuspiciousTypeOfGuard
return ( return (
typeof cfg.lat === "number" && isNumber(cfg.lat) &&
typeof cfg.lng === "number" && isNumber(cfg.lng) &&
typeof cfg.defaultZoom === "number" && isNumber(cfg.defaultZoom) &&
isObject(cfg.layers) // TODO: Better types! isObject(cfg.layers) // TODO: Better types!
); );
} }
@ -199,10 +204,9 @@ export function isOtherCommunityInfoConfig(arg: unknown): arg is OtherCommunityI
return false; return false;
} }
const cfg = arg as OtherCommunityInfoConfig; const cfg = arg as OtherCommunityInfoConfig;
// noinspection SuspiciousTypeOfGuard
return ( return (
typeof cfg.showInfo === "boolean" && isBoolean(cfg.showInfo) &&
typeof cfg.showBorderForDebugging === "boolean" && isBoolean(cfg.showBorderForDebugging) &&
isArray(cfg.localCommunityPolygon, isCoords) isArray(cfg.localCommunityPolygon, isCoords)
); );
} }
@ -225,7 +229,6 @@ export function isClientConfig(arg: unknown): arg is ClientConfig {
return false; return false;
} }
const cfg = arg as ClientConfig; const cfg = arg as ClientConfig;
// noinspection SuspiciousTypeOfGuard
return ( return (
isCommunityConfig(cfg.community) && isCommunityConfig(cfg.community) &&
isLegalConfig(cfg.legal) && isLegalConfig(cfg.legal) &&
@ -233,7 +236,7 @@ export function isClientConfig(arg: unknown): arg is ClientConfig {
isMonitoringConfig(cfg.monitoring) && isMonitoringConfig(cfg.monitoring) &&
isCoordsSelectorConfig(cfg.coordsSelector) && isCoordsSelectorConfig(cfg.coordsSelector) &&
isOtherCommunityInfoConfig(cfg.otherCommunityInfo) && isOtherCommunityInfoConfig(cfg.otherCommunityInfo) &&
typeof cfg.rootPath === "string" isString(cfg.rootPath)
); );
} }
@ -281,19 +284,18 @@ export function isNode(arg: unknown): arg is Node {
return false; return false;
} }
const node = arg as Node; const node = arg as Node;
// noinspection SuspiciousTypeOfGuard
return ( return (
isToken(node.token) && isToken(node.token) &&
typeof node.nickname === "string" && isString(node.nickname) &&
typeof node.email === "string" && isString(node.email) &&
typeof node.hostname === "string" && isString(node.hostname) &&
(node.coords === undefined || typeof node.coords === "string") && isOptional(node.coords, isString) &&
(node.key === undefined || isFastdKey(node.key)) && isOptional(node.key, isFastdKey) &&
isMAC(node.mac) && isMAC(node.mac) &&
typeof node.monitoring === "boolean" && isBoolean(node.monitoring) &&
typeof node.monitoringConfirmed === "boolean" && isBoolean(node.monitoringConfirmed) &&
isMonitoringState(node.monitoringState) && isMonitoringState(node.monitoringState) &&
typeof node.modifiedAt === "number" isNumber(node.modifiedAt)
); );
} }
@ -321,8 +323,26 @@ export function isEnhancedNode(arg: unknown): arg is EnhancedNode {
} }
const node = arg as EnhancedNode; const node = arg as EnhancedNode;
return ( return (
(node.site === undefined || isSite(node.site)) && isOptional(node.site, isSite) &&
(node.domain === undefined || isDomain(node.domain)) && isOptional(node.domain, isDomain) &&
(node.onlineState === undefined || isOnlineState(node.onlineState)) isOptional(node.onlineState, isOnlineState)
);
}
export interface NodesFilter {
hasKey?: boolean;
hasCoords?: boolean;
monitoringState?: MonitoringState;
}
export function isNodesFilter(arg: unknown): arg is NodesFilter {
if (!isObject(arg)) {
return false;
}
const filter = arg as NodesFilter;
return (
isOptional(filter.hasKey, isBoolean) &&
isOptional(filter.hasCoords, isBoolean) &&
isOptional(filter.monitoringState, isMonitoringState)
); );
} }