diff --git a/frontend/src/stores/nodes.ts b/frontend/src/stores/nodes.ts index f559cdf..03ebd63 100644 --- a/frontend/src/stores/nodes.ts +++ b/frontend/src/stores/nodes.ts @@ -1,9 +1,10 @@ import {defineStore} from "pinia"; -import {isEnhancedNodes, type EnhancedNode} from "@/types"; +import {isEnhancedNode, type EnhancedNode} from "@/types"; import {internalApi} from "@/utils/Api"; interface NodesStoreState { nodes: EnhancedNode[]; + totalNodes: number; } export const useNodesStore = defineStore({ @@ -11,19 +12,26 @@ export const useNodesStore = defineStore({ state(): NodesStoreState { return { nodes: [], + totalNodes: 0, }; }, getters: { getNodes(state: NodesStoreState): EnhancedNode[] { return state.nodes; }, + + getTotalNodes(state: NodesStoreState): number { + return state.totalNodes; + } }, actions: { async refresh(): Promise { - this.nodes = await internalApi.get( + const result = await internalApi.getPagedList( "nodes", - isEnhancedNodes + isEnhancedNode ); + this.nodes = result.entries; + this.totalNodes = result.total; }, }, }); diff --git a/frontend/src/utils/Api.ts b/frontend/src/utils/Api.ts index 94596b8..a7308ea 100644 --- a/frontend/src/utils/Api.ts +++ b/frontend/src/utils/Api.ts @@ -1,4 +1,17 @@ import type {TypeGuard} from "@/types/shared"; +import {toIsArray} from "@/types/shared"; +import type {Headers} from "request"; +import {parseInteger} from "@/utils/Numbers"; + +interface PagedListResult { + entries: T[]; + total: number; +} + +interface Response { + result: T; + headers: Headers; +} class Api { private baseURL: string = import.meta.env.BASE_URL; @@ -14,7 +27,7 @@ class Api { return this.baseURL + this.apiPrefix + path; } - async get(path: string, isT: TypeGuard): Promise { + private async doGet(path: string, isT: TypeGuard): Promise> { const url = this.toURL(path); const result = await fetch(url); const json = await result.json(); @@ -24,7 +37,26 @@ class Api { throw new Error(`API get result has wrong type. ${url} => ${json}`); } - return json; + return { + result: json, + headers: result.headers, + }; + } + + async get(path: string, isT: TypeGuard): Promise { + const response = await this.doGet(path, isT); + return response.result; + } + + async getPagedList(path: string, isT: TypeGuard): Promise> { + 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, + } } } diff --git a/frontend/src/utils/Numbers.ts b/frontend/src/utils/Numbers.ts new file mode 100644 index 0000000..3727305 --- /dev/null +++ b/frontend/src/utils/Numbers.ts @@ -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}`); + } +} diff --git a/frontend/src/views/AdminNodesView.vue b/frontend/src/views/AdminNodesView.vue index 58a5e4b..fe0f7bc 100644 --- a/frontend/src/views/AdminNodesView.vue +++ b/frontend/src/views/AdminNodesView.vue @@ -33,12 +33,20 @@ function setRedactField(node: EnhancedNode, field: NodeRedactField, value: boole redactFieldsMap[field] = value; } +function hasOnlineState(node: EnhancedNode): boolean { + return node.onlineState !== undefined; +} + refresh();