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:
Zaid Humaimidi committed 2026-03-12 14:58:20 -07:00
1 parent 75908e624f
commit 6f3ce6f7c8
71 files changed
+4045 -3449

No files matched your search

+57 -35
View File
@@ -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
View File
@@ -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(
+4 -4
View File
@@ -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)
+13 -6
View File
@@ -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",
+5 -3
View File
@@ -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);
+12 -9
View File
@@ -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;
+7 -3
View File
@@ -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),
);
+40 -33
View File
@@ -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
View File
@@ -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"]],
],
});
},
+14 -6
View File
@@ -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
+5 -4
View File
@@ -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;