Apply filters to nodes list.
This commit is contained in:
parent
5b703917b5
commit
cce665d149
6 changed files with 96 additions and 77 deletions
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ 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 : {}
|
||||||
|
})
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue