Add sorting parameters and refactor server part to use enums for sorting.

This commit is contained in:
baldo 2022-06-23 14:26:15 +02:00
parent f95829adc6
commit a3dce0b8a2
9 changed files with 174 additions and 100 deletions

View file

@ -1,5 +1,5 @@
import {defineStore} from "pinia"; import {defineStore} from "pinia";
import {type EnhancedNode, isEnhancedNode, type NodesFilter} from "@/types"; import {type EnhancedNode, isEnhancedNode, type NodesFilter, NodeSortField, SortDirection} from "@/types";
import {internalApi} from "@/utils/Api"; import {internalApi} from "@/utils/Api";
interface NodesStoreState { interface NodesStoreState {
@ -7,6 +7,8 @@ interface NodesStoreState {
page: number; page: number;
nodesPerPage: number; nodesPerPage: number;
totalNodes: number; totalNodes: number;
sortDirection: SortDirection;
sortField: NodeSortField;
} }
export const useNodesStore = defineStore({ export const useNodesStore = defineStore({
@ -17,6 +19,8 @@ export const useNodesStore = defineStore({
page: 1, page: 1,
nodesPerPage: 20, nodesPerPage: 20,
totalNodes: 0, totalNodes: 0,
sortDirection: SortDirection.ASCENDING,
sortField: NodeSortField.HOSTNAME,
}; };
}, },
getters: { getters: {
@ -37,18 +41,27 @@ export const useNodesStore = defineStore({
}, },
}, },
actions: { actions: {
async refresh(page: number, nodesPerPage: number, filter: NodesFilter, searchTerm?: string): Promise<void> { async refresh(
page: number,
nodesPerPage: number,
sortDirection: SortDirection,
sortField: NodeSortField,
filter: NodesFilter,
searchTerm?: string
): Promise<void> {
const query: Record<string, any> = { const query: Record<string, any> = {
...filter, ...filter,
}; };
if (searchTerm) { if (searchTerm) {
query.q = searchTerm; query.q = searchTerm;
} }
const result = await internalApi.getPagedList<EnhancedNode>( const result = await internalApi.getPagedList<EnhancedNode, NodeSortField>(
"nodes", "nodes",
isEnhancedNode, isEnhancedNode,
page, page,
nodesPerPage, nodesPerPage,
sortDirection,
sortField,
query, query,
); );
this.nodes = result.entries; this.nodes = result.entries;

View file

@ -1,4 +1,4 @@
import {toIsArray, type TypeGuard} from "@/types"; import {SortDirection, toIsArray, type TypeGuard} from "@/types";
import type {Headers} from "request"; import type {Headers} from "request";
import {parseInteger} from "@/utils/Numbers"; import {parseInteger} from "@/utils/Numbers";
@ -57,16 +57,20 @@ class Api {
return response.result; return response.result;
} }
async getPagedList<T>( async getPagedList<Element, SortField>(
path: string, path: string,
isT: TypeGuard<T>, isElement: TypeGuard<Element>,
page: number, page: number,
itemsPerPage: number, itemsPerPage: number,
sortDirection?: SortDirection,
sortField?: SortField,
filter?: object, filter?: object,
): Promise<PagedListResult<T>> { ): Promise<PagedListResult<Element>> {
const response = await this.doGet(path, toIsArray(isT), { const response = await this.doGet(path, toIsArray(isElement), {
_page: page, _page: page,
_perPage: itemsPerPage, _perPage: itemsPerPage,
_sortDir: sortDirection,
_sortField: sortField,
...filter, ...filter,
}); });
const totalStr = response.headers.get("x-total-count"); const totalStr = response.headers.get("x-total-count");

View file

@ -2,6 +2,7 @@
import {useNodesStore} from "@/stores/nodes"; import {useNodesStore} from "@/stores/nodes";
import {onMounted, ref, watch} from "vue"; import {onMounted, ref, watch} from "vue";
import type {EnhancedNode, MAC, NodesFilter} from "@/types"; import type {EnhancedNode, MAC, NodesFilter} from "@/types";
import {NodeSortField, SortDirection} 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";
import NodesFilterPanel from "@/components/nodes/NodesFilterPanel.vue"; import NodesFilterPanel from "@/components/nodes/NodesFilterPanel.vue";
@ -32,7 +33,14 @@ 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, currentFilter.value, currentSearchTerm.value); await nodes.refresh(
page,
NODE_PER_PAGE,
SortDirection.ASCENDING,
NodeSortField.HOSTNAME,
currentFilter.value,
currentSearchTerm.value,
);
} finally { } finally {
loading.value = false; loading.value = false;
} }

View file

@ -10,7 +10,7 @@ import {forConstraint, forConstraints} from "../validation/validator";
import * as Resources from "../utils/resources"; import * as Resources from "../utils/resources";
import {Entity} from "../utils/resources"; import {Entity} from "../utils/resources";
import {Request, Response} from "express"; import {Request, Response} from "express";
import {EnhancedNode, Node} from "../types"; import {EnhancedNode, isNodeSortField, Node} from "../types";
const nodeFields = ['hostname', 'key', 'email', 'nickname', 'mac', 'coords', 'monitoring']; const nodeFields = ['hostname', 'key', 'email', 'nickname', 'mac', 'coords', 'monitoring'];
@ -130,19 +130,7 @@ async function doGetAll(req: Request): Promise<{ total: number; pageNodes: any }
const sortedNodes = Resources.sort( const sortedNodes = Resources.sort(
filteredNodes, filteredNodes,
[ isNodeSortField,
'hostname',
'nickname',
'email',
'token',
'mac',
'key',
'site',
'domain',
'coords',
'onlineState',
'monitoringState'
],
restParams restParams
); );
const pageNodes = Resources.getPageEntities(sortedNodes, restParams); const pageNodes = Resources.getPageEntities(sortedNodes, restParams);

View file

@ -8,6 +8,7 @@ import {getTasks, Task, TaskState} from "../jobs/scheduler";
import {normalizeString} from "../utils/strings"; import {normalizeString} from "../utils/strings";
import {forConstraint} from "../validation/validator"; import {forConstraint} from "../validation/validator";
import {Request, Response} from "express"; import {Request, Response} from "express";
import {isTaskSortField} from "../types";
const isValidId = forConstraint(CONSTRAINTS.id, false); const isValidId = forConstraint(CONSTRAINTS.id, false);
@ -81,7 +82,7 @@ async function doGetAll(req: Request): Promise<{total: number, pageTasks: Entity
const tasks = Resources.sort( const tasks = Resources.sort(
_.values(getTasks()), _.values(getTasks()),
['id', 'name', 'schedule', 'state', 'runningSince', 'lastRunStarted'], isTaskSortField,
restParams restParams
); );
const filteredTasks = Resources.filter( const filteredTasks = Resources.filter(

View file

@ -9,7 +9,7 @@ import Logger from "../logger";
import * as MailTemplateService from "./mailTemplateService"; import * as MailTemplateService from "./mailTemplateService";
import * as Resources from "../utils/resources"; import * as Resources from "../utils/resources";
import {RestParams} from "../utils/resources"; import {RestParams} from "../utils/resources";
import {Mail, MailData, MailId, MailType} from "../types"; import {isMailSortField, Mail, MailData, MailId, MailSortField, MailType} from "../types";
const MAIL_QUEUE_DB_BATCH_SIZE = 50; const MAIL_QUEUE_DB_BATCH_SIZE = 50;
@ -128,8 +128,8 @@ export async function getPendingMails (restParams: RestParams): Promise<{mails:
const filter = Resources.filterClause( const filter = Resources.filterClause(
restParams, restParams,
'id', MailSortField.ID,
['id', 'failures', 'sender', 'recipient', 'email', 'created_at', 'modified_at'], isMailSortField,
['id', 'failures', 'sender', 'recipient', 'email'] ['id', 'failures', 'sender', 'recipient', 'email']
); );

View file

@ -16,7 +16,17 @@ import {normalizeMac} from "../utils/strings";
import {monitoringDisableUrl} from "../utils/urlBuilder"; import {monitoringDisableUrl} from "../utils/urlBuilder";
import CONSTRAINTS from "../validation/constraints"; import CONSTRAINTS from "../validation/constraints";
import {forConstraint} from "../validation/validator"; import {forConstraint} from "../validation/validator";
import {MAC, MailType, Node, NodeId, OnlineState, NodeStateData, UnixTimestampSeconds} from "../types"; import {
isMonitoringSortField,
MAC,
MailType,
MonitoringSortField,
Node,
NodeId,
NodeStateData,
OnlineState,
UnixTimestampSeconds
} from "../types";
const MONITORING_STATE_MACS_CHUNK_SIZE = 100; const MONITORING_STATE_MACS_CHUNK_SIZE = 100;
const NEVER_ONLINE_NODES_DELETION_CHUNK_SIZE = 20; const NEVER_ONLINE_NODES_DELETION_CHUNK_SIZE = 20;
@ -255,8 +265,7 @@ export function parseNodesJson (body: string): NodesParsingResult {
const parsedNode = parseNode(result.importTimestamp, nodeData); const parsedNode = parseNode(result.importTimestamp, nodeData);
Logger.tag('monitoring', 'parsing-nodes-json').debug(`Parsing node successful: ${parsedNode.mac}`); Logger.tag('monitoring', 'parsing-nodes-json').debug(`Parsing node successful: ${parsedNode.mac}`);
result.nodes.push(parsedNode); result.nodes.push(parsedNode);
} } catch (error) {
catch (error) {
result.failedNodesCount += 1; result.failedNodesCount += 1;
Logger.tag('monitoring', 'parsing-nodes-json').error("Could not parse node.", error, nodeData); Logger.tag('monitoring', 'parsing-nodes-json').error("Could not parse node.", error, nodeData);
} }
@ -532,21 +541,6 @@ async function retrieveNodeInformationForUrls(urls: string[]): Promise<RetrieveN
} }
export async function getAll(restParams: RestParams): Promise<{ total: number, monitoringStates: any[] }> { export async function getAll(restParams: RestParams): Promise<{ total: number, monitoringStates: any[] }> {
const sortFields = [
'id',
'hostname',
'mac',
'site',
'domain',
'monitoring_state',
'state',
'last_seen',
'import_timestamp',
'last_status_mail_type',
'last_status_mail_sent',
'created_at',
'modified_at'
];
const filterFields = [ const filterFields = [
'hostname', 'hostname',
'mac', 'mac',
@ -566,8 +560,8 @@ export async function getAll(restParams: RestParams): Promise<{total: number, mo
const filter = Resources.filterClause( const filter = Resources.filterClause(
restParams, restParams,
'id', MonitoringSortField.ID,
sortFields, isMonitoringSortField,
filterFields filterFields
); );
@ -648,8 +642,7 @@ export async function sendMonitoringMails(): Promise<void> {
try { try {
await sendOnlineAgainMails(startTime); await sendOnlineAgainMails(startTime);
} } catch (error) {
catch (error) {
// only logging an continuing with next type // only logging an continuing with next type
Logger Logger
.tag('monitoring', 'mail-sending') .tag('monitoring', 'mail-sending')
@ -659,8 +652,7 @@ export async function sendMonitoringMails(): Promise<void> {
for (let mailNumber = 1; mailNumber <= 3; mailNumber++) { for (let mailNumber = 1; mailNumber <= 3; mailNumber++) {
try { try {
await sendOfflineMails(startTime, mailNumber); await sendOfflineMails(startTime, mailNumber);
} } catch (error) {
catch (error) {
// only logging an continuing with next type // only logging an continuing with next type
Logger Logger
.tag('monitoring', 'mail-sending') .tag('monitoring', 'mail-sending')
@ -786,8 +778,7 @@ async function deleteNodeByMac(mac: MAC): Promise<void> {
try { try {
node = await NodeService.getNodeDataByMac(mac); node = await NodeService.getNodeDataByMac(mac);
} } catch (error) {
catch (error) {
// Only log error. We try to delete the nodes state anyways. // Only log error. We try to delete the nodes state anyways.
Logger.tag('nodes', 'delete-offline').error('Could not find node to delete: ' + mac, error); Logger.tag('nodes', 'delete-offline').error('Could not find node to delete: ' + mac, error);
} }
@ -801,8 +792,7 @@ async function deleteNodeByMac(mac: MAC): Promise<void> {
'DELETE FROM node_state WHERE mac = ? AND state = ?', 'DELETE FROM node_state WHERE mac = ? AND state = ?',
[mac, 'OFFLINE'], [mac, 'OFFLINE'],
); );
} } catch (error) {
catch (error) {
// Only log error and continue with next node. // Only log error and continue with next node.
Logger.tag('nodes', 'delete-offline').error('Could not delete node state: ' + mac, error); Logger.tag('nodes', 'delete-offline').error('Could not delete node state: ' + mac, error);
} }

View file

@ -3,6 +3,9 @@ import {ArrayField, Field, RawJsonField} from "sparkson";
// Types shared with the client. // Types shared with the client.
export type TypeGuard<T> = (arg: unknown) => arg is T; export type TypeGuard<T> = (arg: unknown) => arg is T;
export type EnumValue<E> = E[keyof E];
export type EnumTypeGuard<E> = TypeGuard<EnumValue<E>>;
export function isObject(arg: unknown): arg is object { export function isObject(arg: unknown): arg is object {
return arg !== null && typeof arg === "object"; return arg !== null && typeof arg === "object";
} }
@ -39,8 +42,8 @@ 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);
} }
export function toIsEnum<E>(enumDef: E): TypeGuard<E> { export function toIsEnum<E>(enumDef: E): EnumTypeGuard<E> {
return (arg): arg is E => Object.values(enumDef).includes(arg as [keyof E]); return (arg): arg is EnumValue<E> => Object.values(enumDef).includes(arg as [keyof E]);
} }
export function isOptional<T>(arg: unknown, isT: TypeGuard<T>): arg is (T | undefined) { export function isOptional<T>(arg: unknown, isT: TypeGuard<T>): arg is (T | undefined) {
@ -333,6 +336,22 @@ export function isEnhancedNode(arg: unknown): arg is EnhancedNode {
); );
} }
export enum NodeSortField {
HOSTNAME = 'hostname',
NICKNAME = 'nickname',
EMAIL = 'email',
TOKEN = 'token',
MAC = 'mac',
KEY = 'key',
SITE = 'site',
DOMAIN = 'domain',
COORDS = 'coords',
ONLINE_STATE = 'onlineState',
MONITORING_STATE = 'monitoringState',
}
export const isNodeSortField = toIsEnum(NodeSortField);
export interface NodesFilter { export interface NodesFilter {
hasKey?: boolean; hasKey?: boolean;
hasCoords?: boolean; hasCoords?: boolean;
@ -365,3 +384,53 @@ export function isNodesFilter(arg: unknown): arg is NodesFilter {
isOptional(filter.onlineState, isOnlineState) isOptional(filter.onlineState, isOnlineState)
); );
} }
export enum MonitoringSortField {
ID = 'id',
HOSTNAME = 'hostname',
MAC = 'mac',
SITE = 'site',
DOMAIN = 'domain',
MONITORING_STATE = 'monitoring_state',
STATE = 'state',
LAST_SEEN = 'last_seen',
IMPORT_TIMESTAMP = 'import_timestamp',
LAST_STATUS_MAIL_TYPE = 'last_status_mail_type',
LAST_STATUS_MAIL_SENT = 'last_status_mail_sent',
CREATED_AT = 'created_at',
MODIFIED_AT = 'modified_at',
}
export const isMonitoringSortField = toIsEnum(MonitoringSortField);
export enum TaskSortField {
ID = 'id',
NAME = 'name',
SCHEDULE = 'schedule',
STATE = 'state',
RUNNING_SINCE = 'runningSince',
LAST_RUN_STARTED = 'lastRunStarted',
}
export const isTaskSortField = toIsEnum(TaskSortField);
export enum MailSortField {
ID = 'id',
FAILURES = 'failures',
SENDER = 'sender',
RECIPIENT = 'recipient',
EMAIL = 'email',
CREATED_AT = 'created_at',
MODIFIED_AT = 'modified_at',
}
export const isMailSortField = toIsEnum(MailSortField);
export type GenericSortField = string;
export enum SortDirection {
ASCENDING = "ASC",
DESCENDING = "DESC",
}
export const isSortDirection = toIsEnum(SortDirection);

View file

@ -5,14 +5,15 @@ import ErrorTypes from "../utils/errorTypes";
import Logger from "../logger"; import Logger from "../logger";
import {Constraints, forConstraints, isConstraints} from "../validation/validator"; import {Constraints, forConstraints, isConstraints} from "../validation/validator";
import {Request, Response} from "express"; import {Request, Response} from "express";
import {EnumTypeGuard, EnumValue, type GenericSortField, SortDirection, TypeGuard} from "../types";
export type Entity = { [key: string]: any }; export type Entity = { [key: string]: any };
export type RestParams = { export type RestParams = {
q?: string; q?: string;
_sortField?: string; _sortField?: GenericSortField;
_sortDir?: string; _sortDir?: SortDirection;
_page: number; _page: number;
_perPage: number; _perPage: number;
@ -38,18 +39,18 @@ function respond(res: Response, httpCode: number, data: any, type: string): void
} }
} }
function orderByClause( function orderByClause<S>(
restParams: RestParams, restParams: RestParams,
defaultSortField: string, defaultSortField: EnumValue<S>,
allowedSortFields: string[] isSortField: EnumTypeGuard<S>,
): OrderByClause { ): OrderByClause {
let sortField = _.includes(allowedSortFields, restParams._sortField) ? restParams._sortField : undefined; let sortField: EnumValue<S> | undefined = isSortField(restParams._sortField) ? restParams._sortField : undefined;
if (!sortField) { if (!sortField) {
sortField = defaultSortField; sortField = defaultSortField;
} }
return { return {
query: 'ORDER BY ' + sortField + ' ' + (restParams._sortDir === 'ASC' ? 'ASC' : 'DESC'), query: 'ORDER BY LOWER(' + sortField + ') ' + (restParams._sortDir === SortDirection.ASCENDING ? 'ASC' : 'DESC'),
params: [] params: []
}; };
} }
@ -208,14 +209,14 @@ export function filter<E>(entities: ArrayLike<E>, allowedFilterFields: string[],
}); });
} }
export function sort<T>(entities: ArrayLike<T>, allowedSortFields: string[], restParams: RestParams): ArrayLike<T> { export function sort<T, S>(entities: ArrayLike<T>, isSortField: TypeGuard<S>, restParams: RestParams): ArrayLike<T> {
const sortField = _.includes(allowedSortFields, restParams._sortField) ? restParams._sortField : undefined; const sortField: S | undefined = isSortField(restParams._sortField) ? restParams._sortField : undefined;
if (!sortField) { if (!sortField) {
return entities; return entities;
} }
const sorted: T[] = _.sortBy(entities, [sortField]); const sorted: T[] = _.sortBy(entities, [sortField]);
if (restParams._sortDir === 'ASC') { if (restParams._sortDir === SortDirection.ASCENDING) {
return sorted; return sorted;
} else { } else {
return _.reverse(sorted); return _.reverse(sorted);
@ -231,16 +232,16 @@ export function getPageEntities (entities: ArrayLike<Entity>, restParams: RestPa
export {filterCondition as whereCondition}; export {filterCondition as whereCondition};
export function filterClause ( export function filterClause<S>(
restParams: RestParams, restParams: RestParams,
defaultSortField: string, defaultSortField: EnumValue<S>,
allowedSortFields: string[], isSortField: EnumTypeGuard<S>,
filterFields: string[], filterFields: string[],
): FilterClause { ): FilterClause {
const orderBy = orderByClause( const orderBy = orderByClause<S>(
restParams, restParams,
defaultSortField, defaultSortField,
allowedSortFields isSortField,
); );
const limitOffset = limitOffsetClause(restParams); const limitOffset = limitOffsetClause(restParams);