refactor: replace array-based expressions with typed class hierarchy
Replace the old Lisp-like array expression representation (["AND", ["PARAM", ...], ...]) with a class hierarchy rooted in an abstract Expression class (Literal, Parameter, Binary, Unary, FunctionCall, Conditional). This gives compile-time type safety to expression construction, evaluation, and pattern matching. Key changes: - New lib/common/expression.ts defines the class hierarchy with map/evaluate/toString methods - Rewrite parser as hand-rolled recursive descent, removing the parsimmon dependency - Extend Path with a colon separator to unify parameter identity and attributes into a single address space (e.g. "Device.WiFi.SSID.1.Name:writable"). The colon field tracks how many trailing segments are attribute qualifiers. PathSet rewritten with separate param/attr segment indexes - Flatten device attribute representation in the UI to colon-delimited keys mirroring the new path syntax
This commit is contained in:
1 parent
75908e624f
commit
6f3ce6f7c8
71 files changed
+4045
-3449
No files matched your search
@@ -20,19 +20,37 @@ import { contextifyComponent } from "./components.ts";
|
||||
import { PermissionSet, UiConfig } from "../lib/types.ts";
|
||||
import drawerComponent from "./drawer-component.ts";
|
||||
import { render as renderOverlay } from "./overlay.ts";
|
||||
import Expression from "../lib/common/expression.ts";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
authorizer: Authorizer;
|
||||
permissionSets: PermissionSet[];
|
||||
permissionSets: {
|
||||
[resource: string]: { access: number; validate: string; filter: string };
|
||||
}[][];
|
||||
username: string;
|
||||
clientConfig: { ui: UiConfig };
|
||||
clientConfig: UiConfig;
|
||||
configSnapshot: string;
|
||||
genieacsVersion: string;
|
||||
}
|
||||
}
|
||||
|
||||
window.authorizer = new Authorizer(window.permissionSets);
|
||||
const permissionSets: PermissionSet[] = window.permissionSets.map((p) =>
|
||||
p.map((s) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(s).map(([resource, { access, validate, filter }]) => [
|
||||
resource,
|
||||
{
|
||||
access,
|
||||
validate: Expression.parse(validate),
|
||||
filter: Expression.parse(filter),
|
||||
},
|
||||
]),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
window.authorizer = new Authorizer(permissionSets);
|
||||
|
||||
let state;
|
||||
|
||||
|
||||
@@ -1,26 +1,41 @@
|
||||
import { ClosureComponent } from "mithril";
|
||||
import { m } from "../components.ts";
|
||||
import * as taskQueue from "../task-queue.ts";
|
||||
import { parse } from "../../lib/common/expression/parser.ts";
|
||||
import memoize from "../../lib/common/memoize.ts";
|
||||
import { icon } from "../tailwind-utility-components.ts";
|
||||
import { QueryResponse, evaluateExpression } from "../store.ts";
|
||||
import debounce from "../../lib/common/debounce.ts";
|
||||
import { Expression } from "../../lib/types.ts";
|
||||
import Expression, { Value } from "../../lib/common/expression.ts";
|
||||
import { FlatDevice } from "../../lib/ui/db.ts";
|
||||
|
||||
const memoizedParse = memoize(parse);
|
||||
import Path from "../../lib/common/path.ts";
|
||||
|
||||
function escapeRegExp(str): string {
|
||||
return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&");
|
||||
}
|
||||
|
||||
const keysByDepth: WeakMap<Record<string, unknown>, string[][]> = new WeakMap();
|
||||
interface Parameter {
|
||||
path: Expression.Parameter;
|
||||
value?: Value;
|
||||
writable?: boolean;
|
||||
object?: boolean;
|
||||
}
|
||||
|
||||
function orderKeysByDepth(device: Record<string, unknown>): string[][] {
|
||||
if (keysByDepth.has(device)) return keysByDepth.get(device);
|
||||
const res: string[][] = [];
|
||||
for (const key of Object.keys(device)) {
|
||||
const prepareParams = memoize((device: FlatDevice): Parameter[][] => {
|
||||
const map = new Map<string, Parameter>();
|
||||
|
||||
for (const [k, v] of Object.entries(device)) {
|
||||
const [param, attr] = k.split(":");
|
||||
let attrs = map.get(param);
|
||||
if (!attrs)
|
||||
map.set(
|
||||
param,
|
||||
(attrs = { path: new Expression.Parameter(Path.parse(param)) }),
|
||||
);
|
||||
attrs[attr || "value"] = v;
|
||||
}
|
||||
|
||||
const res: Parameter[][] = [];
|
||||
for (const [key, attrs] of map) {
|
||||
let count = 0;
|
||||
for (
|
||||
let i = key.lastIndexOf(".", key.length - 2);
|
||||
@@ -29,11 +44,10 @@ function orderKeysByDepth(device: Record<string, unknown>): string[][] {
|
||||
)
|
||||
++count;
|
||||
while (res.length <= count) res.push([]);
|
||||
res[count].push(key);
|
||||
res[count].push(attrs);
|
||||
}
|
||||
keysByDepth.set(device, res);
|
||||
return res;
|
||||
}
|
||||
});
|
||||
|
||||
interface Attrs {
|
||||
device: FlatDevice;
|
||||
@@ -51,9 +65,13 @@ const component: ClosureComponent<Attrs> = () => {
|
||||
return {
|
||||
view: (vnode) => {
|
||||
const device = vnode.attrs.device;
|
||||
const allParams = prepareParams(device);
|
||||
|
||||
const limit =
|
||||
(evaluateExpression(vnode.attrs.limit, device) as number) || 100;
|
||||
let limit = 100;
|
||||
if (vnode.attrs.limit) {
|
||||
const l = evaluateExpression(vnode.attrs.limit, device);
|
||||
if (typeof l.value === "number") limit = l.value;
|
||||
}
|
||||
|
||||
const search = m(
|
||||
"input.appearance-none border-0 block w-full px-4 py-3 border-stone-300 placeholder-stone-500 text-stone-900 focus:ring-cyan-500 text-sm rounded-t-lg font-mono focus:ring-2",
|
||||
@@ -75,35 +93,36 @@ const component: ClosureComponent<Attrs> = () => {
|
||||
re = new RegExp(keywords.map((s) => escapeRegExp(s)).join(".*"), "i");
|
||||
}
|
||||
|
||||
const filteredKeys: string[] = [];
|
||||
const allKeys = orderKeysByDepth(device);
|
||||
const filteredParams: Parameter[] = [];
|
||||
let count = 0;
|
||||
for (const keys of allKeys) {
|
||||
for (const keys of allParams) {
|
||||
let c = 0;
|
||||
for (const k of keys) {
|
||||
const p = device[k];
|
||||
const str = p.value?.[0] ? `${k} ${p.value[0]}` : k;
|
||||
const str = k.value ? `${k.path.toString()} ${k.value}` : k;
|
||||
if (re && !re.test(str)) continue;
|
||||
++c;
|
||||
if (count < limit) filteredKeys.push(k);
|
||||
if (count < limit) filteredParams.push(k);
|
||||
}
|
||||
count += c;
|
||||
}
|
||||
|
||||
filteredKeys.sort();
|
||||
filteredParams.sort((a, b) => {
|
||||
if (a.path < b.path) return -1;
|
||||
if (a.path > b.path) return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
const rows = filteredKeys.map((k) => {
|
||||
const p = device[k];
|
||||
const rows = filteredParams.map((p) => {
|
||||
const val = [];
|
||||
if (p.object === false) {
|
||||
if (p.value) {
|
||||
val.push(
|
||||
m(
|
||||
"parameter",
|
||||
Object.assign({ device: device, parameter: memoizedParse(k) }),
|
||||
Object.assign({ device: device, parameter: p.path }),
|
||||
),
|
||||
);
|
||||
} else if (p.object && p.writable) {
|
||||
if (instanceRegex.test(k)) {
|
||||
if (instanceRegex.test(p.path.toString())) {
|
||||
val.push(
|
||||
m(
|
||||
"button",
|
||||
@@ -112,8 +131,8 @@ const component: ClosureComponent<Attrs> = () => {
|
||||
onclick: () => {
|
||||
taskQueue.queueTask({
|
||||
name: "deleteObject",
|
||||
device: device["DeviceID.ID"].value[0] as string,
|
||||
objectName: k,
|
||||
device: device["DeviceID.ID"] as string,
|
||||
objectName: p.path.toString(),
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -133,8 +152,8 @@ const component: ClosureComponent<Attrs> = () => {
|
||||
onclick: () => {
|
||||
taskQueue.queueTask({
|
||||
name: "addObject",
|
||||
device: device["DeviceID.ID"].value[0] as string,
|
||||
objectName: k,
|
||||
device: device["DeviceID.ID"] as string,
|
||||
objectName: p.path.toString(),
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -156,8 +175,8 @@ const component: ClosureComponent<Attrs> = () => {
|
||||
onclick: () => {
|
||||
taskQueue.queueTask({
|
||||
name: "getParameterValues",
|
||||
device: device["DeviceID.ID"].value[0] as string,
|
||||
parameterNames: [k],
|
||||
device: device["DeviceID.ID"] as string,
|
||||
parameterNames: [p.path.toString()],
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -170,7 +189,10 @@ const component: ClosureComponent<Attrs> = () => {
|
||||
|
||||
return m(
|
||||
"tr",
|
||||
m("td.pl-4 pr-2 py-2 truncate", m("long-text", { text: k })),
|
||||
m(
|
||||
"td.pl-4 pr-2 py-2 truncate",
|
||||
m("long-text", { text: p.path.toString() }),
|
||||
),
|
||||
m("td.pr-4 py-2 text-right flex justify-end", val),
|
||||
);
|
||||
});
|
||||
@@ -195,7 +217,7 @@ const component: ClosureComponent<Attrs> = () => {
|
||||
m(
|
||||
"span.text-xs",
|
||||
"Displaying ",
|
||||
m("span.font-medium", "" + filteredKeys.length),
|
||||
m("span.font-medium", "" + filteredParams.length),
|
||||
" out of ",
|
||||
m("span.font-medium", "" + count),
|
||||
" parameters",
|
||||
@@ -204,7 +226,7 @@ const component: ClosureComponent<Attrs> = () => {
|
||||
"a.text-cyan-700 hover:text-cyan-900 text-sm font-medium",
|
||||
{
|
||||
href: `api/devices/${encodeURIComponent(
|
||||
device["DeviceID.ID"].value[0],
|
||||
device["DeviceID.ID"],
|
||||
)}.csv`,
|
||||
download: "",
|
||||
},
|
||||
|
||||
+16
-15
@@ -1,7 +1,6 @@
|
||||
import { Attributes, ClosureComponent } from "mithril";
|
||||
import { map } from "../../lib/common/expression/parser.ts";
|
||||
import memoize from "../../lib/common/memoize.ts";
|
||||
import { Expression } from "../../lib/types.ts";
|
||||
import Expression from "../../lib/common/expression.ts";
|
||||
import { m } from "../components.ts";
|
||||
import { evaluateExpression, getTimestamp } from "../store.ts";
|
||||
import { FlatDevice } from "../../lib/ui/db.ts";
|
||||
@@ -14,15 +13,16 @@ const evaluateAttributes = memoize(
|
||||
): Attributes => {
|
||||
const res: Attributes = {};
|
||||
for (const [k, v] of Object.entries(attrs)) {
|
||||
const vv = map(v, (e) => {
|
||||
if (
|
||||
Array.isArray(e) &&
|
||||
e[0] === "FUNC" &&
|
||||
e[1] === "ENCODEURICOMPONENT"
|
||||
) {
|
||||
const a = evaluateExpression(e[2], obj);
|
||||
if (a == null) return null;
|
||||
return encodeURIComponent(a as string);
|
||||
const vv = v.evaluate((e) => {
|
||||
if (e instanceof Expression.Literal) return e;
|
||||
else if (e instanceof Expression.FunctionCall) {
|
||||
if (e.name === "ENCODEURICOMPONENT") {
|
||||
const a = evaluateExpression(e.args[0], obj);
|
||||
if (a instanceof Expression.Literal) {
|
||||
if (a.value == null) return new Expression.Literal(null);
|
||||
return new Expression.Literal(encodeURIComponent(a.value));
|
||||
}
|
||||
}
|
||||
}
|
||||
return e;
|
||||
});
|
||||
@@ -47,9 +47,10 @@ const component: ClosureComponent<Attrs> = () => {
|
||||
}
|
||||
|
||||
const children = Object.values(vnode.attrs.components).map((c) => {
|
||||
if (Array.isArray(c)) c = evaluateExpression(c, device || {});
|
||||
if (c instanceof Expression)
|
||||
c = evaluateExpression(c, device || {}).value;
|
||||
if (typeof c !== "object") return `${c}`;
|
||||
const type = evaluateExpression(c["type"], device || {});
|
||||
const type = evaluateExpression(c["type"], device || {}).value;
|
||||
if (!type) return null;
|
||||
return m(type as string, c);
|
||||
});
|
||||
@@ -59,8 +60,8 @@ const component: ClosureComponent<Attrs> = () => {
|
||||
if (el == null) return children;
|
||||
|
||||
let attrs: Attributes;
|
||||
if (Array.isArray(el)) {
|
||||
el = evaluateExpression(el, device || {});
|
||||
if (el instanceof Expression) {
|
||||
el = evaluateExpression(el, device || {}).value;
|
||||
} else if (typeof el === "object") {
|
||||
if (el["attributes"] != null) {
|
||||
attrs = evaluateAttributes(
|
||||
|
||||
@@ -19,7 +19,7 @@ const component: ClosureComponent = (): Component => {
|
||||
onclick: () => {
|
||||
taskQueue.queueTask({
|
||||
name: "reboot",
|
||||
device: device["DeviceID.ID"].value[0],
|
||||
device: device["DeviceID.ID"],
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -35,7 +35,7 @@ const component: ClosureComponent = (): Component => {
|
||||
onclick: () => {
|
||||
taskQueue.queueTask({
|
||||
name: "factoryReset",
|
||||
device: device["DeviceID.ID"].value[0],
|
||||
device: device["DeviceID.ID"],
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -51,7 +51,7 @@ const component: ClosureComponent = (): Component => {
|
||||
onclick: () => {
|
||||
taskQueue.stageDownload({
|
||||
name: "download",
|
||||
devices: [device["DeviceID.ID"].value[0]],
|
||||
devices: [device["DeviceID.ID"]],
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -66,7 +66,7 @@ const component: ClosureComponent = (): Component => {
|
||||
title: "Delete device",
|
||||
onclick: () => {
|
||||
if (!confirm("Deleting this device. Are you sure?")) return;
|
||||
const deviceId = device["DeviceID.ID"].value[0];
|
||||
const deviceId = device["DeviceID.ID"];
|
||||
|
||||
store
|
||||
.deleteResource("devices", deviceId)
|
||||
|
||||
@@ -3,17 +3,24 @@ import { m } from "../components.ts";
|
||||
import * as store from "../store.ts";
|
||||
import * as notifications from "../notifications.ts";
|
||||
import { stringify } from "../../lib/common/yaml.ts";
|
||||
import Expression from "../../lib/common/expression.ts";
|
||||
import Path from "../../lib/common/path.ts";
|
||||
|
||||
const component: ClosureComponent = (): Component => {
|
||||
return {
|
||||
view: (vnode) => {
|
||||
const device = vnode.attrs["device"];
|
||||
const deviceId = device["DeviceID.ID"].value[0];
|
||||
const faults = store.fetch("faults", [
|
||||
"AND",
|
||||
[">", ["PARAM", "_id"], `${deviceId}:`],
|
||||
["<", ["PARAM", "_id"], `${deviceId}:zzzz`],
|
||||
]);
|
||||
const deviceId = device["DeviceID.ID"];
|
||||
const p = new Expression.Parameter(Path.parse("_id"));
|
||||
const exp = Expression.and(
|
||||
new Expression.Binary(">", p, new Expression.Literal(`${deviceId}:`)),
|
||||
new Expression.Binary(
|
||||
"<",
|
||||
p,
|
||||
new Expression.Literal(`${deviceId}:zzzz`),
|
||||
),
|
||||
);
|
||||
const faults = store.fetch("faults", exp);
|
||||
|
||||
const headers = [
|
||||
"Channel",
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import { ClosureComponent, Component } from "mithril";
|
||||
import { m } from "../components.ts";
|
||||
import { evaluateExpression } from "../store.ts";
|
||||
import Expression from "../../lib/common/expression.ts";
|
||||
|
||||
const component: ClosureComponent = (): Component => {
|
||||
return {
|
||||
view: (vnode) => {
|
||||
let deviceId;
|
||||
const device = vnode.attrs["device"];
|
||||
if (device) deviceId = device["DeviceID.ID"].value[0];
|
||||
if (device) deviceId = device["DeviceID.ID"];
|
||||
|
||||
const children = Object.values(vnode.attrs["components"]).map((c) => {
|
||||
if (Array.isArray(c)) c = evaluateExpression(c, device || {});
|
||||
if (c instanceof Expression)
|
||||
c = evaluateExpression(c, device ?? {}).value;
|
||||
if (typeof c !== "object") return `${c}`;
|
||||
const type = evaluateExpression(c["type"], device || {});
|
||||
const type = evaluateExpression(c["type"], device ?? {}).value;
|
||||
if (!type) return null;
|
||||
const attrs = Object.assign({}, vnode.attrs, c);
|
||||
return m(type as string, attrs);
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
import { ClosureComponent, Component } from "mithril";
|
||||
import { m } from "../components.ts";
|
||||
import config from "../config.ts";
|
||||
import { overview } from "../config.ts";
|
||||
import { evaluateExpression } from "../store.ts";
|
||||
|
||||
const CHARTS = config.ui.overview.charts;
|
||||
const CHARTS = overview.charts;
|
||||
|
||||
const component: ClosureComponent = (): Component => {
|
||||
return {
|
||||
view: (vnode) => {
|
||||
const device = vnode.attrs["device"];
|
||||
const chartName = evaluateExpression(vnode.attrs["chart"], device || {});
|
||||
const chart = CHARTS[chartName as string] as Record<string, unknown>;
|
||||
const chartName = evaluateExpression(
|
||||
vnode.attrs["chart"],
|
||||
device ?? {},
|
||||
).value;
|
||||
const chart = CHARTS[chartName as string];
|
||||
if (!chart) return null;
|
||||
for (const slice of Object.values(chart.slices)) {
|
||||
const filter = slice["filter"];
|
||||
if (evaluateExpression(filter, device || {})) {
|
||||
for (const slice of chart.slices) {
|
||||
const filter = slice.filter;
|
||||
if (evaluateExpression(filter, device ?? {}).value) {
|
||||
const dot = m(
|
||||
"svg.inline",
|
||||
{
|
||||
@@ -28,10 +31,10 @@ const component: ClosureComponent = (): Component => {
|
||||
cx: "0.5em",
|
||||
cy: "0.5em",
|
||||
r: "0.4em",
|
||||
fill: evaluateExpression(slice["color"], null),
|
||||
fill: slice.color,
|
||||
}),
|
||||
);
|
||||
return m("span", dot, evaluateExpression(slice["label"], null));
|
||||
return m("span", dot, slice.label);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ClosureComponent, VnodeDOM } from "mithril";
|
||||
import { m } from "../components.ts";
|
||||
import { QueryResponse, evaluateExpression } from "../store.ts";
|
||||
import { FlatDevice } from "../../lib/ui/db.ts";
|
||||
import { Expression } from "../../lib/types.ts";
|
||||
import Expression from "../../lib/common/expression.ts";
|
||||
|
||||
interface Attrs {
|
||||
device: FlatDevice;
|
||||
@@ -19,7 +19,11 @@ const component: ClosureComponent<Attrs> = () => {
|
||||
const device = vnode.attrs.device;
|
||||
|
||||
const rows = Object.values(vnode.attrs.parameters).map((parameter) => {
|
||||
const type = evaluateExpression(parameter.type, device);
|
||||
let type = "parameter";
|
||||
if (parameter.type) {
|
||||
const t = evaluateExpression(parameter.type, device);
|
||||
if (typeof t.value === "string") type = t.value;
|
||||
}
|
||||
const p = m.context(
|
||||
{
|
||||
device: device,
|
||||
@@ -45,7 +49,7 @@ const component: ClosureComponent<Attrs> = () => {
|
||||
},
|
||||
m(
|
||||
"dt.text-sm font-medium text-stone-500",
|
||||
evaluateExpression(parameter["label"], device),
|
||||
evaluateExpression(parameter["label"], device).value,
|
||||
),
|
||||
m("dd.text-sm text-stone-900 col-span-2", p),
|
||||
);
|
||||
|
||||
@@ -2,47 +2,45 @@ import { ClosureComponent } from "mithril";
|
||||
import { m } from "../components.ts";
|
||||
import * as taskQueue from "../task-queue.ts";
|
||||
import { QueryResponse, evaluateExpression } from "../store.ts";
|
||||
import * as expressionParser from "../../lib/common/expression/parser.ts";
|
||||
import { icon } from "../tailwind-utility-components.ts";
|
||||
import { FlatDevice } from "../../lib/ui/db.ts";
|
||||
import { Expression } from "../../lib/types.ts";
|
||||
import Expression from "../../lib/common/expression.ts";
|
||||
import Path from "../../lib/common/path.ts";
|
||||
|
||||
interface Attrs {
|
||||
device: FlatDevice;
|
||||
parameter: Expression;
|
||||
label: Expression;
|
||||
childParameters: Record<string, { label: string; parameter: Expression }>;
|
||||
childParameters: Record<string, { label: Expression; parameter: Expression }>;
|
||||
filter?: Expression;
|
||||
deviceQuery: QueryResponse;
|
||||
}
|
||||
|
||||
const component: ClosureComponent<Attrs> = () => {
|
||||
let object: Path;
|
||||
let parameters: { label: Expression; parameter: Expression }[];
|
||||
|
||||
return {
|
||||
oninit: (vnode) => {
|
||||
const obj = vnode.attrs.parameter;
|
||||
if (!Array.isArray(obj) || obj[0] !== "PARAM")
|
||||
if (!(obj instanceof Expression.Parameter))
|
||||
throw new Error("Object must be a parameter path");
|
||||
vnode.state["object"] = obj[1];
|
||||
vnode.state["parameters"] = Object.values(vnode.attrs.childParameters);
|
||||
object = obj.path;
|
||||
parameters = Object.values(vnode.attrs.childParameters);
|
||||
},
|
||||
view: (vnode) => {
|
||||
const device = vnode.attrs.device;
|
||||
const object = evaluateExpression(vnode.state["object"], device);
|
||||
const parameters = vnode.state["parameters"];
|
||||
|
||||
if (typeof object !== "string" || !device[object]) return null;
|
||||
|
||||
const instances: Set<string> = new Set();
|
||||
const prefix = `${object}.`;
|
||||
const prefix = `${object.toString()}.`;
|
||||
for (const p in device) {
|
||||
if (p.startsWith(prefix)) {
|
||||
const i = p.indexOf(".", prefix.length);
|
||||
if (i === -1) instances.add(p);
|
||||
else instances.add(p.slice(0, i));
|
||||
}
|
||||
if (!p.startsWith(prefix)) continue;
|
||||
if (p.lastIndexOf(":") !== -1) continue;
|
||||
const i = p.indexOf(".", prefix.length);
|
||||
if (i === -1) instances.add(p);
|
||||
else instances.add(p.slice(0, i));
|
||||
}
|
||||
|
||||
const headers = Object.values(parameters).map((p, i) => {
|
||||
const headers = parameters.map((p, i) => {
|
||||
const padding = i ? "px-3" : "pl-6 pr-3";
|
||||
|
||||
return m(
|
||||
@@ -53,7 +51,7 @@ const component: ClosureComponent<Attrs> = () => {
|
||||
"py-3.5 text-left text-sm font-semibold text-stone-500 " +
|
||||
padding,
|
||||
},
|
||||
evaluateExpression(p["label"], device),
|
||||
evaluateExpression(p.label, device).value,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -63,24 +61,33 @@ const component: ClosureComponent<Attrs> = () => {
|
||||
|
||||
const rows = [];
|
||||
for (const i of instances) {
|
||||
let filter = "filter" in vnode.attrs ? vnode.attrs.filter : true;
|
||||
let filter: Expression =
|
||||
"filter" in vnode.attrs
|
||||
? vnode.attrs.filter
|
||||
: new Expression.Literal(true);
|
||||
|
||||
filter = expressionParser.map(filter, (e) => {
|
||||
if (Array.isArray(e) && e[0] === "PARAM")
|
||||
return ["PARAM", ["||", i, ".", e[1]]];
|
||||
const root = Path.parse(i);
|
||||
filter = filter.evaluate((e) => {
|
||||
if (e instanceof Expression.Parameter)
|
||||
return new Expression.Parameter(root.concat(e.path));
|
||||
return e;
|
||||
});
|
||||
|
||||
if (!evaluateExpression(filter, device)) continue;
|
||||
if (!evaluateExpression(filter, device).value) continue;
|
||||
|
||||
const row = parameters.map((p, j) => {
|
||||
const padding = j ? "px-3" : "pl-6 pr-3";
|
||||
|
||||
const param = expressionParser.map(p.parameter, (e) => {
|
||||
if (Array.isArray(e) && e[0] === "PARAM")
|
||||
return ["PARAM", ["||", i, ".", e[1]]];
|
||||
const param = p.parameter.evaluate((e) => {
|
||||
if (e instanceof Expression.Parameter)
|
||||
return new Expression.Parameter(root.concat(e.path));
|
||||
return e;
|
||||
});
|
||||
|
||||
let type = "parameter";
|
||||
if (p["type"] instanceof Expression)
|
||||
type = evaluateExpression(p["type"], device).value + "";
|
||||
|
||||
return m(
|
||||
"td",
|
||||
{
|
||||
@@ -91,7 +98,7 @@ const component: ClosureComponent<Attrs> = () => {
|
||||
device: device,
|
||||
parameter: param,
|
||||
},
|
||||
p.type || "parameter",
|
||||
type,
|
||||
Object.assign({}, p, {
|
||||
device: device,
|
||||
parameter: param,
|
||||
@@ -101,7 +108,7 @@ const component: ClosureComponent<Attrs> = () => {
|
||||
);
|
||||
});
|
||||
|
||||
if (device[i].writable === true) {
|
||||
if (device[i + ":writable"]) {
|
||||
row.push(
|
||||
m(
|
||||
"td",
|
||||
@@ -116,7 +123,7 @@ const component: ClosureComponent<Attrs> = () => {
|
||||
onclick: () => {
|
||||
taskQueue.queueTask({
|
||||
name: "deleteObject",
|
||||
device: device["DeviceID.ID"].value[0] as string,
|
||||
device: device["DeviceID.ID"] as string,
|
||||
objectName: i,
|
||||
});
|
||||
},
|
||||
@@ -147,7 +154,7 @@ const component: ClosureComponent<Attrs> = () => {
|
||||
);
|
||||
}
|
||||
|
||||
if (device[object].writable === true) {
|
||||
if (device[object.toString() + ":writable"]) {
|
||||
rows.push(
|
||||
m(
|
||||
"tr",
|
||||
@@ -161,8 +168,8 @@ const component: ClosureComponent<Attrs> = () => {
|
||||
onclick: () => {
|
||||
taskQueue.queueTask({
|
||||
name: "addObject",
|
||||
device: device["DeviceID.ID"].value[0] as string,
|
||||
objectName: object,
|
||||
device: device["DeviceID.ID"] as string,
|
||||
objectName: object.toString(),
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
+42
-48
@@ -2,54 +2,52 @@ import { ClosureComponent, Component, VnodeDOM } from "mithril";
|
||||
import { m } from "../components.ts";
|
||||
import * as taskQueue from "../task-queue.ts";
|
||||
import * as store from "../store.ts";
|
||||
import { evaluate } from "../../lib/common/expression/util.ts";
|
||||
import Expression, { Value } from "../../lib/common/expression.ts";
|
||||
import memoize from "../../lib/common/memoize.ts";
|
||||
import timeAgo from "../timeago.ts";
|
||||
import { icon } from "../tailwind-utility-components.ts";
|
||||
|
||||
const evaluateParam = memoize((exp, obj, now: number) => {
|
||||
let timestamp = now;
|
||||
exp = evaluate(exp, null, now, (e) => {
|
||||
if (!Array.isArray(e)) return e;
|
||||
for (let i = 1; i < e.length; ++i) {
|
||||
if (
|
||||
Array.isArray(e[i]) &&
|
||||
e[i][0] === "PARAM" &&
|
||||
!Array.isArray(e[i][1])
|
||||
) {
|
||||
let v = null;
|
||||
const p = obj[e[i][1]];
|
||||
if (p?.value) {
|
||||
v = p.value[0];
|
||||
timestamp = Math.min(timestamp, p.valueTimestamp);
|
||||
const evaluateParam = memoize(
|
||||
(
|
||||
exp: Expression,
|
||||
obj: any,
|
||||
now: number,
|
||||
): { value: Value; timestamp: number; parameter: string } => {
|
||||
let timestamp = now;
|
||||
const valueMap: Map<Expression.Literal, string> = new Map();
|
||||
const lit = exp.evaluate((e): Expression.Literal => {
|
||||
if (e instanceof Expression.Literal) return e;
|
||||
if (e instanceof Expression.Parameter) {
|
||||
let v = obj[e.path.toString()];
|
||||
if (v) {
|
||||
timestamp = Math.min(
|
||||
timestamp,
|
||||
obj[e.path.toString() + ":valueTimestamp"] ?? 0,
|
||||
);
|
||||
const t = obj[e.path.toString() + ":type"];
|
||||
if (t === "xsd:dateTime" && typeof v === "number")
|
||||
v = new Date(v).toLocaleString();
|
||||
const val = new Expression.Literal(v);
|
||||
valueMap.set(val, e.path.toString());
|
||||
return val;
|
||||
}
|
||||
} else if (e instanceof Expression.FunctionCall) {
|
||||
if (e.name === "NOW") return new Expression.Literal(now);
|
||||
else if (e.name === "DATE_STRING") {
|
||||
const v = e.args[0];
|
||||
if (v instanceof Expression.Literal) {
|
||||
return new Expression.Literal(
|
||||
new Date(v.value as string | number).toLocaleString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
e = e.slice();
|
||||
e[i] = v;
|
||||
}
|
||||
}
|
||||
if (e[0] === "FUNC" && e[1] === "DATE_STRING" && !Array.isArray(e[2]))
|
||||
return new Date(e[2]).toLocaleString();
|
||||
return e;
|
||||
});
|
||||
return new Expression.Literal(null);
|
||||
});
|
||||
|
||||
let parameter = null;
|
||||
let value = null;
|
||||
|
||||
if (!Array.isArray(exp)) {
|
||||
value = exp;
|
||||
} else if (exp[0] === "PARAM") {
|
||||
const p = obj[exp[1]];
|
||||
if (p?.value) {
|
||||
timestamp = p.valueTimestamp;
|
||||
value = p.value[0];
|
||||
parameter = exp[1];
|
||||
if (p.value[1] === "xsd:dateTime" && typeof value === "number")
|
||||
value = new Date(value).toLocaleString();
|
||||
}
|
||||
}
|
||||
|
||||
return { value, timestamp, parameter };
|
||||
});
|
||||
return { value: lit.value, timestamp, parameter: valueMap.get(lit) };
|
||||
},
|
||||
);
|
||||
|
||||
const component: ClosureComponent = (): Component => {
|
||||
return {
|
||||
@@ -65,7 +63,7 @@ const component: ClosureComponent = (): Component => {
|
||||
if (value == null) return null;
|
||||
|
||||
let edit;
|
||||
if (device[parameter]?.writable) {
|
||||
if (device[parameter + ":writable"]) {
|
||||
edit = m(
|
||||
"button",
|
||||
{
|
||||
@@ -73,13 +71,9 @@ const component: ClosureComponent = (): Component => {
|
||||
onclick: () => {
|
||||
taskQueue.stageSpv({
|
||||
name: "setParameterValues",
|
||||
devices: [device["DeviceID.ID"].value[0]],
|
||||
devices: [device["DeviceID.ID"]],
|
||||
parameterValues: [
|
||||
[
|
||||
parameter,
|
||||
device[parameter].value[0],
|
||||
device[parameter].value[1],
|
||||
],
|
||||
[parameter, device[parameter], device[parameter + ":type"]],
|
||||
],
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
import { ClosureComponent, Component } from "mithril";
|
||||
import { ClosureComponent } from "mithril";
|
||||
import { m } from "../components.ts";
|
||||
import * as taskQueue from "../task-queue.ts";
|
||||
import * as store from "../store.ts";
|
||||
import * as notifications from "../notifications.ts";
|
||||
import Expression from "../../lib/common/expression.ts";
|
||||
import { FlatDevice } from "../../lib/ui/db.ts";
|
||||
|
||||
const component: ClosureComponent = (): Component => {
|
||||
interface Attrs {
|
||||
device: FlatDevice;
|
||||
parameters: Record<string, Expression>;
|
||||
}
|
||||
|
||||
const component: ClosureComponent<Attrs> = () => {
|
||||
return {
|
||||
view: (vnode) => {
|
||||
const device = vnode.attrs["device"];
|
||||
const device = vnode.attrs.device;
|
||||
|
||||
return m(
|
||||
"button",
|
||||
@@ -19,15 +26,16 @@ const component: ClosureComponent = (): Component => {
|
||||
e.target.disabled = true;
|
||||
const params = Object.values(vnode.attrs["parameters"])
|
||||
.map((exp) => {
|
||||
if (!Array.isArray(exp) || exp[0] !== "PARAM") return null;
|
||||
return store.evaluateExpression(exp[1], device) as string;
|
||||
if (exp instanceof Expression.Parameter)
|
||||
return exp.path.toString();
|
||||
return null;
|
||||
})
|
||||
.filter((exp) => !!exp);
|
||||
|
||||
const task = {
|
||||
name: "getParameterValues",
|
||||
parameterNames: params,
|
||||
device: device["DeviceID.ID"].value[0],
|
||||
device: device["DeviceID.ID"] as string,
|
||||
};
|
||||
|
||||
taskQueue
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as notifications from "../notifications.ts";
|
||||
import * as store from "../store.ts";
|
||||
import { icon } from "../tailwind-utility-components.ts";
|
||||
import { decodeTag } from "../../lib/util.ts";
|
||||
import { Expression } from "../../lib/types.ts";
|
||||
import Expression from "../../lib/common/expression.ts";
|
||||
import { FlatDevice } from "../../lib/ui/db.ts";
|
||||
|
||||
interface Attrs {
|
||||
@@ -22,7 +22,8 @@ const component: ClosureComponent<Attrs> = () => {
|
||||
|
||||
const tags = [];
|
||||
for (const p of Object.keys(device))
|
||||
if (p.startsWith("Tags.")) tags.push(decodeTag(p.slice(5)));
|
||||
if (p.startsWith("Tags.") && p.lastIndexOf(":") === -1)
|
||||
tags.push(decodeTag(p.slice(5)));
|
||||
|
||||
tags.sort();
|
||||
|
||||
@@ -60,7 +61,7 @@ const component: ClosureComponent<Attrs> = () => {
|
||||
"flex-shrink-0 ml-0.5 h-4 w-4 rounded-full inline-flex items-center justify-center text-yellow-400 hover:bg-yellow-200 hover:text-yellow-500 focus:outline-hidden focus:bg-yellow-500 focus:text-white",
|
||||
onclick: (e) => {
|
||||
e.target.disabled = true;
|
||||
const deviceId = device["DeviceID.ID"].value[0] as string;
|
||||
const deviceId = device["DeviceID.ID"] as string;
|
||||
store
|
||||
.updateTags(deviceId, { [tag]: false })
|
||||
.then(() => {
|
||||
@@ -103,7 +104,7 @@ const component: ClosureComponent<Attrs> = () => {
|
||||
"flex-shrink-0 h-4 w-4 rounded-full inline-flex items-center justify-center text-yellow-400 hover:bg-yellow-200 hover:text-yellow-500 focus:outline-hidden focus:bg-yellow-500 focus:text-white",
|
||||
onclick: (e) => {
|
||||
e.target.disabled = true;
|
||||
const deviceId = device["DeviceID.ID"].value[0] as string;
|
||||
const deviceId = device["DeviceID.ID"] as string;
|
||||
const tag = prompt(`Enter tag to assign to device:`);
|
||||
if (!tag) {
|
||||
e.target.disabled = false;
|
||||
|
||||
+3
-3
@@ -5,7 +5,7 @@ import * as notifications from "./notifications.ts";
|
||||
import putFormComponent from "./put-form-component.ts";
|
||||
import uiConfigComponent from "./ui-config-component.ts";
|
||||
import * as overlay from "./overlay.ts";
|
||||
import { parse, stringify } from "../lib/common/expression/parser.ts";
|
||||
import Expression from "../lib/common/expression.ts";
|
||||
import { loadCodeMirror, loadYaml } from "./dynamic-loader.ts";
|
||||
import { icon } from "./tailwind-utility-components.ts";
|
||||
|
||||
@@ -30,7 +30,7 @@ function putActionHandler(action, _object, isNew?): Promise<ValidationErrors> {
|
||||
if (!id.match(regex)) return void resolve({ _id: "Invalid ID" });
|
||||
|
||||
try {
|
||||
object.value = stringify(parse(object.value || ""));
|
||||
object.value = Expression.parse(object.value || "").toString();
|
||||
} catch {
|
||||
return void resolve({
|
||||
value: "Config value must be valid expression",
|
||||
@@ -232,7 +232,7 @@ export const component: ClosureComponent = (): Component => {
|
||||
},
|
||||
);
|
||||
|
||||
const confs = store.fetch("config", true);
|
||||
const confs = store.fetch("config", new Expression.Literal(true));
|
||||
|
||||
let newConfig;
|
||||
const subs = [];
|
||||
|
||||
+111
-1
@@ -1,3 +1,113 @@
|
||||
export default window.clientConfig;
|
||||
import Expression from "../lib/common/expression.ts";
|
||||
export const configSnapshot = window.configSnapshot;
|
||||
export const genieacsVersion = window.genieacsVersion;
|
||||
|
||||
type Filters = { label: string; parameter: Expression; type: string }[];
|
||||
type pageSize = number;
|
||||
type overview = {
|
||||
charts: {
|
||||
[name: string]: {
|
||||
label: string;
|
||||
slices: {
|
||||
label: string;
|
||||
filter: Expression;
|
||||
color: string;
|
||||
}[];
|
||||
};
|
||||
};
|
||||
groups: {
|
||||
label: string;
|
||||
charts: string[];
|
||||
}[];
|
||||
};
|
||||
|
||||
type Index = {
|
||||
label: string;
|
||||
type?: string;
|
||||
parameter: Expression;
|
||||
unsortable: boolean;
|
||||
raw: NestedRecord;
|
||||
}[];
|
||||
|
||||
type NestedRecord = { [k: string]: Expression | NestedRecord };
|
||||
|
||||
const conf: NestedRecord = {};
|
||||
for (const [key, value] of Object.entries(window.clientConfig)) {
|
||||
const exp = Expression.parse(value).evaluate((e) => e);
|
||||
let ref = conf;
|
||||
const keyParts = key.split(".");
|
||||
while (keyParts.length > 1) {
|
||||
const k = keyParts.shift();
|
||||
if (ref[k] == null || typeof ref[k] !== "object") ref[k] = {};
|
||||
ref = ref[k] as NestedRecord;
|
||||
}
|
||||
ref[keyParts[0]] = exp;
|
||||
}
|
||||
|
||||
export const filters: Filters = [];
|
||||
export let pageSize: number = 10;
|
||||
export const overview: overview = { charts: {}, groups: [] };
|
||||
export const index: Index = [];
|
||||
export let device: NestedRecord = {};
|
||||
|
||||
for (const obj of Object.values(conf["filters"] || {})) {
|
||||
let label = "";
|
||||
let parameter: Expression = new Expression.Literal(false);
|
||||
let type = "string";
|
||||
if (obj["label"] instanceof Expression.Literal)
|
||||
label = obj["label"].value as string;
|
||||
if (obj["parameter"] instanceof Expression) parameter = obj["parameter"];
|
||||
if (obj["type"] instanceof Expression.Literal)
|
||||
type = obj["type"].value as string;
|
||||
filters.push({ label, parameter, type });
|
||||
}
|
||||
|
||||
for (const obj of Object.values(conf["index"] || {})) {
|
||||
let label = "";
|
||||
let parameter: Expression = new Expression.Literal(null);
|
||||
let unsortable = false;
|
||||
let type = "";
|
||||
if (obj["label"] instanceof Expression.Literal)
|
||||
label = obj["label"].value as string;
|
||||
if (obj["type"] instanceof Expression.Literal)
|
||||
type = obj["type"].value as string;
|
||||
if (obj["parameter"] instanceof Expression) parameter = obj["parameter"];
|
||||
if (obj["unsortable"] instanceof Expression.Literal)
|
||||
unsortable = obj["unsortable"].value as boolean;
|
||||
index.push({ label, type, parameter, unsortable, raw: obj });
|
||||
}
|
||||
|
||||
for (const obj of Object.values(conf["overview"]?.["groups"] || {})) {
|
||||
let label = "";
|
||||
const charts: string[] = [];
|
||||
if (obj["label"] instanceof Expression.Literal)
|
||||
label = obj["label"].value as string;
|
||||
for (const chart of Object.values(obj["charts"] || {})) {
|
||||
if (chart instanceof Expression.Literal) charts.push(chart.value as string);
|
||||
}
|
||||
overview.groups.push({ label, charts });
|
||||
}
|
||||
|
||||
for (const [name, obj] of Object.entries(conf["overview"]?.["charts"] || {})) {
|
||||
const slices: { label: string; filter: Expression; color: string }[] = [];
|
||||
for (const slice of Object.values(obj["slices"] || {})) {
|
||||
let label = "";
|
||||
let filter: Expression = new Expression.Literal(false);
|
||||
let color = "";
|
||||
if (slice["label"] instanceof Expression.Literal)
|
||||
label = slice["label"].value as string;
|
||||
if (slice["filter"] instanceof Expression) filter = slice["filter"];
|
||||
if (slice["color"] instanceof Expression.Literal)
|
||||
color = slice["color"].value as string;
|
||||
slices.push({ label, filter, color });
|
||||
}
|
||||
let label = "";
|
||||
if (obj["label"] instanceof Expression.Literal)
|
||||
label = obj["label"].value as string;
|
||||
overview.charts[name] = { label, slices };
|
||||
}
|
||||
|
||||
if (conf["pageSize"] instanceof Expression.Literal)
|
||||
pageSize = +conf["pageSize"].value || 10;
|
||||
|
||||
device = conf["device"] as NestedRecord;
|
||||
+20
-9
@@ -1,7 +1,9 @@
|
||||
import { ClosureComponent, Component } from "mithril";
|
||||
import { ClosureComponent } from "mithril";
|
||||
import { m } from "./components.ts";
|
||||
import config from "./config.ts";
|
||||
import { device as deviceConfig } from "./config.ts";
|
||||
import * as store from "./store.ts";
|
||||
import Expression from "../lib/common/expression.ts";
|
||||
import Path from "../lib/common/path.ts";
|
||||
|
||||
export function init(
|
||||
args: Record<string, unknown>,
|
||||
@@ -14,16 +16,25 @@ export function init(
|
||||
|
||||
return Promise.resolve({
|
||||
deviceId: args.id,
|
||||
deviceFilter: ["=", ["PARAM", "DeviceID.ID"], args.id],
|
||||
deviceFilter: new Expression.Binary(
|
||||
"=",
|
||||
new Expression.Parameter(Path.parse("DeviceID.ID")),
|
||||
new Expression.Literal(args.id as string),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
export const component: ClosureComponent = (): Component => {
|
||||
interface Attrs {
|
||||
deviceId: string;
|
||||
deviceFilter: Expression;
|
||||
}
|
||||
|
||||
export const component: ClosureComponent<Attrs> = () => {
|
||||
return {
|
||||
view: (vnode) => {
|
||||
document.title = `${vnode.attrs["deviceId"]} - Devices - GenieACS`;
|
||||
document.title = `${vnode.attrs.deviceId} - Devices - GenieACS`;
|
||||
|
||||
const dev = store.fetch("devices", vnode.attrs["deviceFilter"]);
|
||||
const dev = store.fetch("devices", vnode.attrs.deviceFilter);
|
||||
if (!dev.value.length) {
|
||||
if (!dev.fulfilling) {
|
||||
return m(
|
||||
@@ -38,15 +49,15 @@ export const component: ClosureComponent = (): Component => {
|
||||
);
|
||||
}
|
||||
|
||||
const conf = config.ui.device;
|
||||
const conf = deviceConfig;
|
||||
const cmps = [];
|
||||
|
||||
for (const c of Object.values(conf)) {
|
||||
cmps.push(
|
||||
m.context(
|
||||
{ device: dev.value[0], deviceQuery: dev },
|
||||
store.evaluateExpression(c["type"], null) as string,
|
||||
c,
|
||||
store.evaluateExpression(c["type"], {}).value as string,
|
||||
c as any,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
+68
-56
@@ -1,63 +1,72 @@
|
||||
import { ClosureComponent, Component, Children } from "mithril";
|
||||
import { ClosureComponent, Children } from "mithril";
|
||||
import { m } from "./components.ts";
|
||||
import config from "./config.ts";
|
||||
import { pageSize as PAGE_SIZE, index as indexConfig } from "./config.ts";
|
||||
import indexTableComponent from "./index-table-component.ts";
|
||||
import filterComponent from "./filter-component.ts";
|
||||
import * as store from "./store.ts";
|
||||
import { queueTask, stageDownload } from "./task-queue.ts";
|
||||
import * as notifications from "./notifications.ts";
|
||||
import { parse, stringify, map } from "../lib/common/expression/parser.ts";
|
||||
import { evaluate, extractParams } from "../lib/common/expression/util.ts";
|
||||
import Expression, { extractPaths } from "../lib/common/expression.ts";
|
||||
import memoize from "../lib/common/memoize.ts";
|
||||
import * as smartQuery from "./smart-query.ts";
|
||||
|
||||
const PAGE_SIZE = config.ui.pageSize || 10;
|
||||
|
||||
const memoizedParse = memoize(parse);
|
||||
const memoizedJsonParse = memoize(JSON.parse);
|
||||
const memoizedGetSortable = memoize((p) => {
|
||||
const expressionParams = extractParams(p);
|
||||
if (expressionParams.length === 1) {
|
||||
const param = evaluate(expressionParams[0]);
|
||||
if (typeof param === "string") return param;
|
||||
}
|
||||
const memoizedGetSortable = memoize((p: Expression) => {
|
||||
const expressionParams = extractPaths(p);
|
||||
if (expressionParams.length === 1) return expressionParams[0];
|
||||
return null;
|
||||
});
|
||||
|
||||
const getDownloadUrl = memoize((filter, indexParameters) => {
|
||||
const columns = {};
|
||||
for (const p of indexParameters)
|
||||
columns[store.evaluateExpression(p.label, null) as string] = stringify(
|
||||
p.parameter,
|
||||
);
|
||||
return `api/devices.csv?${m.buildQueryString({
|
||||
filter: stringify(filter),
|
||||
columns: JSON.stringify(columns),
|
||||
})}`;
|
||||
});
|
||||
const getDownloadUrl = memoize(
|
||||
(
|
||||
filter: Expression,
|
||||
indexParameters: { label: string; parameter: Expression }[],
|
||||
) => {
|
||||
const columns = {};
|
||||
for (const p of indexParameters) columns[p.label] = p.parameter.toString();
|
||||
return `api/devices.csv?${m.buildQueryString({
|
||||
filter: filter.toString(),
|
||||
columns: JSON.stringify(columns),
|
||||
})}`;
|
||||
},
|
||||
);
|
||||
|
||||
const unpackSmartQuery = memoize((query) => {
|
||||
return map(query, (e) => {
|
||||
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "Q")
|
||||
return smartQuery.unpack("devices", e[2], e[3]);
|
||||
const unpackSmartQuery = memoize((query: Expression) => {
|
||||
return query.evaluate((e) => {
|
||||
if (e instanceof Expression.FunctionCall) {
|
||||
if (e.name === "Q") {
|
||||
if (
|
||||
e.args[0] instanceof Expression.Literal &&
|
||||
e.args[1] instanceof Expression.Literal
|
||||
) {
|
||||
return smartQuery.unpack(
|
||||
"devices",
|
||||
e.args[0].value as string,
|
||||
e.args[1].value as string,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return e;
|
||||
});
|
||||
});
|
||||
|
||||
export function init(
|
||||
args: Record<string, unknown>,
|
||||
): Promise<Record<string, unknown>> {
|
||||
export function init(args: Record<string, unknown>): Promise<Attrs> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!window.authorizer.hasAccess("devices", 2))
|
||||
return void reject(new Error("You are not authorized to view this page"));
|
||||
|
||||
const filter = args.hasOwnProperty("filter") ? "" + args["filter"] : "";
|
||||
const sort = args.hasOwnProperty("sort") ? "" + args["sort"] : "";
|
||||
const indexParameters = Object.values(config.ui.index);
|
||||
let filter: Expression = null;
|
||||
let sort: Record<string, number> = null;
|
||||
if (args.hasOwnProperty("filter"))
|
||||
filter = Expression.parse(args["filter"] as string);
|
||||
if (args.hasOwnProperty("sort")) sort = JSON.parse(args["sort"] as string);
|
||||
const indexParameters = indexConfig;
|
||||
if (!indexParameters.length) {
|
||||
indexParameters.push({
|
||||
label: "ID",
|
||||
parameter: ["PARAM", "DeviceID.ID"],
|
||||
parameter: Expression.parse("DeviceID.ID"),
|
||||
unsortable: false,
|
||||
raw: {},
|
||||
});
|
||||
}
|
||||
resolve({ filter, indexParameters, sort });
|
||||
@@ -220,11 +229,17 @@ function renderActions(selected: Set<string>): Children {
|
||||
return buttons;
|
||||
}
|
||||
|
||||
export const component: ClosureComponent = (): Component => {
|
||||
interface Attrs {
|
||||
indexParameters: typeof indexConfig;
|
||||
filter?: Expression;
|
||||
sort?: Record<string, number>;
|
||||
}
|
||||
|
||||
export const component: ClosureComponent<Attrs> = () => {
|
||||
return {
|
||||
view: (vnode) => {
|
||||
document.title = "Devices - GenieACS";
|
||||
const attributes = vnode.attrs["indexParameters"];
|
||||
const attributes = vnode.attrs.indexParameters;
|
||||
|
||||
function showMore(): void {
|
||||
vnode.state["showCount"] =
|
||||
@@ -232,22 +247,22 @@ export const component: ClosureComponent = (): Component => {
|
||||
m.redraw();
|
||||
}
|
||||
|
||||
function onFilterChanged(filter): void {
|
||||
const ops = { filter };
|
||||
if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"];
|
||||
function onFilterChanged(filter: Expression): void {
|
||||
const ops = {};
|
||||
if (!(filter instanceof Expression.Literal && filter.value))
|
||||
ops["filter"] = filter.toString();
|
||||
if (vnode.attrs.sort) ops["sort"] = vnode.attrs.sort;
|
||||
m.route.set("/devices", ops);
|
||||
}
|
||||
|
||||
const sort = vnode.attrs["sort"]
|
||||
? memoizedJsonParse(vnode.attrs["sort"])
|
||||
: {};
|
||||
const sort = vnode.attrs.sort || {};
|
||||
|
||||
const sortAttributes = {};
|
||||
for (let i = 0; i < attributes.length; i++) {
|
||||
const attr = attributes[i];
|
||||
if (attr.unsortable) continue;
|
||||
const param = memoizedGetSortable(attr.parameter);
|
||||
if (param) sortAttributes[i] = sort[param] || 0;
|
||||
if (param) sortAttributes[i] = sort[param.toString()] || 0;
|
||||
}
|
||||
|
||||
function onSortChange(sortedAttrs): void {
|
||||
@@ -256,17 +271,16 @@ export const component: ClosureComponent = (): Component => {
|
||||
const param = memoizedGetSortable(
|
||||
attributes[Math.abs(index) - 1].parameter,
|
||||
);
|
||||
_sort[param] = Math.sign(index);
|
||||
_sort[param.toString()] = Math.sign(index);
|
||||
}
|
||||
const ops = { sort: JSON.stringify(_sort) };
|
||||
if (vnode.attrs["filter"]) ops["filter"] = vnode.attrs["filter"];
|
||||
m.route.set("/devices", ops);
|
||||
}
|
||||
|
||||
let filter = vnode.attrs["filter"]
|
||||
? memoizedParse(vnode.attrs["filter"])
|
||||
: true;
|
||||
filter = unpackSmartQuery(filter);
|
||||
const filter = unpackSmartQuery(
|
||||
vnode.attrs["filter"] ?? new Expression.Literal(true),
|
||||
);
|
||||
|
||||
const devs = store.fetch("devices", filter, {
|
||||
limit: vnode.state["showCount"] || PAGE_SIZE,
|
||||
@@ -280,15 +294,15 @@ export const component: ClosureComponent = (): Component => {
|
||||
return m.context(
|
||||
{ device: device, parameter: attr.parameter },
|
||||
attr.type || "parameter",
|
||||
attr,
|
||||
attr.raw,
|
||||
);
|
||||
};
|
||||
|
||||
const attrs = {};
|
||||
attrs["attributes"] = attributes.map((a) => ({
|
||||
...a,
|
||||
label: store.evaluateExpression(a.label, null),
|
||||
type: store.evaluateExpression(a.type, null),
|
||||
label: a.label,
|
||||
type: a.type,
|
||||
}));
|
||||
attrs["data"] = devs.value;
|
||||
attrs["total"] = count.value;
|
||||
@@ -301,9 +315,7 @@ export const component: ClosureComponent = (): Component => {
|
||||
return m(
|
||||
"a.text-cyan-700 hover:text-cyan-900",
|
||||
{
|
||||
href: `#!/devices/${encodeURIComponent(
|
||||
device["DeviceID.ID"].value[0],
|
||||
)}`,
|
||||
href: `#!/devices/${encodeURIComponent(device["DeviceID.ID"])}`,
|
||||
},
|
||||
"Show",
|
||||
);
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
queueTask,
|
||||
StageTask,
|
||||
} from "./task-queue.ts";
|
||||
import Expression from "../lib/common/expression.ts";
|
||||
|
||||
const invalid: WeakSet<StageTask> = new WeakSet();
|
||||
|
||||
@@ -95,7 +96,7 @@ function renderStagingSpv(task: StageTask, queueFunc, cancelFunc): Children {
|
||||
function renderStagingDownload(task: StageTask): Children {
|
||||
if (!task.fileName || !task.fileType) invalid.add(task);
|
||||
else invalid.delete(task);
|
||||
const files = store.fetch("files", true);
|
||||
const files = store.fetch("files", new Expression.Literal(true));
|
||||
let oui = "";
|
||||
let productClass = "";
|
||||
for (const d of task.devices) {
|
||||
|
||||
+30
-18
@@ -1,18 +1,15 @@
|
||||
import { ClosureComponent, Component, Children } from "mithril";
|
||||
import { m } from "./components.ts";
|
||||
import config from "./config.ts";
|
||||
import { pageSize as PAGE_SIZE } from "./config.ts";
|
||||
import indexTableComponent from "./index-table-component.ts";
|
||||
import filterComponent from "./filter-component.ts";
|
||||
import * as store from "./store.ts";
|
||||
import * as notifications from "./notifications.ts";
|
||||
import memoize from "../lib/common/memoize.ts";
|
||||
import * as smartQuery from "./smart-query.ts";
|
||||
import { map, parse, stringify } from "../lib/common/expression/parser.ts";
|
||||
import { stringify as yamlStringify } from "../lib/common/yaml.ts";
|
||||
import Expression from "../lib/common/expression.ts";
|
||||
|
||||
const PAGE_SIZE = config.ui.pageSize || 10;
|
||||
|
||||
const memoizedParse = memoize(parse);
|
||||
const memoizedJsonParse = memoize(JSON.parse);
|
||||
|
||||
const attributes = [
|
||||
@@ -33,15 +30,27 @@ const getDownloadUrl = memoize((filter) => {
|
||||
}
|
||||
|
||||
return `api/faults.csv?${m.buildQueryString({
|
||||
filter: stringify(filter),
|
||||
filter: filter.toString(),
|
||||
columns: JSON.stringify(cols),
|
||||
})}`;
|
||||
});
|
||||
|
||||
const unpackSmartQuery = memoize((query) => {
|
||||
return map(query, (e) => {
|
||||
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "Q")
|
||||
return smartQuery.unpack("faults", e[2], e[3]);
|
||||
const unpackSmartQuery = memoize((query: Expression) => {
|
||||
return query.evaluate((e) => {
|
||||
if (e instanceof Expression.FunctionCall) {
|
||||
if (e.name === "Q") {
|
||||
if (
|
||||
e.args[0] instanceof Expression.Literal &&
|
||||
e.args[1] instanceof Expression.Literal
|
||||
) {
|
||||
return smartQuery.unpack(
|
||||
"faults",
|
||||
e.args[0].value as string,
|
||||
e.args[1].value as string,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return e;
|
||||
});
|
||||
});
|
||||
@@ -66,9 +75,11 @@ export function init(
|
||||
new Error("You are not authorized to view this page"),
|
||||
);
|
||||
}
|
||||
|
||||
const sort = args.hasOwnProperty("sort") ? "" + args["sort"] : "";
|
||||
const filter = args.hasOwnProperty("filter") ? "" + args["filter"] : "";
|
||||
let filter: Expression = null;
|
||||
let sort: Record<string, number> = null;
|
||||
if (args.hasOwnProperty("filter"))
|
||||
filter = Expression.parse(args["filter"] as string);
|
||||
if (args.hasOwnProperty("sort")) sort = JSON.parse(args["sort"] as string);
|
||||
return Promise.resolve({ filter, sort });
|
||||
}
|
||||
|
||||
@@ -84,7 +95,9 @@ export const component: ClosureComponent = (): Component => {
|
||||
}
|
||||
|
||||
function onFilterChanged(filter): void {
|
||||
const ops = { filter };
|
||||
const ops = {};
|
||||
if (!(filter instanceof Expression.Literal && filter.value))
|
||||
ops["filter"] = filter.toString();
|
||||
if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"];
|
||||
m.route.set("/faults", ops);
|
||||
}
|
||||
@@ -108,10 +121,9 @@ export const component: ClosureComponent = (): Component => {
|
||||
m.route.set("/faults", ops);
|
||||
}
|
||||
|
||||
let filter = vnode.attrs["filter"]
|
||||
? memoizedParse(vnode.attrs["filter"])
|
||||
: true;
|
||||
filter = unpackSmartQuery(filter);
|
||||
const filter = unpackSmartQuery(
|
||||
vnode.attrs["filter"] ?? new Expression.Literal(true),
|
||||
);
|
||||
|
||||
const faults = store.fetch("faults", filter, {
|
||||
limit: vnode.state["showCount"] || PAGE_SIZE,
|
||||
|
||||
+30
-17
@@ -1,6 +1,6 @@
|
||||
import { ClosureComponent, Component, Children } from "mithril";
|
||||
import { m } from "./components.ts";
|
||||
import config from "./config.ts";
|
||||
import { pageSize as PAGE_SIZE } from "./config.ts";
|
||||
import filterComponent from "./filter-component.ts";
|
||||
import * as store from "./store.ts";
|
||||
import * as notifications from "./notifications.ts";
|
||||
@@ -9,11 +9,8 @@ import putFormComponent from "./put-form-component.ts";
|
||||
import indexTableComponent from "./index-table-component.ts";
|
||||
import * as overlay from "./overlay.ts";
|
||||
import * as smartQuery from "./smart-query.ts";
|
||||
import { map, parse, stringify } from "../lib/common/expression/parser.ts";
|
||||
import Expression from "../lib/common/expression.ts";
|
||||
|
||||
const PAGE_SIZE = config.ui.pageSize || 10;
|
||||
|
||||
const memoizedParse = memoize(parse);
|
||||
const memoizedJsonParse = memoize(JSON.parse);
|
||||
|
||||
const attributes: {
|
||||
@@ -46,10 +43,22 @@ const formData = {
|
||||
.concat([{ id: "file", label: "File", type: "file" }]),
|
||||
};
|
||||
|
||||
const unpackSmartQuery = memoize((query) => {
|
||||
return map(query, (e) => {
|
||||
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "Q")
|
||||
return smartQuery.unpack("files", e[2], e[3]);
|
||||
const unpackSmartQuery = memoize((query: Expression) => {
|
||||
return query.evaluate((e) => {
|
||||
if (e instanceof Expression.FunctionCall) {
|
||||
if (e.name === "Q") {
|
||||
if (
|
||||
e.args[0] instanceof Expression.Literal &&
|
||||
e.args[1] instanceof Expression.Literal
|
||||
) {
|
||||
return smartQuery.unpack(
|
||||
"files",
|
||||
e.args[0].value as string,
|
||||
e.args[1].value as string,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return e;
|
||||
});
|
||||
});
|
||||
@@ -87,7 +96,7 @@ const getDownloadUrl = memoize((filter) => {
|
||||
const cols = {};
|
||||
for (const attr of attributes) cols[attr.label] = attr.id;
|
||||
return `api/files.csv?${m.buildQueryString({
|
||||
filter: stringify(filter),
|
||||
filter: filter.toString(),
|
||||
columns: JSON.stringify(cols),
|
||||
})}`;
|
||||
});
|
||||
@@ -101,8 +110,11 @@ export function init(
|
||||
);
|
||||
}
|
||||
|
||||
const sort = args.hasOwnProperty("sort") ? "" + args["sort"] : "";
|
||||
const filter = args.hasOwnProperty("filter") ? "" + args["filter"] : "";
|
||||
let filter: Expression = null;
|
||||
let sort: Record<string, number> = null;
|
||||
if (args.hasOwnProperty("filter"))
|
||||
filter = Expression.parse(args["filter"] as string);
|
||||
if (args.hasOwnProperty("sort")) sort = JSON.parse(args["sort"] as string);
|
||||
return Promise.resolve({ filter, sort });
|
||||
}
|
||||
|
||||
@@ -118,7 +130,9 @@ export const component: ClosureComponent = (): Component => {
|
||||
}
|
||||
|
||||
function onFilterChanged(filter): void {
|
||||
const ops = { filter };
|
||||
const ops = {};
|
||||
if (!(filter instanceof Expression.Literal && filter.value))
|
||||
ops["filter"] = filter.toString();
|
||||
if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"];
|
||||
m.route.set("/files", ops);
|
||||
}
|
||||
@@ -140,10 +154,9 @@ export const component: ClosureComponent = (): Component => {
|
||||
m.route.set("/files", ops);
|
||||
}
|
||||
|
||||
let filter = vnode.attrs["filter"]
|
||||
? memoizedParse(vnode.attrs["filter"])
|
||||
: true;
|
||||
filter = unpackSmartQuery(filter);
|
||||
const filter = unpackSmartQuery(
|
||||
vnode.attrs["filter"] ?? new Expression.Literal(true),
|
||||
);
|
||||
|
||||
const files = store.fetch("files", filter, {
|
||||
limit: vnode.state["showCount"] || PAGE_SIZE,
|
||||
|
||||
+51
-33
@@ -1,10 +1,9 @@
|
||||
import m, { ClosureComponent } from "mithril";
|
||||
import { parse, stringify, map } from "../lib/common/expression/parser.ts";
|
||||
import memoize from "../lib/common/memoize.ts";
|
||||
import Autocomplete from "./autocomplete-compnent.ts";
|
||||
import * as smartQuery from "./smart-query.ts";
|
||||
import { validQuery } from "../lib/db/synth.ts";
|
||||
import { Expression } from "../lib/types.ts";
|
||||
import Expression from "../lib/common/expression.ts";
|
||||
|
||||
const getAutocomplete = memoize((resource) => {
|
||||
const labels = smartQuery.getLabels(resource);
|
||||
@@ -22,20 +21,35 @@ const getAutocomplete = memoize((resource) => {
|
||||
return autocomplete;
|
||||
});
|
||||
|
||||
function parseFilter(resource, f): Expression {
|
||||
function parseFilter(resource: string, f: string): Expression {
|
||||
let exp;
|
||||
if (/^[\s0-9a-zA-Z]+:/.test(f)) {
|
||||
const k = f.split(":", 1)[0];
|
||||
const v = f.slice(k.length + 1).trim();
|
||||
exp = ["FUNC", "Q", k.trim(), v];
|
||||
exp = new Expression.FunctionCall("Q", [
|
||||
new Expression.Literal(k.trim()),
|
||||
new Expression.Literal(v),
|
||||
]);
|
||||
} else {
|
||||
exp = parse(f);
|
||||
exp = Expression.parse(f);
|
||||
}
|
||||
|
||||
const unpacked = map(exp, (e) => {
|
||||
if (Array.isArray(e) && e[0] === "FUNC") {
|
||||
if (e[1] === "Q") return smartQuery.unpack(resource, e[2], e[3]);
|
||||
else if (e[1] === "NOW") return Date.now();
|
||||
const unpacked = exp.evaluate((e) => {
|
||||
if (e instanceof Expression.FunctionCall) {
|
||||
if (e.name === "NOW") return new Expression.Literal(Date.now());
|
||||
else if (e.name === "Q") {
|
||||
if (
|
||||
e.args[0] instanceof Expression.Literal &&
|
||||
e.args[1] instanceof Expression.Literal
|
||||
) {
|
||||
const r = smartQuery.unpack(
|
||||
resource,
|
||||
e.args[0].value as string,
|
||||
e.args[1].value as string,
|
||||
);
|
||||
return r;
|
||||
}
|
||||
}
|
||||
}
|
||||
return e;
|
||||
});
|
||||
@@ -46,28 +60,33 @@ function parseFilter(resource, f): Expression {
|
||||
return exp;
|
||||
}
|
||||
|
||||
function stringifyFilter(f: Expression): string {
|
||||
if (Array.isArray(f) && f[0] === "FUNC" && f[1] === "Q")
|
||||
return `${f[2]}: ${f[3]}`;
|
||||
return stringify(f);
|
||||
}
|
||||
|
||||
function splitFilter(filter: string): string[] {
|
||||
function splitFilter(filter: Expression): string[] {
|
||||
if (!filter) return [""];
|
||||
const list: string[] = [];
|
||||
const f = parse(filter);
|
||||
if (Array.isArray(f) && f[0] === "AND")
|
||||
for (const ff of f.slice(1)) list.push(stringifyFilter(ff));
|
||||
else list.push(stringifyFilter(f));
|
||||
if (filter instanceof Expression.Literal && filter.value) return [""];
|
||||
const list: Expression[] = [filter];
|
||||
const res: string[] = [];
|
||||
while (list.length) {
|
||||
const f = list.pop();
|
||||
if (f instanceof Expression.Binary && f.operator === "AND") {
|
||||
list.push(f.right);
|
||||
list.push(f.left);
|
||||
} else if (f instanceof Expression.FunctionCall && f.name === "Q") {
|
||||
const l = f.args[0] as Expression.Literal;
|
||||
const r = f.args[1] as Expression.Literal;
|
||||
res.push(`${l.value}: ${r.value}`);
|
||||
} else {
|
||||
res.push(f.toString());
|
||||
}
|
||||
}
|
||||
|
||||
list.push("");
|
||||
return list;
|
||||
res.push("");
|
||||
return res;
|
||||
}
|
||||
|
||||
interface Attrs {
|
||||
resource: string;
|
||||
filter: string;
|
||||
onChange: (filter: string) => void;
|
||||
filter?: Expression;
|
||||
onChange: (filter: Expression) => void;
|
||||
}
|
||||
|
||||
const component: ClosureComponent<Attrs> = (initialVnode) => {
|
||||
@@ -80,23 +99,22 @@ const component: ClosureComponent<Attrs> = (initialVnode) => {
|
||||
filterTouched = false;
|
||||
filterInvalid = 0;
|
||||
filterList = filterList.filter((f) => f);
|
||||
const list = filterList.map((f, idx) => {
|
||||
let filter: Expression = new Expression.Literal(true);
|
||||
for (const [idx, f] of filterList.entries()) {
|
||||
try {
|
||||
return parseFilter(attrs.resource, f);
|
||||
filter = Expression.and(filter, parseFilter(attrs.resource, f));
|
||||
} catch {
|
||||
filterInvalid |= 1 << idx;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
filterList.push("");
|
||||
|
||||
if (filterInvalid) {
|
||||
m.redraw();
|
||||
return;
|
||||
}
|
||||
if (list.length === 0) attrs.onChange("");
|
||||
else if (list.length > 1) attrs.onChange(stringify(["AND", ...list]));
|
||||
else attrs.onChange(stringify(list[0]));
|
||||
if (!filterList.length) attrs.onChange(null);
|
||||
else attrs.onChange(filter);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -120,7 +138,7 @@ const component: ClosureComponent<Attrs> = (initialVnode) => {
|
||||
"appearance-none rounded-none relative block w-full px-3 py-2 border-stone-300 placeholder-stone-500 text-stone-900 focus:ring-cyan-500 focus:border-cyan-500 focus:z-10 sm:text-sm";
|
||||
if (idx === 0) classNames += " rounded-t-md";
|
||||
if (idx === filterList.length - 1) classNames += " rounded-b-md";
|
||||
if (filterInvalid & (1 << idx)) classNames += " text-red-700";
|
||||
if (filterInvalid & (1 << idx)) classNames += " !text-red-700";
|
||||
|
||||
return m(`input`, {
|
||||
type: "text",
|
||||
|
||||
@@ -63,7 +63,7 @@ function renderTable(
|
||||
checked: records.length && selected.size === records.length,
|
||||
onchange: (e) => {
|
||||
for (const record of records) {
|
||||
const id = record["_id"] || record["DeviceID.ID"].value[0];
|
||||
const id = record["_id"] ?? record["DeviceID.ID"];
|
||||
if (e.target.checked) selected.add(id);
|
||||
else selected.delete(id);
|
||||
}
|
||||
@@ -148,7 +148,7 @@ function renderTable(
|
||||
// Table rows
|
||||
const rows = [];
|
||||
for (const record of records) {
|
||||
const id = record["_id"] || record["DeviceID.ID"].value[0];
|
||||
const id = record["_id"] ?? record["DeviceID.ID"];
|
||||
const tds = [];
|
||||
const isSelected = selected.has(id);
|
||||
if (buttons.length) {
|
||||
@@ -224,7 +224,7 @@ function renderTable(
|
||||
{
|
||||
class: isSelected ? "bg-stone-50" : "",
|
||||
onclick: (e) => {
|
||||
if (["INPUT", "BUTTON", "A"].includes(e.target.nodeName)) {
|
||||
if (e.target.closest("input, button, a")) {
|
||||
e.redraw = false;
|
||||
return;
|
||||
}
|
||||
@@ -339,7 +339,7 @@ const component: ClosureComponent = (): Component => {
|
||||
|
||||
const _selected = new Set<string>();
|
||||
for (const record of data) {
|
||||
const id = record["_id"] || record["DeviceID.ID"].value[0];
|
||||
const id = record["_id"] ?? record["DeviceID.ID"];
|
||||
if (selected.has(id)) _selected.add(id);
|
||||
}
|
||||
|
||||
|
||||
+25
-28
@@ -1,31 +1,29 @@
|
||||
import { ClosureComponent, Component } from "mithril";
|
||||
import { ClosureComponent } from "mithril";
|
||||
import { m } from "./components.ts";
|
||||
import config from "./config.ts";
|
||||
import { overview } from "./config.ts";
|
||||
import * as store from "./store.ts";
|
||||
import pieChartComponent from "./pie-chart-component.ts";
|
||||
|
||||
const GROUPS = config.ui.overview.groups || {};
|
||||
const CHARTS = {};
|
||||
for (const group of Object.values(GROUPS)) {
|
||||
for (const chartName of Object.values(group["charts"]) as string[])
|
||||
CHARTS[chartName] = config.ui.overview.charts[chartName];
|
||||
const GROUPS = overview.groups;
|
||||
const CHARTS: typeof overview.charts = {};
|
||||
for (const group of GROUPS) {
|
||||
for (const chartName of group.charts)
|
||||
CHARTS[chartName] = overview.charts[chartName];
|
||||
}
|
||||
|
||||
function queryCharts(charts: Record<string, unknown>): Record<string, unknown> {
|
||||
function queryCharts(charts: typeof overview.charts): typeof charts {
|
||||
charts = Object.assign({}, charts);
|
||||
for (let [chartName, chart] of Object.entries(charts)) {
|
||||
charts[chartName] = chart = Object.assign({}, chart);
|
||||
chart["slices"] = Object.assign({}, chart["slices"]);
|
||||
for (let [sliceName, slice] of Object.entries(chart["slices"])) {
|
||||
const filter = slice["filter"];
|
||||
chart["slices"][sliceName] = slice = Object.assign({}, slice);
|
||||
slice["count"] = store.count("devices", filter);
|
||||
charts[chartName] = chart = { ...chart };
|
||||
chart.slices = chart.slices.map((s) => ({ ...s }));
|
||||
for (const slice of chart.slices) {
|
||||
slice["count"] = store.count("devices", slice.filter);
|
||||
}
|
||||
}
|
||||
return charts;
|
||||
}
|
||||
|
||||
export function init(): Promise<Record<string, unknown>> {
|
||||
export function init(): Promise<{ charts: typeof overview.charts }> {
|
||||
if (!window.authorizer.hasAccess("devices", 1)) {
|
||||
return Promise.reject(
|
||||
new Error("You are not authorized to view this page"),
|
||||
@@ -35,37 +33,36 @@ export function init(): Promise<Record<string, unknown>> {
|
||||
return Promise.resolve({ charts: queryCharts(CHARTS) });
|
||||
}
|
||||
|
||||
export const component: ClosureComponent = (): Component => {
|
||||
interface Attrs {
|
||||
charts: typeof overview.charts;
|
||||
}
|
||||
|
||||
export const component: ClosureComponent<Attrs> = () => {
|
||||
return {
|
||||
view: (vnode) => {
|
||||
document.title = "Overview - GenieACS";
|
||||
const children = [];
|
||||
for (const group of Object.values(GROUPS)) {
|
||||
if (group["label"]) {
|
||||
for (const group of GROUPS) {
|
||||
if (group.label) {
|
||||
children.push(
|
||||
m(
|
||||
"h1.text-xl font-medium text-stone-900 mb-5",
|
||||
store.evaluateExpression(group["label"], null),
|
||||
),
|
||||
m("h1.text-xl font-medium text-stone-900 mb-5", group["label"]),
|
||||
);
|
||||
}
|
||||
|
||||
const groupChildren = [];
|
||||
for (const chartName of Object.values(group["charts"]) as string[]) {
|
||||
const chart = vnode.attrs["charts"][chartName];
|
||||
for (const chartName of group.charts) {
|
||||
const chart = vnode.attrs.charts[chartName];
|
||||
const chartChildren = [];
|
||||
if (chart.label) {
|
||||
chartChildren.push(
|
||||
m(
|
||||
"h2.text-lg font-semibold text-stone-700 truncate mb-5 text-center",
|
||||
store.evaluateExpression(chart.label, null),
|
||||
chart.label,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const attrs = {};
|
||||
attrs["chart"] = chart;
|
||||
chartChildren.push(m(pieChartComponent, attrs));
|
||||
chartChildren.push(m(pieChartComponent, { chart }));
|
||||
|
||||
groupChildren.push(
|
||||
m(
|
||||
|
||||
+33
-19
@@ -1,6 +1,6 @@
|
||||
import { Children, ClosureComponent, Component } from "mithril";
|
||||
import { m } from "./components.ts";
|
||||
import config from "./config.ts";
|
||||
import { pageSize as PAGE_SIZE } from "./config.ts";
|
||||
import * as store from "./store.ts";
|
||||
import * as notifications from "./notifications.ts";
|
||||
import memoize from "../lib/common/memoize.ts";
|
||||
@@ -8,12 +8,10 @@ import putFormComponent from "./put-form-component.ts";
|
||||
import indexTableComponent from "./index-table-component.ts";
|
||||
import * as overlay from "./overlay.ts";
|
||||
import * as smartQuery from "./smart-query.ts";
|
||||
import { map, parse, stringify } from "../lib/common/expression/parser.ts";
|
||||
import Expression from "../lib/common/expression.ts";
|
||||
import filterComponent from "./filter-component.ts";
|
||||
|
||||
const PAGE_SIZE = config.ui.pageSize || 10;
|
||||
|
||||
const memoizedParse = memoize(parse);
|
||||
const memoizedParse = memoize((str) => Expression.parse(str));
|
||||
const memoizedJsonParse = memoize(JSON.parse);
|
||||
|
||||
const attributes = [
|
||||
@@ -60,10 +58,22 @@ function getExcerpt(text: string, maxLength = 80, maxLines = 10): string[] {
|
||||
return lines;
|
||||
}
|
||||
|
||||
const unpackSmartQuery = memoize((query) => {
|
||||
return map(query, (e) => {
|
||||
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "Q")
|
||||
return smartQuery.unpack("permissions", e[2], e[3]);
|
||||
const unpackSmartQuery = memoize((query: Expression) => {
|
||||
return query.evaluate((e) => {
|
||||
if (e instanceof Expression.FunctionCall) {
|
||||
if (e.name === "Q") {
|
||||
if (
|
||||
e.args[0] instanceof Expression.Literal &&
|
||||
e.args[1] instanceof Expression.Literal
|
||||
) {
|
||||
return smartQuery.unpack(
|
||||
"permissions",
|
||||
e.args[0].value as string,
|
||||
e.args[1].value as string,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return e;
|
||||
});
|
||||
});
|
||||
@@ -89,7 +99,7 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
|
||||
|
||||
if (object.filter) {
|
||||
try {
|
||||
object.filter = stringify(memoizedParse(object.filter));
|
||||
object.filter = memoizedParse(object.filter).toString();
|
||||
} catch {
|
||||
return void resolve({
|
||||
filter: "Filter must be valid expression",
|
||||
@@ -99,7 +109,7 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
|
||||
|
||||
if (object.validate) {
|
||||
try {
|
||||
object.validate = stringify(memoizedParse(object.validate));
|
||||
object.validate = memoizedParse(object.validate).toString();
|
||||
} catch {
|
||||
return void resolve({
|
||||
validate: "Validate must be valid expression",
|
||||
@@ -163,7 +173,7 @@ const getDownloadUrl = memoize((filter) => {
|
||||
const cols = {};
|
||||
for (const attr of attributes) cols[attr.label] = attr.id;
|
||||
return `api/permissions.csv?${m.buildQueryString({
|
||||
filter: stringify(filter),
|
||||
filter: filter.toString(),
|
||||
columns: JSON.stringify(cols),
|
||||
})}`;
|
||||
});
|
||||
@@ -176,8 +186,11 @@ export function init(
|
||||
new Error("You are not authorized to view this page"),
|
||||
);
|
||||
}
|
||||
const sort = args.hasOwnProperty("sort") ? "" + args["sort"] : "";
|
||||
const filter = args.hasOwnProperty("filter") ? "" + args["filter"] : "";
|
||||
let filter: Expression = null;
|
||||
let sort: Record<string, number> = null;
|
||||
if (args.hasOwnProperty("filter"))
|
||||
filter = Expression.parse(args["filter"] as string);
|
||||
if (args.hasOwnProperty("sort")) sort = JSON.parse(args["sort"] as string);
|
||||
return Promise.resolve({ filter, sort });
|
||||
}
|
||||
|
||||
@@ -193,7 +206,9 @@ export const component: ClosureComponent = (): Component => {
|
||||
}
|
||||
|
||||
function onFilterChanged(filter): void {
|
||||
const ops = { filter };
|
||||
const ops = {};
|
||||
if (!(filter instanceof Expression.Literal && filter.value))
|
||||
ops["filter"] = filter.toString();
|
||||
if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"];
|
||||
m.route.set("/permissions", ops);
|
||||
}
|
||||
@@ -218,10 +233,9 @@ export const component: ClosureComponent = (): Component => {
|
||||
m.route.set("/permissions", ops);
|
||||
}
|
||||
|
||||
let filter = vnode.attrs["filter"]
|
||||
? memoizedParse(vnode.attrs["filter"])
|
||||
: true;
|
||||
filter = unpackSmartQuery(filter);
|
||||
const filter = unpackSmartQuery(
|
||||
vnode.attrs["filter"] ?? new Expression.Literal(true),
|
||||
);
|
||||
|
||||
const permissions = store.fetch("permissions", filter, {
|
||||
limit: vnode.state["showCount"] || PAGE_SIZE,
|
||||
|
||||
+21
-14
@@ -1,12 +1,8 @@
|
||||
import { ClosureComponent, Component, Children } from "mithril";
|
||||
import { ClosureComponent, Children } from "mithril";
|
||||
import { m } from "./components.ts";
|
||||
import { stringify } from "../lib/common/expression/parser.ts";
|
||||
import memoize from "../lib/common/memoize.ts";
|
||||
import * as store from "./store.ts";
|
||||
import Expression from "../lib/common/expression.ts";
|
||||
|
||||
const memoizedStringify = memoize(stringify);
|
||||
|
||||
function drawChart(chartData): Children {
|
||||
function drawChart(chartData: Attrs["chart"]): Children {
|
||||
const slices = chartData.slices;
|
||||
const total: number = Array.from(Object.values(chartData.slices)).reduce(
|
||||
(a: number, s) => a + (s["count"]["value"] || 0),
|
||||
@@ -27,10 +23,10 @@ function drawChart(chartData): Children {
|
||||
m(
|
||||
"td",
|
||||
m("span.inline-block w-3 h-3 border border-stone-200 mr-1", {
|
||||
style: `background-color: ${store.evaluateExpression(slice["color"], null)} !important;`,
|
||||
style: `background-color: ${slice.color} !important;`,
|
||||
}),
|
||||
),
|
||||
m("td.w-full", store.evaluateExpression(slice["label"], null)),
|
||||
m("td.w-full", slice.label),
|
||||
m(
|
||||
"td.text-stone-500 text-right tabular-nums",
|
||||
`${Math.round(percent * 100)}%`,
|
||||
@@ -41,7 +37,7 @@ function drawChart(chartData): Children {
|
||||
"a.text-cyan-700 hover:text-cyan-900 font-medium ml-2",
|
||||
{
|
||||
href: `#!/devices/?${m.buildQueryString({
|
||||
filter: memoizedStringify(slice["filter"]),
|
||||
filter: slice.filter.toString(),
|
||||
})}`,
|
||||
},
|
||||
slice["count"]["value"] || 0,
|
||||
@@ -67,7 +63,7 @@ function drawChart(chartData): Children {
|
||||
paths.push(
|
||||
m("path.stroke-white stroke-1", {
|
||||
d: sketch,
|
||||
fill: store.evaluateExpression(slice["color"], null),
|
||||
fill: slice.color,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -81,7 +77,7 @@ function drawChart(chartData): Children {
|
||||
"a.opacity-0 hover:opacity-100 focus-visible:opacity-100 outline-hidden",
|
||||
{
|
||||
"xlink:href": `#!/devices/?${m.buildQueryString({
|
||||
filter: memoizedStringify(slice["filter"]),
|
||||
filter: slice.filter.toString(),
|
||||
})}`,
|
||||
},
|
||||
[
|
||||
@@ -137,10 +133,21 @@ function drawChart(chartData): Children {
|
||||
);
|
||||
}
|
||||
|
||||
const component: ClosureComponent = (): Component => {
|
||||
interface Attrs {
|
||||
chart: {
|
||||
label: string;
|
||||
slices: {
|
||||
label: string;
|
||||
filter: Expression;
|
||||
color: string;
|
||||
}[];
|
||||
};
|
||||
}
|
||||
|
||||
const component: ClosureComponent<Attrs> = () => {
|
||||
return {
|
||||
view: (vnode) => {
|
||||
return drawChart(vnode.attrs["chart"]);
|
||||
return drawChart(vnode.attrs.chart);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
+36
-19
@@ -1,6 +1,6 @@
|
||||
import { ClosureComponent, Component, Children, Vnode } from "mithril";
|
||||
import { m } from "./components.ts";
|
||||
import config from "./config.ts";
|
||||
import { pageSize as PAGE_SIZE } from "./config.ts";
|
||||
import filterComponent from "./filter-component.ts";
|
||||
import * as overlay from "./overlay.ts";
|
||||
import * as store from "./store.ts";
|
||||
@@ -9,11 +9,9 @@ import putFormComponent from "./put-form-component.ts";
|
||||
import indexTableComponent from "./index-table-component.ts";
|
||||
import memoize from "../lib/common/memoize.ts";
|
||||
import * as smartQuery from "./smart-query.ts";
|
||||
import { map, parse, stringify } from "../lib/common/expression/parser.ts";
|
||||
import Expression from "../lib/common/expression.ts";
|
||||
|
||||
const PAGE_SIZE = config.ui.pageSize || 10;
|
||||
|
||||
const memoizedParse = memoize(parse);
|
||||
const memoizedParse = memoize((str) => Expression.parse(str));
|
||||
const memoizedJsonParse = memoize(JSON.parse);
|
||||
|
||||
const attributes = [
|
||||
@@ -27,10 +25,22 @@ const attributes = [
|
||||
{ id: "provisionArgs", label: "Arguments", type: "textarea" },
|
||||
];
|
||||
|
||||
const unpackSmartQuery = memoize((query) => {
|
||||
return map(query, (e) => {
|
||||
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "Q")
|
||||
return smartQuery.unpack("presets", e[2], e[3]);
|
||||
const unpackSmartQuery = memoize((query: Expression) => {
|
||||
return query.evaluate((e) => {
|
||||
if (e instanceof Expression.FunctionCall) {
|
||||
if (e.name === "Q") {
|
||||
if (
|
||||
e.args[0] instanceof Expression.Literal &&
|
||||
e.args[1] instanceof Expression.Literal
|
||||
) {
|
||||
return smartQuery.unpack(
|
||||
"presets",
|
||||
e.args[0].value as string,
|
||||
e.args[1].value as string,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return e;
|
||||
});
|
||||
});
|
||||
@@ -71,7 +81,7 @@ function putActionHandler(action, _object, isNew): Promise<ValidationErrors> {
|
||||
|
||||
if (object.precondition) {
|
||||
try {
|
||||
object.precondition = stringify(memoizedParse(object.precondition));
|
||||
object.precondition = memoizedParse(object.precondition).toString();
|
||||
} catch {
|
||||
return void resolve({
|
||||
precondition: "Precondition must be valid expression",
|
||||
@@ -134,7 +144,7 @@ const getDownloadUrl = memoize((filter) => {
|
||||
const cols = {};
|
||||
for (const attr of attributes) cols[attr.label] = attr.id;
|
||||
return `api/presets.csv?${m.buildQueryString({
|
||||
filter: stringify(filter),
|
||||
filter: filter.toString(),
|
||||
columns: JSON.stringify(cols),
|
||||
})}`;
|
||||
});
|
||||
@@ -148,8 +158,11 @@ export function init(
|
||||
);
|
||||
}
|
||||
|
||||
const sort = args.hasOwnProperty("sort") ? "" + args["sort"] : "";
|
||||
const filter = args.hasOwnProperty("filter") ? "" + args["filter"] : "";
|
||||
let filter: Expression = null;
|
||||
let sort: Record<string, number> = null;
|
||||
if (args.hasOwnProperty("filter"))
|
||||
filter = Expression.parse(args["filter"] as string);
|
||||
if (args.hasOwnProperty("sort")) sort = JSON.parse(args["sort"] as string);
|
||||
return Promise.resolve({ filter, sort });
|
||||
}
|
||||
|
||||
@@ -165,7 +178,9 @@ export const component: ClosureComponent = (): Component => {
|
||||
}
|
||||
|
||||
function onFilterChanged(filter): void {
|
||||
const ops = { filter };
|
||||
const ops = {};
|
||||
if (!(filter instanceof Expression.Literal && filter.value))
|
||||
ops["filter"] = filter.toString();
|
||||
if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"];
|
||||
m.route.set("/presets", ops);
|
||||
}
|
||||
@@ -197,10 +212,9 @@ export const component: ClosureComponent = (): Component => {
|
||||
m.route.set("/presets", ops);
|
||||
}
|
||||
|
||||
let filter = vnode.attrs["filter"]
|
||||
? memoizedParse(vnode.attrs["filter"])
|
||||
: true;
|
||||
filter = unpackSmartQuery(filter);
|
||||
const filter = unpackSmartQuery(
|
||||
vnode.attrs["filter"] ?? new Expression.Literal(true),
|
||||
);
|
||||
|
||||
const presets = store.fetch("presets", filter, {
|
||||
limit: vnode.state["showCount"] || PAGE_SIZE,
|
||||
@@ -220,7 +234,10 @@ export const component: ClosureComponent = (): Component => {
|
||||
"instances",
|
||||
]);
|
||||
|
||||
const provisions = store.fetch("provisions", true);
|
||||
const provisions = store.fetch(
|
||||
"provisions",
|
||||
new Expression.Literal(true),
|
||||
);
|
||||
if (provisions.fulfilled) {
|
||||
for (const p of provisions.value) {
|
||||
userDefinedProvisions.add(p["_id"]);
|
||||
|
||||
+30
-17
@@ -1,6 +1,6 @@
|
||||
import { ClosureComponent, Component, Children } from "mithril";
|
||||
import { m } from "./components.ts";
|
||||
import config from "./config.ts";
|
||||
import { pageSize as PAGE_SIZE } from "./config.ts";
|
||||
import filterComponent from "./filter-component.ts";
|
||||
import * as store from "./store.ts";
|
||||
import * as notifications from "./notifications.ts";
|
||||
@@ -9,12 +9,9 @@ import putFormComponent from "./put-form-component.ts";
|
||||
import indexTableComponent from "./index-table-component.ts";
|
||||
import * as overlay from "./overlay.ts";
|
||||
import * as smartQuery from "./smart-query.ts";
|
||||
import { map, parse, stringify } from "../lib/common/expression/parser.ts";
|
||||
import Expression from "../lib/common/expression.ts";
|
||||
import { loadCodeMirror } from "./dynamic-loader.ts";
|
||||
|
||||
const PAGE_SIZE = config.ui.pageSize || 10;
|
||||
|
||||
const memoizedParse = memoize(parse);
|
||||
const memoizedJsonParse = memoize(JSON.parse);
|
||||
|
||||
const attributes = [
|
||||
@@ -22,10 +19,22 @@ const attributes = [
|
||||
{ id: "script", label: "Script", type: "code" },
|
||||
];
|
||||
|
||||
const unpackSmartQuery = memoize((query) => {
|
||||
return map(query, (e) => {
|
||||
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "Q")
|
||||
return smartQuery.unpack("provisions", e[2], e[3]);
|
||||
const unpackSmartQuery = memoize((query: Expression) => {
|
||||
return query.evaluate((e) => {
|
||||
if (e instanceof Expression.FunctionCall) {
|
||||
if (e.name === "Q") {
|
||||
if (
|
||||
e.args[0] instanceof Expression.Literal &&
|
||||
e.args[1] instanceof Expression.Literal
|
||||
) {
|
||||
return smartQuery.unpack(
|
||||
"provisions",
|
||||
e.args[0].value as string,
|
||||
e.args[1].value as string,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return e;
|
||||
});
|
||||
});
|
||||
@@ -104,7 +113,7 @@ const getDownloadUrl = memoize((filter) => {
|
||||
const cols = {};
|
||||
for (const attr of attributes) cols[attr.label] = attr.id;
|
||||
return `api/provisions.csv?${m.buildQueryString({
|
||||
filter: stringify(filter),
|
||||
filter: filter.toString(),
|
||||
columns: JSON.stringify(cols),
|
||||
})}`;
|
||||
});
|
||||
@@ -118,8 +127,11 @@ export function init(
|
||||
);
|
||||
}
|
||||
|
||||
const sort = args.hasOwnProperty("sort") ? "" + args["sort"] : "";
|
||||
const filter = args.hasOwnProperty("filter") ? "" + args["filter"] : "";
|
||||
let filter: Expression = null;
|
||||
let sort: Record<string, number> = null;
|
||||
if (args.hasOwnProperty("filter"))
|
||||
filter = Expression.parse(args["filter"] as string);
|
||||
if (args.hasOwnProperty("sort")) sort = JSON.parse(args["sort"] as string);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
loadCodeMirror()
|
||||
@@ -142,7 +154,9 @@ export const component: ClosureComponent = (): Component => {
|
||||
}
|
||||
|
||||
function onFilterChanged(filter): void {
|
||||
const ops = { filter };
|
||||
const ops = {};
|
||||
if (!(filter instanceof Expression.Literal && filter.value))
|
||||
ops["filter"] = filter.toString();
|
||||
if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"];
|
||||
m.route.set("/provisions", ops);
|
||||
}
|
||||
@@ -164,10 +178,9 @@ export const component: ClosureComponent = (): Component => {
|
||||
m.route.set("/provisions", ops);
|
||||
}
|
||||
|
||||
let filter = vnode.attrs["filter"]
|
||||
? memoizedParse(vnode.attrs["filter"])
|
||||
: true;
|
||||
filter = unpackSmartQuery(filter);
|
||||
const filter = unpackSmartQuery(
|
||||
vnode.attrs["filter"] ?? new Expression.Literal(true),
|
||||
);
|
||||
|
||||
const provisions = store.fetch("provisions", filter, {
|
||||
limit: vnode.state["showCount"] || PAGE_SIZE,
|
||||
|
||||
+150
-72
@@ -1,51 +1,103 @@
|
||||
import config from "./config.ts";
|
||||
import { Expression } from "../lib/types.ts";
|
||||
import { filters } from "./config.ts";
|
||||
import Expression from "../lib/common/expression.ts";
|
||||
import { encodeTag } from "../lib/util.ts";
|
||||
import Path from "../lib/common/path.ts";
|
||||
|
||||
const resources = {
|
||||
devices: {},
|
||||
faults: {
|
||||
Device: { parameter: ["PARAM", "device"], type: "string" },
|
||||
Channel: { parameter: ["PARAM", "channel"], type: "string" },
|
||||
Code: { parameter: ["PARAM", "code"], type: "string" },
|
||||
Retries: { parameter: ["PARAM", "retries"], type: "number" },
|
||||
Timestamp: { parameter: ["PARAM", "timestamp"], type: "timestamp" },
|
||||
Device: {
|
||||
parameter: new Expression.Parameter(Path.parse("device")),
|
||||
type: "string",
|
||||
},
|
||||
Channel: {
|
||||
parameter: new Expression.Parameter(Path.parse("channel")),
|
||||
type: "string",
|
||||
},
|
||||
Code: {
|
||||
parameter: new Expression.Parameter(Path.parse("code")),
|
||||
type: "string",
|
||||
},
|
||||
Retries: {
|
||||
parameter: new Expression.Parameter(Path.parse("retries")),
|
||||
type: "number",
|
||||
},
|
||||
Timestamp: {
|
||||
parameter: new Expression.Parameter(Path.parse("timestamp")),
|
||||
type: "timestamp",
|
||||
},
|
||||
},
|
||||
presets: {
|
||||
ID: { parameter: ["PARAM", "_id"], type: "string" },
|
||||
Channel: { parameter: ["PARAM", "channel"], type: "string" },
|
||||
Weight: { parameter: ["PARAM", "weight"], type: "number" },
|
||||
ID: {
|
||||
parameter: new Expression.Parameter(Path.parse("_id")),
|
||||
type: "string",
|
||||
},
|
||||
Channel: {
|
||||
parameter: new Expression.Parameter(Path.parse("channel")),
|
||||
type: "string",
|
||||
},
|
||||
Weight: {
|
||||
parameter: new Expression.Parameter(Path.parse("weight")),
|
||||
type: "number",
|
||||
},
|
||||
},
|
||||
provisions: {
|
||||
ID: { parameter: ["PARAM", "_id"], type: "string" },
|
||||
ID: {
|
||||
parameter: new Expression.Parameter(Path.parse("_id")),
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
virtualParameters: {
|
||||
ID: { parameter: ["PARAM", "_id"], type: "string" },
|
||||
ID: {
|
||||
parameter: new Expression.Parameter(Path.parse("_id")),
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
files: {
|
||||
ID: { parameter: ["PARAM", "_id"], type: "string" },
|
||||
Type: { parameter: ["PARAM", "metadata.fileType"], type: "string" },
|
||||
OUI: { parameter: ["PARAM", "metadata.oui"], type: "string" },
|
||||
ID: {
|
||||
parameter: new Expression.Parameter(Path.parse("_id")),
|
||||
type: "string",
|
||||
},
|
||||
Type: {
|
||||
parameter: new Expression.Parameter(Path.parse("metadata.fileType")),
|
||||
type: "string",
|
||||
},
|
||||
OUI: {
|
||||
parameter: new Expression.Parameter(Path.parse("metadata.oui")),
|
||||
type: "string",
|
||||
},
|
||||
"Product class": {
|
||||
parameter: ["PARAM", "metadata.productClass"],
|
||||
parameter: new Expression.Parameter(Path.parse("metadata.productClass")),
|
||||
type: "string",
|
||||
},
|
||||
Version: {
|
||||
parameter: new Expression.Parameter(Path.parse("metadata.version")),
|
||||
type: "string",
|
||||
},
|
||||
Version: { parameter: ["PARAM", "metadata.version"], type: "string" },
|
||||
},
|
||||
permissions: {
|
||||
Role: { parameter: ["PARAM", "role"], type: "string" },
|
||||
Resource: { parameter: ["PARAM", "resource"], type: "string" },
|
||||
Access: { parameter: ["PARAM", "access"], type: "number" },
|
||||
Role: {
|
||||
parameter: new Expression.Parameter(Path.parse("role")),
|
||||
type: "string",
|
||||
},
|
||||
Resource: {
|
||||
parameter: new Expression.Parameter(Path.parse("resource")),
|
||||
type: "string",
|
||||
},
|
||||
Access: {
|
||||
parameter: new Expression.Parameter(Path.parse("access")),
|
||||
type: "number",
|
||||
},
|
||||
},
|
||||
users: {
|
||||
Username: {
|
||||
parameter: new Expression.Parameter(Path.parse("_id")),
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
users: { Username: { parameter: ["PARAM", "_id"], type: "string" } },
|
||||
};
|
||||
|
||||
for (const v of Object.values(
|
||||
config.ui.filters as Record<
|
||||
string,
|
||||
{ label: string; parameter: string; type: string }
|
||||
>,
|
||||
)) {
|
||||
for (const v of filters) {
|
||||
resources.devices[v.label] = {
|
||||
parameter: v.parameter,
|
||||
type: (v.type || "").split(",").map((s) => s.trim()),
|
||||
@@ -70,7 +122,7 @@ function queryNumber(param: Expression, value: string): Expression {
|
||||
const v = parseInt(value);
|
||||
if (v !== +value) return null;
|
||||
|
||||
return [op, param, v];
|
||||
return new Expression.Binary(op, param, new Expression.Literal(v));
|
||||
}
|
||||
|
||||
function queryTimestamp(param: Expression, value: string): Expression {
|
||||
@@ -86,52 +138,71 @@ function queryTimestamp(param: Expression, value: string): Expression {
|
||||
let v = parseInt(value);
|
||||
if (v !== +value) v = Date.parse(value);
|
||||
if (isNaN(v)) return null;
|
||||
return [op, param, v];
|
||||
return new Expression.Binary(op, param, new Expression.Literal(v));
|
||||
}
|
||||
|
||||
function queryString(param: Expression, value: string): Expression {
|
||||
return ["LIKE", ["FUNC", "LOWER", param], value.toLowerCase()];
|
||||
return new Expression.Binary(
|
||||
"LIKE",
|
||||
new Expression.FunctionCall("LOWER", [param]),
|
||||
new Expression.Literal(value.toLowerCase()),
|
||||
);
|
||||
}
|
||||
|
||||
function queryStringCaseSensitive(
|
||||
param: Expression,
|
||||
value: string,
|
||||
): Expression {
|
||||
return ["LIKE", param, value];
|
||||
return new Expression.Binary("LIKE", param, new Expression.Literal(value));
|
||||
}
|
||||
|
||||
function queryStringMonoCase(param: Expression, value: string): Expression {
|
||||
return [
|
||||
"OR",
|
||||
["LIKE", param, value.toLowerCase()],
|
||||
["LIKE", param, value.toUpperCase()],
|
||||
];
|
||||
return Expression.or(
|
||||
new Expression.Binary(
|
||||
"LIKE",
|
||||
param,
|
||||
new Expression.Literal(value.toLowerCase()),
|
||||
),
|
||||
new Expression.Binary(
|
||||
"LIKE",
|
||||
param,
|
||||
new Expression.Literal(value.toUpperCase()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function queryMac(param: Expression, value: string): Expression {
|
||||
value = value.replace(/[^a-f0-9]/gi, "").toLowerCase();
|
||||
if (!value) return null;
|
||||
if (value.length === 12) {
|
||||
return [
|
||||
"OR",
|
||||
["=", param, value.replace(/(..)(?!$)/g, "$1:").toLowerCase()],
|
||||
["=", param, value.replace(/(..)(?!$)/g, "$1:").toUpperCase()],
|
||||
];
|
||||
value = value.replace(/(..)(?!$)/g, "$1:");
|
||||
return Expression.or(
|
||||
new Expression.Binary(
|
||||
"=",
|
||||
param,
|
||||
new Expression.Literal(value.toLowerCase()),
|
||||
),
|
||||
new Expression.Binary(
|
||||
"=",
|
||||
param,
|
||||
new Expression.Literal(value.toUpperCase()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
"OR",
|
||||
[
|
||||
param = new Expression.FunctionCall("LOWER", [param]);
|
||||
return Expression.or(
|
||||
new Expression.Binary(
|
||||
"LIKE",
|
||||
["FUNC", "LOWER", param],
|
||||
`%${value.replace(/(..)(?!$)/g, "$1:")}%`,
|
||||
],
|
||||
[
|
||||
param,
|
||||
new Expression.Literal(`%${value.replace(/(..)(?!$)/g, "$1:")}%`),
|
||||
),
|
||||
new Expression.Binary(
|
||||
"LIKE",
|
||||
["FUNC", "LOWER", param],
|
||||
`%${value.replace(/(.)(.)/g, "$1:$2")}%`,
|
||||
],
|
||||
];
|
||||
param,
|
||||
new Expression.Literal(`%${value.replace(/(.)(.)/g, "$1:$2")}%`),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function queryMacWildcard(param: Expression, value: string): Expression {
|
||||
@@ -143,23 +214,32 @@ function queryMacWildcard(param: Expression, value: string): Expression {
|
||||
p.replace(/(.)(.)/gi, "$1:$2"),
|
||||
]);
|
||||
|
||||
const res = new Set();
|
||||
const set = new Set<string>();
|
||||
for (let i = 0; i < 2 ** groups.length; ++i) {
|
||||
const r = groups.map((g, j) => g[(i >> j) & 1]).join("%");
|
||||
if (/^[a-f0-9]:/i.test(r) || /:[a-f0-9]$/i.test(r)) continue;
|
||||
res.add(r.toLocaleLowerCase());
|
||||
res.add(r.toUpperCase());
|
||||
set.add(r.toLocaleLowerCase());
|
||||
set.add(r.toUpperCase());
|
||||
}
|
||||
if (!res.size) return queryStringMonoCase(param, value);
|
||||
if (!set.size) return queryStringMonoCase(param, value);
|
||||
|
||||
const clauses = [...res].map((r) => ["LIKE", param, r]);
|
||||
if (clauses.length === 1) return clauses[0];
|
||||
return ["OR", ...clauses];
|
||||
let res: Expression = new Expression.Literal(false);
|
||||
for (const s of set) {
|
||||
res = Expression.or(
|
||||
res,
|
||||
new Expression.Binary("LIKE", param, new Expression.Literal(s)),
|
||||
);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
function queryTag(tag: string): Expression {
|
||||
const t = encodeTag(tag);
|
||||
return ["IS NOT NULL", ["PARAM", `Tags.${t}`]];
|
||||
return new Expression.Unary(
|
||||
"IS NOT NULL",
|
||||
new Expression.Parameter(Path.parse(`Tags.${t}`)),
|
||||
);
|
||||
}
|
||||
|
||||
export function getTip(resource: string, label: string): string {
|
||||
@@ -214,21 +294,21 @@ export function unpack(
|
||||
if (!resources[resource]) return null;
|
||||
const type = resources[resource][label].type;
|
||||
value = value.trim();
|
||||
const res: Expression = ["OR"];
|
||||
let res: Expression = new Expression.Literal(false);
|
||||
|
||||
if (type.length === 0 || type.includes("number")) {
|
||||
const q = queryNumber(resources[resource][label].parameter, value);
|
||||
if (q) res.push(q);
|
||||
if (q) res = Expression.or(res, q);
|
||||
}
|
||||
|
||||
if (type.length === 0 || type.includes("string")) {
|
||||
const q = queryString(resources[resource][label].parameter, value);
|
||||
if (q) res.push(q);
|
||||
if (q) res = Expression.or(res, q);
|
||||
}
|
||||
|
||||
if (type.length === 0 || type.includes("timestamp")) {
|
||||
const q = queryTimestamp(resources[resource][label].parameter, value);
|
||||
if (q) res.push(q);
|
||||
if (q) res = Expression.or(res, q);
|
||||
}
|
||||
|
||||
if (type.includes("string-casesensitive")) {
|
||||
@@ -236,30 +316,28 @@ export function unpack(
|
||||
resources[resource][label].parameter,
|
||||
value,
|
||||
);
|
||||
if (q) res.push(q);
|
||||
if (q) res = Expression.or(res, q);
|
||||
}
|
||||
|
||||
if (type.includes("string-monocase")) {
|
||||
const q = queryStringMonoCase(resources[resource][label].parameter, value);
|
||||
if (q) res.push(q);
|
||||
if (q) res = Expression.or(res, q);
|
||||
}
|
||||
|
||||
if (type.includes("mac")) {
|
||||
const q = queryMac(resources[resource][label].parameter, value);
|
||||
if (q) res.push(q);
|
||||
if (q) res = Expression.or(res, q);
|
||||
}
|
||||
|
||||
if (type.includes("mac-wildcard")) {
|
||||
const q = queryMacWildcard(resources[resource][label].parameter, value);
|
||||
if (q) res.push(q);
|
||||
if (q) res = Expression.or(res, q);
|
||||
}
|
||||
|
||||
if (type.includes("tag")) {
|
||||
const q = queryTag(value);
|
||||
if (q) res.push(q);
|
||||
if (q) res = Expression.or(res, q);
|
||||
}
|
||||
|
||||
if (res.length <= 1) return null;
|
||||
else if (res.length === 2) return res[1];
|
||||
else return res;
|
||||
return res;
|
||||
}
|
||||
+85
-41
@@ -1,8 +1,8 @@
|
||||
import m from "mithril";
|
||||
import { stringify } from "../lib/common/expression/parser.ts";
|
||||
import { or, and, evaluate } from "../lib/common/expression/util.ts";
|
||||
import Expression from "../lib/common/expression.ts";
|
||||
import Path from "../lib/common/path.ts";
|
||||
import memoize from "../lib/common/memoize.ts";
|
||||
import { Expression, Task } from "../lib/types.ts";
|
||||
import { Task } from "../lib/types.ts";
|
||||
import * as notifications from "./notifications.ts";
|
||||
import { configSnapshot, genieacsVersion } from "./config.ts";
|
||||
import { QueueTask } from "./task-queue.ts";
|
||||
@@ -14,7 +14,30 @@ import {
|
||||
toBookmark,
|
||||
} from "../lib/common/expression/pagination.ts";
|
||||
|
||||
const memoizedStringify = memoize(stringify);
|
||||
function evaluate(exp: Expression, timestamp: number): Expression;
|
||||
function evaluate(
|
||||
exp: Expression,
|
||||
timestamp: number,
|
||||
obj: Record<string, unknown>,
|
||||
): Expression.Literal;
|
||||
function evaluate(
|
||||
exp: Expression,
|
||||
timestamp: number,
|
||||
obj?: Record<string, unknown>,
|
||||
): Expression {
|
||||
return exp.evaluate((e) => {
|
||||
if (e instanceof Expression.Literal) return e;
|
||||
else if (e instanceof Expression.FunctionCall) {
|
||||
if (e.name === "NOW") return new Expression.Literal(timestamp);
|
||||
} else if (e instanceof Expression.Parameter && obj) {
|
||||
let v = obj[e.path.toString()];
|
||||
if (v == null) return new Expression.Literal(null);
|
||||
if (typeof v === "object") v = v["value"]?.[0];
|
||||
return new Expression.Literal(v as any);
|
||||
}
|
||||
return e;
|
||||
});
|
||||
}
|
||||
const memoizedEvaluate = memoize(evaluate);
|
||||
|
||||
let fulfillTimestamp = 0;
|
||||
@@ -59,7 +82,7 @@ for (const r of [
|
||||
objects: new Map(),
|
||||
count: new Map(),
|
||||
fetch: new Map(),
|
||||
combinedFilter: null,
|
||||
combinedFilter: new Expression.Literal(false) as Expression,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -215,13 +238,11 @@ export async function xhrRequest(
|
||||
}
|
||||
|
||||
export function unpackExpression(exp: Expression): Expression {
|
||||
if (!Array.isArray(exp)) return exp;
|
||||
const e = memoizedEvaluate(exp, null, fulfillTimestamp + clockSkew);
|
||||
return e;
|
||||
return memoizedEvaluate(exp, fulfillTimestamp + clockSkew);
|
||||
}
|
||||
|
||||
export function count(resourceType: string, filter: Expression): QueryResponse {
|
||||
const filterStr = memoizedStringify(filter);
|
||||
const filterStr = filter.toString();
|
||||
let queryResponse = resources[resourceType].count.get(filterStr);
|
||||
if (queryResponse) return queryResponse;
|
||||
|
||||
@@ -271,7 +292,8 @@ function compareFunction(sort: {
|
||||
function findMatches(resourceType, filter, sort, limit): any[] {
|
||||
let value = [];
|
||||
for (const obj of resources[resourceType].objects.values())
|
||||
if (evaluate(filter, obj, fulfillTimestamp + clockSkew)) value.push(obj);
|
||||
if (evaluate(filter, fulfillTimestamp + clockSkew, obj).value)
|
||||
value.push(obj);
|
||||
|
||||
value = value.sort(compareFunction(sort));
|
||||
if (limit) value = value.slice(0, limit);
|
||||
@@ -284,7 +306,6 @@ export function fetch(
|
||||
filter: Expression,
|
||||
options: { limit?: number; sort?: { [param: string]: number } } = {},
|
||||
): QueryResponse {
|
||||
const filterStr = memoizedStringify(filter);
|
||||
const sort = Object.assign({}, options.sort);
|
||||
|
||||
const limit = options.limit || 0;
|
||||
@@ -292,7 +313,7 @@ export function fetch(
|
||||
sort["DeviceID.ID"] = sort["DeviceID.ID"] || 1;
|
||||
else sort["_id"] = sort["_id"] || 1;
|
||||
|
||||
const key = `${filterStr}:${limit}:${JSON.stringify(sort)}`;
|
||||
const key = `${filter.toString()}:${limit}:${JSON.stringify(sort)}`;
|
||||
let queryResponse = resources[resourceType].fetch.get(key);
|
||||
if (queryResponse) return queryResponse;
|
||||
|
||||
@@ -308,7 +329,10 @@ export function fetch(
|
||||
);
|
||||
const matches = findMatches(resourceType, satisfied, sort, limit);
|
||||
queries.value.set(queryResponse, matches);
|
||||
if (!diff || (limit && matches.length >= limit))
|
||||
if (
|
||||
(diff instanceof Expression.Literal && !diff.value) ||
|
||||
(limit && matches.length >= limit)
|
||||
)
|
||||
queries.fulfilled.set(queryResponse, fulfillTimestamp);
|
||||
else queries.unsatisfied.set(queryResponse, diff);
|
||||
return queryResponse;
|
||||
@@ -336,7 +360,7 @@ export function fulfill(accessTimestamp: number): void {
|
||||
url:
|
||||
`api/${resourceType}/?` +
|
||||
m.buildQueryString({
|
||||
filter: memoizedStringify(filter),
|
||||
filter: filter.toString(),
|
||||
}),
|
||||
extract: (xhr) => {
|
||||
if (xhr.status === 403) throw new Error("Not authorized");
|
||||
@@ -393,7 +417,7 @@ export function fulfill(accessTimestamp: number): void {
|
||||
url:
|
||||
`api/${resourceType}/?` +
|
||||
m.buildQueryString({
|
||||
filter: memoizedStringify(filter),
|
||||
filter: filter.toString(),
|
||||
limit: 1,
|
||||
skip: limit - 1,
|
||||
sort: JSON.stringify(sort),
|
||||
@@ -419,16 +443,19 @@ export function fulfill(accessTimestamp: number): void {
|
||||
let updated = false;
|
||||
const allPromises2 = [];
|
||||
for (const [resourceType, toFetch] of Object.entries(toFetchAll)) {
|
||||
let combinedFilter = null;
|
||||
let combinedFilter = new Expression.Literal(false) as Expression;
|
||||
|
||||
for (const queryResponse of toFetch) {
|
||||
let filter = queries.filter.get(queryResponse);
|
||||
filter = memoizedEvaluate(filter, null, fulfillTimestamp + clockSkew);
|
||||
filter = memoizedEvaluate(filter, fulfillTimestamp + clockSkew);
|
||||
const bookmark = queries.bookmark.get(queryResponse);
|
||||
const sort = queries.sort.get(queryResponse);
|
||||
if (bookmark)
|
||||
filter = and(filter, bookmarkToExpression(bookmark, sort));
|
||||
combinedFilter = or(combinedFilter, filter);
|
||||
filter = Expression.and(
|
||||
filter,
|
||||
bookmarkToExpression(bookmark, sort),
|
||||
);
|
||||
combinedFilter = Expression.or(combinedFilter, filter);
|
||||
}
|
||||
|
||||
const [union, diff] = unionDiff(
|
||||
@@ -436,19 +463,18 @@ export function fulfill(accessTimestamp: number): void {
|
||||
combinedFilter,
|
||||
);
|
||||
|
||||
if (!diff) {
|
||||
if (diff instanceof Expression.Literal && !diff.value) {
|
||||
for (const queryResponse of toFetch) {
|
||||
let filter = queries.filter.get(queryResponse);
|
||||
filter = memoizedEvaluate(
|
||||
filter,
|
||||
null,
|
||||
fulfillTimestamp + clockSkew,
|
||||
);
|
||||
filter = memoizedEvaluate(filter, fulfillTimestamp + clockSkew);
|
||||
const limit = queries.limit.get(queryResponse);
|
||||
const bookmark = queries.bookmark.get(queryResponse);
|
||||
const sort = queries.sort.get(queryResponse);
|
||||
if (bookmark)
|
||||
filter = and(filter, bookmarkToExpression(bookmark, sort));
|
||||
filter = Expression.and(
|
||||
filter,
|
||||
bookmarkToExpression(bookmark, sort),
|
||||
);
|
||||
|
||||
queries.value.set(
|
||||
queryResponse,
|
||||
@@ -462,7 +488,11 @@ export function fulfill(accessTimestamp: number): void {
|
||||
}
|
||||
|
||||
let deleted = new Set<string>();
|
||||
if (!resources[resourceType].combinedFilter)
|
||||
if (
|
||||
resources[resourceType].combinedFilter instanceof
|
||||
Expression.Literal &&
|
||||
!resources[resourceType].combinedFilter.value
|
||||
)
|
||||
deleted = new Set(resources[resourceType].objects.keys());
|
||||
|
||||
const combinedFilterDiff = diff;
|
||||
@@ -474,15 +504,12 @@ export function fulfill(accessTimestamp: number): void {
|
||||
url:
|
||||
`api/${resourceType}/?` +
|
||||
m.buildQueryString({
|
||||
filter: memoizedStringify(combinedFilterDiff),
|
||||
filter: combinedFilterDiff.toString(),
|
||||
}),
|
||||
background: false,
|
||||
}).then((res) => {
|
||||
for (const r of res as any[]) {
|
||||
const id =
|
||||
resourceType === "devices"
|
||||
? r["DeviceID.ID"].value[0]
|
||||
: r["_id"];
|
||||
const id = r["DeviceID.ID"] ?? r["_id"];
|
||||
resources[resourceType].objects.set(id, r);
|
||||
deleted.delete(id);
|
||||
}
|
||||
@@ -490,7 +517,8 @@ export function fulfill(accessTimestamp: number): void {
|
||||
for (const d of deleted) {
|
||||
const obj = resources[resourceType].objects.get(d);
|
||||
if (
|
||||
evaluate(combinedFilterDiff, obj, fulfillTimestamp + clockSkew)
|
||||
evaluate(combinedFilterDiff, fulfillTimestamp + clockSkew, obj)
|
||||
.value
|
||||
)
|
||||
resources[resourceType].objects.delete(d);
|
||||
}
|
||||
@@ -502,7 +530,10 @@ export function fulfill(accessTimestamp: number): void {
|
||||
const bookmark = queries.bookmark.get(queryResponse);
|
||||
const sort = queries.sort.get(queryResponse);
|
||||
if (bookmark)
|
||||
filter = and(filter, bookmarkToExpression(bookmark, sort));
|
||||
filter = Expression.and(
|
||||
filter,
|
||||
bookmarkToExpression(bookmark, sort),
|
||||
);
|
||||
|
||||
queries.value.set(
|
||||
queryResponse,
|
||||
@@ -530,7 +561,7 @@ export function setTimestamp(t: number): void {
|
||||
if (t > fulfillTimestamp) {
|
||||
fulfillTimestamp = t;
|
||||
for (const resource of Object.values(resources))
|
||||
resource.combinedFilter = null;
|
||||
resource.combinedFilter = new Expression.Literal(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -607,23 +638,32 @@ export function putResource(
|
||||
}
|
||||
|
||||
export function queryConfig(pattern = "%"): Promise<any[]> {
|
||||
const filter = stringify(["LIKE", ["PARAM", "_id"], pattern]);
|
||||
const filter = new Expression.Binary(
|
||||
"LIKE",
|
||||
new Expression.Parameter(Path.parse("_id")),
|
||||
new Expression.Literal(pattern),
|
||||
);
|
||||
return xhrRequest({
|
||||
method: "GET",
|
||||
url: `api/config/?${m.buildQueryString({ filter: filter })}`,
|
||||
url: `api/config/?${m.buildQueryString({ filter: filter.toString() })}`,
|
||||
background: true,
|
||||
});
|
||||
}
|
||||
|
||||
export function resourceExists(resource: string, id: string): Promise<number> {
|
||||
const param = resource === "devices" ? "DeviceID.ID" : "_id";
|
||||
const filter = ["=", ["PARAM", param], id];
|
||||
const filter = new Expression.Binary(
|
||||
"=",
|
||||
new Expression.Parameter(Path.parse(param)),
|
||||
new Expression.Literal(id),
|
||||
);
|
||||
|
||||
return xhrRequest({
|
||||
method: "HEAD",
|
||||
url:
|
||||
`api/${resource}/?` +
|
||||
m.buildQueryString({
|
||||
filter: memoizedStringify(filter),
|
||||
filter: filter.toString(),
|
||||
}),
|
||||
extract: (xhr) => {
|
||||
if (xhr.status === 403) throw new Error("Not authorized");
|
||||
@@ -636,12 +676,16 @@ export function resourceExists(resource: string, id: string): Promise<number> {
|
||||
});
|
||||
}
|
||||
|
||||
export function evaluateExpression(exp: Expression): Expression;
|
||||
export function evaluateExpression(
|
||||
exp: Expression,
|
||||
obj: Record<string, unknown>,
|
||||
): Expression.Literal;
|
||||
export function evaluateExpression(
|
||||
exp: Expression,
|
||||
obj?: Record<string, unknown>,
|
||||
): Expression {
|
||||
if (!Array.isArray(exp)) return exp;
|
||||
return memoizedEvaluate(exp, obj, fulfillTimestamp + clockSkew);
|
||||
return memoizedEvaluate(exp, fulfillTimestamp + clockSkew, obj);
|
||||
}
|
||||
|
||||
export function changePassword(
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as store from "./store.ts";
|
||||
import { yaml } from "./dynamic-loader.ts";
|
||||
import * as configFunctions from "./config-functions.ts";
|
||||
import codeEditorComponent from "./code-editor-component.ts";
|
||||
import { parse } from "../lib/common/expression/parser.ts";
|
||||
import Expression from "../lib/common/expression.ts";
|
||||
|
||||
function putActionHandler(prefix: string[], dataYaml: string): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -27,7 +27,7 @@ function putActionHandler(prefix: string[], dataYaml: string): Promise<any> {
|
||||
}
|
||||
|
||||
// Try parse to ensure valid expressions
|
||||
for (const v of Object.values(updated)) parse(v as string);
|
||||
for (const v of Object.values(updated)) Expression.parse(v as string);
|
||||
|
||||
store
|
||||
.queryConfig(`${prefix.join(".")}.%`)
|
||||
|
||||
+34
-18
@@ -1,6 +1,6 @@
|
||||
import { Children, ClosureComponent, Component } from "mithril";
|
||||
import { m } from "./components.ts";
|
||||
import config from "./config.ts";
|
||||
import { pageSize as PAGE_SIZE } from "./config.ts";
|
||||
import * as store from "./store.ts";
|
||||
import * as notifications from "./notifications.ts";
|
||||
import memoize from "../lib/common/memoize.ts";
|
||||
@@ -8,13 +8,10 @@ import putFormComponent from "./put-form-component.ts";
|
||||
import indexTableComponent from "./index-table-component.ts";
|
||||
import * as overlay from "./overlay.ts";
|
||||
import * as smartQuery from "./smart-query.ts";
|
||||
import { map, parse, stringify } from "../lib/common/expression/parser.ts";
|
||||
import Expression from "../lib/common/expression.ts";
|
||||
import filterComponent from "./filter-component.ts";
|
||||
import changePasswordComponent from "./change-password-component.ts";
|
||||
|
||||
const PAGE_SIZE = config.ui.pageSize || 10;
|
||||
|
||||
const memoizedParse = memoize(parse);
|
||||
const memoizedJsonParse = memoize(JSON.parse);
|
||||
|
||||
const attributes = [
|
||||
@@ -22,10 +19,22 @@ const attributes = [
|
||||
{ id: "roles", label: "Roles", type: "multi", options: [] },
|
||||
];
|
||||
|
||||
const unpackSmartQuery = memoize((query) => {
|
||||
return map(query, (e) => {
|
||||
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "Q")
|
||||
return smartQuery.unpack("users", e[2], e[3]);
|
||||
const unpackSmartQuery = memoize((query: Expression) => {
|
||||
return query.evaluate((e) => {
|
||||
if (e instanceof Expression.FunctionCall) {
|
||||
if (e.name === "Q") {
|
||||
if (
|
||||
e.args[0] instanceof Expression.Literal &&
|
||||
e.args[1] instanceof Expression.Literal
|
||||
) {
|
||||
return smartQuery.unpack(
|
||||
"users",
|
||||
e.args[0].value as string,
|
||||
e.args[1].value as string,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return e;
|
||||
});
|
||||
});
|
||||
@@ -120,7 +129,7 @@ const getDownloadUrl = memoize((filter) => {
|
||||
for (const attr of attributes) cols[attr.label] = attr.id;
|
||||
|
||||
return `api/users.csv?${m.buildQueryString({
|
||||
filter: stringify(filter),
|
||||
filter: filter.toString(),
|
||||
columns: JSON.stringify(cols),
|
||||
})}`;
|
||||
});
|
||||
@@ -134,8 +143,11 @@ export function init(
|
||||
);
|
||||
}
|
||||
|
||||
const sort = args.hasOwnProperty("sort") ? "" + args["sort"] : "";
|
||||
const filter = args.hasOwnProperty("filter") ? "" + args["filter"] : "";
|
||||
let filter: Expression = null;
|
||||
let sort: Record<string, number> = null;
|
||||
if (args.hasOwnProperty("filter"))
|
||||
filter = Expression.parse(args["filter"] as string);
|
||||
if (args.hasOwnProperty("sort")) sort = JSON.parse(args["sort"] as string);
|
||||
return Promise.resolve({ filter, sort });
|
||||
}
|
||||
|
||||
@@ -151,7 +163,9 @@ export const component: ClosureComponent = (): Component => {
|
||||
}
|
||||
|
||||
function onFilterChanged(filter): void {
|
||||
const ops = { filter };
|
||||
const ops = {};
|
||||
if (!(filter instanceof Expression.Literal && filter.value))
|
||||
ops["filter"] = filter.toString();
|
||||
if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"];
|
||||
m.route.set("/users", ops);
|
||||
}
|
||||
@@ -176,10 +190,9 @@ export const component: ClosureComponent = (): Component => {
|
||||
m.route.set("/users", ops);
|
||||
}
|
||||
|
||||
let filter = vnode.attrs["filter"]
|
||||
? memoizedParse(vnode.attrs["filter"])
|
||||
: true;
|
||||
filter = unpackSmartQuery(filter);
|
||||
const filter = unpackSmartQuery(
|
||||
vnode.attrs["filter"] ?? new Expression.Literal(true),
|
||||
);
|
||||
|
||||
const users = store.fetch("users", filter, {
|
||||
limit: vnode.state["showCount"] || PAGE_SIZE,
|
||||
@@ -189,7 +202,10 @@ export const component: ClosureComponent = (): Component => {
|
||||
const count = store.count("users", filter);
|
||||
|
||||
// Getting the roles
|
||||
const permissions = store.fetch("permissions", true);
|
||||
const permissions = store.fetch(
|
||||
"permissions",
|
||||
new Expression.Literal(true),
|
||||
);
|
||||
if (permissions.fulfilled) {
|
||||
for (const attr of attributes) {
|
||||
if (attr.id === "roles")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ClosureComponent, Component, Children } from "mithril";
|
||||
import { m } from "./components.ts";
|
||||
import config from "./config.ts";
|
||||
import { pageSize as PAGE_SIZE } from "./config.ts";
|
||||
import filterComponent from "./filter-component.ts";
|
||||
import * as store from "./store.ts";
|
||||
import * as notifications from "./notifications.ts";
|
||||
@@ -9,12 +9,9 @@ import putFormComponent from "./put-form-component.ts";
|
||||
import indexTableComponent from "./index-table-component.ts";
|
||||
import * as overlay from "./overlay.ts";
|
||||
import * as smartQuery from "./smart-query.ts";
|
||||
import { map, parse, stringify } from "../lib/common/expression/parser.ts";
|
||||
import Expression from "../lib/common/expression.ts";
|
||||
import { loadCodeMirror } from "./dynamic-loader.ts";
|
||||
|
||||
const PAGE_SIZE = config.ui.pageSize || 10;
|
||||
|
||||
const memoizedParse = memoize(parse);
|
||||
const memoizedJsonParse = memoize(JSON.parse);
|
||||
|
||||
const attributes = [
|
||||
@@ -22,10 +19,22 @@ const attributes = [
|
||||
{ id: "script", label: "Script", type: "code" },
|
||||
];
|
||||
|
||||
const unpackSmartQuery = memoize((query) => {
|
||||
return map(query, (e) => {
|
||||
if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "Q")
|
||||
return smartQuery.unpack("virtualParameters", e[2], e[3]);
|
||||
const unpackSmartQuery = memoize((query: Expression) => {
|
||||
return query.evaluate((e) => {
|
||||
if (e instanceof Expression.FunctionCall) {
|
||||
if (e.name === "Q") {
|
||||
if (
|
||||
e.args[0] instanceof Expression.Literal &&
|
||||
e.args[1] instanceof Expression.Literal
|
||||
) {
|
||||
return smartQuery.unpack(
|
||||
"virtualParameters",
|
||||
e.args[0].value as string,
|
||||
e.args[1].value as string,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return e;
|
||||
});
|
||||
});
|
||||
@@ -104,7 +113,7 @@ const getDownloadUrl = memoize((filter) => {
|
||||
const cols = {};
|
||||
for (const attr of attributes) cols[attr.label] = attr.id;
|
||||
return `api/virtualParameters.csv?${m.buildQueryString({
|
||||
filter: stringify(filter),
|
||||
filter: filter.toString(),
|
||||
columns: JSON.stringify(cols),
|
||||
})}`;
|
||||
});
|
||||
@@ -118,8 +127,11 @@ export function init(
|
||||
);
|
||||
}
|
||||
|
||||
const sort = args.hasOwnProperty("sort") ? "" + args["sort"] : "";
|
||||
const filter = args.hasOwnProperty("filter") ? "" + args["filter"] : "";
|
||||
let filter: Expression = null;
|
||||
let sort: Record<string, number> = null;
|
||||
if (args.hasOwnProperty("filter"))
|
||||
filter = Expression.parse(args["filter"] as string);
|
||||
if (args.hasOwnProperty("sort")) sort = JSON.parse(args["sort"] as string);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
loadCodeMirror()
|
||||
@@ -142,7 +154,9 @@ export const component: ClosureComponent = (): Component => {
|
||||
}
|
||||
|
||||
function onFilterChanged(filter): void {
|
||||
const ops = { filter };
|
||||
const ops = {};
|
||||
if (!(filter instanceof Expression.Literal && filter.value))
|
||||
ops["filter"] = filter.toString();
|
||||
if (vnode.attrs["sort"]) ops["sort"] = vnode.attrs["sort"];
|
||||
m.route.set("/virtualParameters", ops);
|
||||
}
|
||||
@@ -164,10 +178,9 @@ export const component: ClosureComponent = (): Component => {
|
||||
m.route.set("/virtualParameters", ops);
|
||||
}
|
||||
|
||||
let filter = vnode.attrs["filter"]
|
||||
? memoizedParse(vnode.attrs["filter"])
|
||||
: true;
|
||||
filter = unpackSmartQuery(filter);
|
||||
const filter = unpackSmartQuery(
|
||||
vnode.attrs["filter"] ?? new Expression.Literal(true),
|
||||
);
|
||||
|
||||
const virtualParameters = store.fetch("virtualParameters", filter, {
|
||||
limit: vnode.state["showCount"] || PAGE_SIZE,
|
||||
|
||||
Reference in new issue
Block a user