Nicer display of node list

This commit is contained in:
baldo 2022-05-30 13:54:57 +02:00
parent 9b5f8f3f37
commit cbd5624fb2
5 changed files with 144 additions and 12 deletions

View file

@ -1,9 +1,10 @@
import {defineStore} from "pinia"; import {defineStore} from "pinia";
import {isEnhancedNodes, type EnhancedNode} from "@/types"; import {isEnhancedNode, type EnhancedNode} from "@/types";
import {internalApi} from "@/utils/Api"; import {internalApi} from "@/utils/Api";
interface NodesStoreState { interface NodesStoreState {
nodes: EnhancedNode[]; nodes: EnhancedNode[];
totalNodes: number;
} }
export const useNodesStore = defineStore({ export const useNodesStore = defineStore({
@ -11,19 +12,26 @@ export const useNodesStore = defineStore({
state(): NodesStoreState { state(): NodesStoreState {
return { return {
nodes: [], nodes: [],
totalNodes: 0,
}; };
}, },
getters: { getters: {
getNodes(state: NodesStoreState): EnhancedNode[] { getNodes(state: NodesStoreState): EnhancedNode[] {
return state.nodes; return state.nodes;
}, },
getTotalNodes(state: NodesStoreState): number {
return state.totalNodes;
}
}, },
actions: { actions: {
async refresh(): Promise<void> { async refresh(): Promise<void> {
this.nodes = await internalApi.get<EnhancedNode[]>( const result = await internalApi.getPagedList<EnhancedNode>(
"nodes", "nodes",
isEnhancedNodes isEnhancedNode
); );
this.nodes = result.entries;
this.totalNodes = result.total;
}, },
}, },
}); });

View file

@ -1,4 +1,17 @@
import type {TypeGuard} from "@/types/shared"; import type {TypeGuard} from "@/types/shared";
import {toIsArray} from "@/types/shared";
import type {Headers} from "request";
import {parseInteger} from "@/utils/Numbers";
interface PagedListResult<T> {
entries: T[];
total: number;
}
interface Response<T> {
result: T;
headers: Headers;
}
class Api { class Api {
private baseURL: string = import.meta.env.BASE_URL; private baseURL: string = import.meta.env.BASE_URL;
@ -14,7 +27,7 @@ class Api {
return this.baseURL + this.apiPrefix + path; return this.baseURL + this.apiPrefix + path;
} }
async get<T>(path: string, isT: TypeGuard<T>): Promise<T> { private async doGet<T>(path: string, isT: TypeGuard<T>): Promise<Response<T>> {
const url = this.toURL(path); const url = this.toURL(path);
const result = await fetch(url); const result = await fetch(url);
const json = await result.json(); const json = await result.json();
@ -24,7 +37,26 @@ class Api {
throw new Error(`API get result has wrong type. ${url} => ${json}`); throw new Error(`API get result has wrong type. ${url} => ${json}`);
} }
return json; return {
result: json,
headers: result.headers,
};
}
async get<T>(path: string, isT: TypeGuard<T>): Promise<T> {
const response = await this.doGet(path, isT);
return response.result;
}
async getPagedList<T>(path: string, isT: TypeGuard<T>): Promise<PagedListResult<T>> {
const response = await this.doGet(path, toIsArray(isT));
const totalStr = response.headers.get("x-total-count");
const total = parseInteger(totalStr, 10);
return {
entries: response.result,
total,
}
} }
} }

View file

@ -0,0 +1,25 @@
// TODO: Write tests!
export function parseInteger(arg: any, radix: number): number {
if (Number.isInteger(arg)) {
return arg;
}
switch (typeof arg) {
case "number":
throw new Error(`Not an integer: ${arg}`);
case "string":
if (radix < 2 || radix > 36 || isNaN(radix)) {
throw new Error(`Radix out of range: ${radix}`);
}
const str = (arg as string).trim();
const num = parseInt(str, radix);
if (isNaN(num)) {
throw new Error(`Not a valid number (radix: ${radix}): ${str}`);
}
if (num.toString(radix).toLowerCase() !== str.toLowerCase()) {
throw new Error(`Parsed integer does not match given string (radix: {radix}): ${str}`);
}
return num;
default:
throw new Error(`Cannot parse number (radix: ${radix}): ${arg}`);
}
}

