Admin: Basic node listing
This commit is contained in:
parent
c9ac65eaad
commit
0f60436b2c
|
@ -1,5 +1,6 @@
|
||||||
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 HomeView from "@/views/HomeView.vue";
|
import HomeView from "@/views/HomeView.vue";
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
|
@ -15,6 +16,11 @@ const router = createRouter({
|
||||||
name: "admin",
|
name: "admin",
|
||||||
component: AdminDashboardView,
|
component: AdminDashboardView,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/admin/nodes",
|
||||||
|
name: "admin-nodes",
|
||||||
|
component: AdminNodesView,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
29
frontend/src/stores/nodes.ts
Normal file
29
frontend/src/stores/nodes.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import {defineStore} from "pinia";
|
||||||
|
import {isEnhancedNodes, type EnhancedNode} from "@/types";
|
||||||
|
import {internalApi} from "@/utils/Api";
|
||||||
|
|
||||||
|
interface NodesStoreState {
|
||||||
|
nodes: EnhancedNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useNodesStore = defineStore({
|
||||||
|
id: "nodes",
|
||||||
|
state(): NodesStoreState {
|
||||||
|
return {
|
||||||
|
nodes: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
getNodes(state: NodesStoreState): EnhancedNode[] {
|
||||||
|
return state.nodes;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
async refresh(): Promise<void> {
|
||||||
|
this.nodes = await internalApi.get<EnhancedNode[]>(
|
||||||
|
"nodes",
|
||||||
|
isEnhancedNodes
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
|
@ -1,3 +1,5 @@
|
||||||
|
import type {TypeGuard} from "@/types/shared";
|
||||||
|
|
||||||
class Api {
|
class Api {
|
||||||
private baseURL: string = import.meta.env.BASE_URL;
|
private baseURL: string = import.meta.env.BASE_URL;
|
||||||
private apiPrefix = "api/";
|
private apiPrefix = "api/";
|
||||||
|
@ -12,7 +14,7 @@ class Api {
|
||||||
return this.baseURL + this.apiPrefix + path;
|
return this.baseURL + this.apiPrefix + path;
|
||||||
}
|
}
|
||||||
|
|
||||||
async get<T>(path: string, isT: (arg: unknown) => arg is T): Promise<T> {
|
async get<T>(path: string, isT: TypeGuard<T>): Promise<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();
|
||||||
|
|
61
frontend/src/views/AdminNodesView.vue
Normal file
61
frontend/src/views/AdminNodesView.vue
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {useNodesStore} from "@/stores/nodes";
|
||||||
|
import {ref} from "vue";
|
||||||
|
import type {MAC} from "@/types";
|
||||||
|
|
||||||
|
type NodeRedactField = "nickname" | "email" | "token";
|
||||||
|
type NodeRedactFieldsMap = Partial<Record<NodeRedactField, boolean>>;
|
||||||
|
type NodesRedactFieldsMap = Partial<Record<MAC, NodeRedactFieldsMap>>;
|
||||||
|
|
||||||
|
const nodes = useNodesStore();
|
||||||
|
const page = ref(0);
|
||||||
|
|
||||||
|
function refresh(): void {
|
||||||
|
nodes.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<main>
|
||||||
|
<h2>Knoten</h2>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Besitzer*in</th>
|
||||||
|
<th>E-Mail</th>
|
||||||
|
<th>Token</th>
|
||||||
|
<th>MAC</th>
|
||||||
|
<th>VPN</th>
|
||||||
|
<th>Site</th>
|
||||||
|
<th>Domäne</th>
|
||||||
|
<th>GPS</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Monitoring</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="node in nodes.getNodes">
|
||||||
|
<td>{{node.hostname}}</td>
|
||||||
|
<td>{{node.nickname}}</td><!-- TODO: Redact values -->
|
||||||
|
<td>{{node.email}}</td><!-- TODO: Redact values -->
|
||||||
|
<td>{{node.token}}</td><!-- TODO: Redact values -->
|
||||||
|
<td>{{node.mac}}</td>
|
||||||
|
<td>{{node.key}}</td><!-- TODO: Icon if set -->
|
||||||
|
<td>{{node.site}}</td>
|
||||||
|
<td>{{node.domain}}</td>
|
||||||
|
<td>{{node.coords}}</td><!-- TODO: Icon with link if set -->
|
||||||
|
<td>{{node.onlineState}}</td>
|
||||||
|
<td>{{node.monitoring}}</td><!-- TODO: Icon regarding state -->
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
</style>
|
|
@ -7,6 +7,8 @@
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"],
|
"@/*": ["./src/*"],
|
||||||
},
|
},
|
||||||
|
"target": "es2018",
|
||||||
|
"lib": ["es2018"],
|
||||||
"experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
"experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||||
"emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
"emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||||
},
|
},
|
||||||
|
|
|
@ -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 {Node} from "../types";
|
import {EnhancedNode, Node} from "../types";
|
||||||
|
|
||||||
const nodeFields = ['hostname', 'key', 'email', 'nickname', 'mac', 'coords', 'monitoring'];
|
const nodeFields = ['hostname', 'key', 'email', 'nickname', 'mac', 'coords', 'monitoring'];
|
||||||
|
|
||||||
|
@ -97,7 +97,7 @@ async function doGetAll(req: Request): Promise<{ total: number; pageNodes: any }
|
||||||
const macs = _.map(realNodes, (node: Node): string => node.mac);
|
const macs = _.map(realNodes, (node: Node): string => node.mac);
|
||||||
const nodeStateByMac = await MonitoringService.getByMacs(macs);
|
const nodeStateByMac = await MonitoringService.getByMacs(macs);
|
||||||
|
|
||||||
const enhancedNodes: Entity[] = _.map(realNodes, (node: Node) => {
|
const enhancedNodes: EnhancedNode[] = _.map(realNodes, (node: Node): EnhancedNode => {
|
||||||
const nodeState = nodeStateByMac[node.mac];
|
const nodeState = nodeStateByMac[node.mac];
|
||||||
if (nodeState) {
|
if (nodeState) {
|
||||||
return deepExtend({}, node, {
|
return deepExtend({}, node, {
|
||||||
|
@ -107,10 +107,10 @@ async function doGetAll(req: Request): Promise<{ total: number; pageNodes: any }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return node;
|
return node as EnhancedNode;
|
||||||
});
|
});
|
||||||
|
|
||||||
const filteredNodes = Resources.filter(
|
const filteredNodes = Resources.filter<EnhancedNode>(
|
||||||
enhancedNodes,
|
enhancedNodes,
|
||||||
[
|
[
|
||||||
'hostname',
|
'hostname',
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import {ParsedNode, parseNode, parseNodesJson, parseTimestamp} from "./monitoringService";
|
import {ParsedNode, parseNode, parseNodesJson, parseTimestamp} from "./monitoringService";
|
||||||
import {NodeState} from "../types";
|
import {OnlineState} from "../types";
|
||||||
import Logger from '../logger';
|
import Logger from '../logger';
|
||||||
import {MockLogger} from "../__mocks__/logger";
|
import {MockLogger} from "../__mocks__/logger";
|
||||||
|
|
||||||
|
@ -242,7 +242,7 @@ test('parseNode() should succeed parsing node without site and domain', () => {
|
||||||
const expectedParsedNode: ParsedNode = {
|
const expectedParsedNode: ParsedNode = {
|
||||||
mac: "12:34:56:78:90:AB",
|
mac: "12:34:56:78:90:AB",
|
||||||
importTimestamp: importTimestamp,
|
importTimestamp: importTimestamp,
|
||||||
state: NodeState.ONLINE,
|
state: OnlineState.ONLINE,
|
||||||
lastSeen: parseTimestamp(TIMESTAMP_VALID_STRING),
|
lastSeen: parseTimestamp(TIMESTAMP_VALID_STRING),
|
||||||
site: '<unknown-site>',
|
site: '<unknown-site>',
|
||||||
domain: '<unknown-domain>'
|
domain: '<unknown-domain>'
|
||||||
|
@ -274,7 +274,7 @@ test('parseNode() should succeed parsing node with site and domain', () => {
|
||||||
const expectedParsedNode: ParsedNode = {
|
const expectedParsedNode: ParsedNode = {
|
||||||
mac: "12:34:56:78:90:AB",
|
mac: "12:34:56:78:90:AB",
|
||||||
importTimestamp: importTimestamp,
|
importTimestamp: importTimestamp,
|
||||||
state: NodeState.ONLINE,
|
state: OnlineState.ONLINE,
|
||||||
lastSeen: parseTimestamp(TIMESTAMP_VALID_STRING),
|
lastSeen: parseTimestamp(TIMESTAMP_VALID_STRING),
|
||||||
site: 'test-site',
|
site: 'test-site',
|
||||||
domain: 'test-domain'
|
domain: 'test-domain'
|
||||||
|
@ -463,7 +463,7 @@ test('parseNodesJson() should parse valid nodes', () => {
|
||||||
const expectedParsedNode: ParsedNode = {
|
const expectedParsedNode: ParsedNode = {
|
||||||
mac: "12:34:56:78:90:AB",
|
mac: "12:34:56:78:90:AB",
|
||||||
importTimestamp: parseTimestamp(TIMESTAMP_VALID_STRING),
|
importTimestamp: parseTimestamp(TIMESTAMP_VALID_STRING),
|
||||||
state: NodeState.ONLINE,
|
state: OnlineState.ONLINE,
|
||||||
lastSeen: parseTimestamp(TIMESTAMP_VALID_STRING),
|
lastSeen: parseTimestamp(TIMESTAMP_VALID_STRING),
|
||||||
site: 'test-site',
|
site: 'test-site',
|
||||||
domain: 'test-domain'
|
domain: 'test-domain'
|
||||||
|
|
|
@ -16,7 +16,7 @@ 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, NodeState, NodeStateData, UnixTimestampSeconds} from "../types";
|
import {MAC, MailType, Node, NodeId, OnlineState, NodeStateData, 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;
|
||||||
|
@ -37,7 +37,7 @@ const DELETE_OFFLINE_NODES_AFTER_DURATION: {amount: number, unit: unitOfTime.Dur
|
||||||
export type ParsedNode = {
|
export type ParsedNode = {
|
||||||
mac: string,
|
mac: string,
|
||||||
importTimestamp: Moment,
|
importTimestamp: Moment,
|
||||||
state: NodeState,
|
state: OnlineState,
|
||||||
lastSeen: Moment,
|
lastSeen: Moment,
|
||||||
site: string,
|
site: string,
|
||||||
domain: string,
|
domain: string,
|
||||||
|
@ -212,7 +212,7 @@ export function parseNode(importTimestamp: Moment, nodeData: any): ParsedNode {
|
||||||
return {
|
return {
|
||||||
mac: mac,
|
mac: mac,
|
||||||
importTimestamp: importTimestamp,
|
importTimestamp: importTimestamp,
|
||||||
state: isOnline ? NodeState.ONLINE : NodeState.OFFLINE,
|
state: isOnline ? OnlineState.ONLINE : OnlineState.OFFLINE,
|
||||||
lastSeen: lastSeen,
|
lastSeen: lastSeen,
|
||||||
site: site || '<unknown-site>',
|
site: site || '<unknown-site>',
|
||||||
domain: domain || '<unknown-domain>'
|
domain: domain || '<unknown-domain>'
|
||||||
|
@ -520,7 +520,7 @@ async function retrieveNodeInformationForUrls(urls: string[]): Promise<RetrieveN
|
||||||
'SET state = ?, modified_at = ?' +
|
'SET state = ?, modified_at = ?' +
|
||||||
'WHERE import_timestamp < ?',
|
'WHERE import_timestamp < ?',
|
||||||
[
|
[
|
||||||
NodeState.OFFLINE, moment().unix(),
|
OnlineState.OFFLINE, moment().unix(),
|
||||||
minTimestamp.unix()
|
minTimestamp.unix()
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,49 +1,15 @@
|
||||||
|
import {Domain, MonitoringToken, OnlineState, Site} from "./shared";
|
||||||
|
|
||||||
export * from "./config";
|
export * from "./config";
|
||||||
export * from "./logger";
|
export * from "./logger";
|
||||||
export * from "./shared";
|
export * from "./shared";
|
||||||
|
|
||||||
// TODO: Token type.
|
|
||||||
export type Token = string;
|
|
||||||
export type FastdKey = string;
|
|
||||||
export type MAC = string;
|
|
||||||
|
|
||||||
export type UnixTimestampSeconds = number;
|
|
||||||
export type UnixTimestampMilliseconds = number;
|
|
||||||
|
|
||||||
export type MonitoringToken = string;
|
|
||||||
export enum MonitoringState {
|
|
||||||
ACTIVE = "active",
|
|
||||||
PENDING = "pending",
|
|
||||||
DISABLED = "disabled",
|
|
||||||
}
|
|
||||||
|
|
||||||
export type NodeId = string;
|
|
||||||
|
|
||||||
export enum NodeState {
|
|
||||||
ONLINE = "ONLINE",
|
|
||||||
OFFLINE = "OFFLINE",
|
|
||||||
}
|
|
||||||
|
|
||||||
export type NodeStateData = {
|
export type NodeStateData = {
|
||||||
site: string,
|
site: Site,
|
||||||
domain: string,
|
domain: Domain,
|
||||||
state: NodeState,
|
state: OnlineState,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Node = {
|
|
||||||
token: Token;
|
|
||||||
nickname: string;
|
|
||||||
email: string;
|
|
||||||
hostname: string;
|
|
||||||
coords?: string; // TODO: Use object with longitude and latitude.
|
|
||||||
key?: FastdKey;
|
|
||||||
mac: MAC;
|
|
||||||
monitoring: boolean;
|
|
||||||
monitoringConfirmed: boolean;
|
|
||||||
monitoringState: MonitoringState;
|
|
||||||
modifiedAt: UnixTimestampSeconds;
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: Complete interface / class declaration.
|
// TODO: Complete interface / class declaration.
|
||||||
export type NodeSecrets = {
|
export type NodeSecrets = {
|
||||||
monitoringToken?: MonitoringToken,
|
monitoringToken?: MonitoringToken,
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import {ArrayField, Field, RawJsonField} from "sparkson";
|
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 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";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isArray<T>(arg: unknown, isT: (arg: unknown) => arg is T): arg is Array<T> {
|
export function isArray<T>(arg: unknown, isT: TypeGuard<T>): arg is Array<T> {
|
||||||
if (!Array.isArray(arg)) {
|
if (!Array.isArray(arg)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -22,12 +23,18 @@ export function isString(arg: unknown): arg is string {
|
||||||
return typeof arg === "string"
|
return typeof arg === "string"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function toIsArray<T>(isT: TypeGuard<T>): TypeGuard<T[]> {
|
||||||
|
return (arg): arg is T[] => isArray(arg, isT);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toIsEnum<E>(enumDef: E): TypeGuard<E> {
|
||||||
|
return (arg): arg is E => Object.values(enumDef).includes(arg as [keyof E]);
|
||||||
|
}
|
||||||
|
|
||||||
export type Version = string;
|
export type Version = string;
|
||||||
|
|
||||||
export function isVersion(arg: unknown): arg is Version {
|
|
||||||
// Should be good enough for now.
|
// Should be good enough for now.
|
||||||
return typeof arg === "string";
|
export const isVersion = isString;
|
||||||
}
|
|
||||||
|
|
||||||
export type NodeStatistics = {
|
export type NodeStatistics = {
|
||||||
registered: number;
|
registered: number;
|
||||||
|
@ -229,3 +236,95 @@ export function isClientConfig(arg: unknown): arg is ClientConfig {
|
||||||
typeof cfg.rootPath === "string"
|
typeof cfg.rootPath === "string"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Token type.
|
||||||
|
export type Token = string;
|
||||||
|
export const isToken = isString;
|
||||||
|
|
||||||
|
export type FastdKey = string;
|
||||||
|
export const isFastdKey = isString;
|
||||||
|
|
||||||
|
export type MAC = string;
|
||||||
|
export const isMAC = isString;
|
||||||
|
|
||||||
|
export type UnixTimestampSeconds = number;
|
||||||
|
export type UnixTimestampMilliseconds = number;
|
||||||
|
|
||||||
|
export type MonitoringToken = string;
|
||||||
|
export enum MonitoringState {
|
||||||
|
ACTIVE = "active",
|
||||||
|
PENDING = "pending",
|
||||||
|
DISABLED = "disabled",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isMonitoringState = toIsEnum(MonitoringState);
|
||||||
|
|
||||||
|
export type NodeId = string;
|
||||||
|
export const isNodeId = isString;
|
||||||
|
|
||||||
|
export interface Node {
|
||||||
|
token: Token;
|
||||||
|
nickname: string;
|
||||||
|
email: string;
|
||||||
|
hostname: string;
|
||||||
|
coords?: string; // TODO: Use object with longitude and latitude.
|
||||||
|
key?: FastdKey;
|
||||||
|
mac: MAC;
|
||||||
|
monitoring: boolean;
|
||||||
|
monitoringConfirmed: boolean;
|
||||||
|
monitoringState: MonitoringState;
|
||||||
|
modifiedAt: UnixTimestampSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isNode(arg: unknown): arg is Node {
|
||||||
|
if (!isObject(arg)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const node = arg as Node;
|
||||||
|
// noinspection SuspiciousTypeOfGuard
|
||||||
|
return (
|
||||||
|
isToken(node.token) &&
|
||||||
|
typeof node.nickname === "string" &&
|
||||||
|
typeof node.email === "string" &&
|
||||||
|
typeof node.hostname === "string" &&
|
||||||
|
(node.coords === undefined || typeof node.coords === "string") &&
|
||||||
|
(node.key === undefined || isFastdKey(node.key)) &&
|
||||||
|
isMAC(node.mac) &&
|
||||||
|
typeof node.monitoring === "boolean" &&
|
||||||
|
typeof node.monitoringConfirmed === "boolean" &&
|
||||||
|
isMonitoringState(node.monitoringState) &&
|
||||||
|
typeof node.modifiedAt === "number"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum OnlineState {
|
||||||
|
ONLINE = "ONLINE",
|
||||||
|
OFFLINE = "OFFLINE",
|
||||||
|
}
|
||||||
|
export const isOnlineState = toIsEnum(OnlineState);
|
||||||
|
|
||||||
|
export type Site = string;
|
||||||
|
export const isSite = isString;
|
||||||
|
|
||||||
|
export type Domain = string;
|
||||||
|
export const isDomain = isString;
|
||||||
|
|
||||||
|
export interface EnhancedNode extends Node {
|
||||||
|
site?: Site,
|
||||||
|
domain?: Domain,
|
||||||
|
onlineState?: OnlineState,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isEnhancedNode(arg: unknown): arg is EnhancedNode {
|
||||||
|
if (!isNode(arg)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const node = arg as EnhancedNode;
|
||||||
|
return (
|
||||||
|
(node.site === undefined || isSite(node.site)) &&
|
||||||
|
(node.domain === undefined || isDomain(node.domain)) &&
|
||||||
|
(node.onlineState === undefined || isOnlineState(node.onlineState))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isEnhancedNodes = toIsArray(isEnhancedNode);
|
||||||
|
|
|
@ -152,7 +152,7 @@ export async function getValidRestParams(
|
||||||
return restParams as RestParams;
|
return restParams as RestParams;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function filter (entities: ArrayLike<Entity>, allowedFilterFields: string[], restParams: RestParams) {
|
export function filter<E>(entities: ArrayLike<E>, allowedFilterFields: string[], restParams: RestParams): E[] {
|
||||||
let query = restParams.q;
|
let query = restParams.q;
|
||||||
if (query) {
|
if (query) {
|
||||||
query = _.toLower(query.trim());
|
query = _.toLower(query.trim());
|
||||||
|
|
Loading…
Reference in a new issue