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
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user