View file

@ -33,12 +33,20 @@ function setRedactField(node: EnhancedNode, field: NodeRedactField, value: boole
redactFieldsMap[field] = value; redactFieldsMap[field] = value;
} }
function hasOnlineState(node: EnhancedNode): boolean {
return node.onlineState !== undefined;
}
refresh(); refresh();
</script> </script>
<template> <template>
<main> <main>
<h2>Knoten</h2> <h2>Knoten</h2>
<div>
<span>Gesamt: {{nodes.getTotalNodes}}</span>
</div>
<table> <table>
<thead> <thead>
<tr> <tr>
@ -57,7 +65,9 @@ refresh();
</thead> </thead>
<tbody> <tbody>
<tr v-for="node in nodes.getNodes"> <tr
v-for="node in nodes.getNodes"
:class="[hasOnlineState ? node.onlineState.toLowerCase() : 'online-state-unknown']">
<td>{{node.hostname}}</td> <td>{{node.hostname}}</td>
<td v-if="shallRedactField(node, 'nickname')"> <td v-if="shallRedactField(node, 'nickname')">
<span <span
@ -102,12 +112,51 @@ refresh();
</span> </span>
</td> </td>
<td>{{node.mac}}</td> <td>{{node.mac}}</td>
<td>{{node.key}}</td><!-- TODO: Icon if set --> <td class="icon">
<i
v-if="node.key"
class="fa fa-lock"
aria-hidden="true"
title="Hat VPN-Schlüssel" />
<i
v-if="!node.key"
class="fa fa-times not-available"
aria-hidden="true"
title="Hat keinen VPN-Schlüssel" />
</td>
<td>{{node.site}}</td> <td>{{node.site}}</td>
<td>{{node.domain}}</td> <td>{{node.domain}}</td>
<td>{{node.coords}}</td><!-- TODO: Icon with link if set --> <td class="icon">
<td>{{node.onlineState}}</td> <i
<td>{{node.monitoring}}</td><!-- TODO: Icon regarding state --> v-if="node.coords"
class="fa fa-map-marker"
aria-hidden="true"
title="Hat Koordinaten" />
<i
v-if="!node.coords"
class="fa fa-times not-available"
aria-hidden="true"
title="Hat keinen Koordinaten" />
</td>
<td v-if="node.onlineState !== undefined">{{node.onlineState.toLowerCase()}}</td>
<td v-if="node.onlineState === undefined">unbekannt</td>
<td class="icon">
<i
v-if="node.monitoring && node.monitoringConfirmed"
class="fa fa-heartbeat"
aria-hidden="true"
title="Monitoring aktiv" />
<i
v-if="node.monitoring && !node.monitoringConfirmed"
class="fa fa-envelope"
aria-hidden="true"
title="Monitoring nicht bestätigt" />
<i
v-if="!node.monitoring"
class="fa fa-times not-available"
aria-hidden="true"
title="Monitoring deaktiviert" />
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@ -127,6 +176,22 @@ table {
border-top: 1px solid $gray-light; border-top: 1px solid $gray-light;
} }
.online {
color: $variant-color-success;
}
.offline {
color: $variant-color-danger;
}
.online-state-unknown {
color: $variant-color-warning;
}
.icon {
text-align: center;
}
.redacted, .redactable { .redacted, .redactable {
cursor: pointer; cursor: pointer;
} }
@ -134,5 +199,9 @@ table {
.redacted { .redacted {
filter: blur(0.2em); filter: blur(0.2em);
} }
.not-available {
color: $gray-dark
}
} }
</style> </style>

View file

@ -326,5 +326,3 @@ export function isEnhancedNode(arg: unknown): arg is EnhancedNode {
(node.onlineState === undefined || isOnlineState(node.onlineState)) (node.onlineState === undefined || isOnlineState(node.onlineState))
); );
} }
export const isEnhancedNodes = toIsArray(isEnhancedNode);