Merge remote-tracking branch 'ui/master'

This commit is contained in:
Zaid Abdulla committed 2018-12-27 22:22:44 -08:00
commit 4530967b6b
70 files changed
+10951 -8

No files matched your search

+133
View File
@@ -0,0 +1,133 @@
"use strict";
import m from "mithril";
import * as components from "../components";
import * as taskQueue from "../task-queue";
import longTextComponent from "../long-text-component";
function escapeRegExp(str) {
return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&");
}
const component = {
view: vnode => {
const device = vnode.attrs.device;
const search = m("input", {
type: "text",
placeholder: "Search parameters",
oninput: e => {
vnode.state.searchString = e.target.value;
e.redraw = false;
clearTimeout(vnode.state.timeout);
vnode.state.timeout = setTimeout(m.redraw, 500);
}
});
const instanceRegex = /\.[0-9]+$/;
let re;
if (vnode.state.searchString) {
const keywords = vnode.state.searchString.split(" ").filter(s => s);
if (keywords.length)
re = new RegExp(keywords.map(s => escapeRegExp(s)).join(".*"), "i");
}
const rows = Object.keys(device)
.sort()
.map(k => {
const p = device[k];
const val = [];
const attrs = {};
if (re) {
const str = p.value && p.value[0] ? `${k} ${p.value[0]}` : k;
if (!re.test(str)) attrs.style = "display: none;";
}
if (p.object === false) {
val.push(
m(
components.get("parameter"),
Object.assign({ device: device, parameter: k })
)
);
} else if (p.object && p.writable) {
if (instanceRegex.test(k)) {
val.push(
m(
"button",
{
title: "Delete this instance",
onclick: () => {
taskQueue.queueTask({
name: "deleteObject",
device: device["DeviceID.ID"].value[0],
objectName: k
});
}
},
"✕"
)
);
} else {
val.push(
m(
"button",
{
title: "Create a new instance",
onclick: () => {
taskQueue.queueTask({
name: "addObject",
device: device["DeviceID.ID"].value[0],
objectName: k
});
}
},
"🞢"
)
);
}
}
val.push(
m(
"button",
{
title: "Refresh tree",
onclick: () => {
taskQueue.queueTask({
name: "getParameterValues",
device: device["DeviceID.ID"].value[0],
parameterNames: [k]
});
}
},
"↺"
)
);
return m(
"tr",
attrs,
m("td.left", m(longTextComponent, { text: k })),
m("td.right", val)
);
});
return m(
".all-parameters",
m(
"a.download-csv",
{
href: `/api/devices/${device["DeviceID.ID"].value[0]}.csv`,
download: "",
style: "float: right;"
},
"Download"
),
search,
m(".parameter-list", m("table", m("tbody", rows)))
);
}
};
export default component;
+28
View File
@@ -0,0 +1,28 @@
"use strict";
import m from "mithril";
import * as components from "../components";
import * as store from "../store";
import * as expression from "../../lib/common/expression";
import memoize from "../../lib/common/memoize";
const memoizedParse = memoize(expression.parse);
const component = {
view: vnode => {
if (vnode.attrs.filter) {
const filter = memoizedParse(vnode.attrs.filter);
if (!store.evaluateExpression(filter, vnode.attrs.device)) return null;
}
const children = Object.values(vnode.attrs.components).map(c => {
if (typeof c !== "object") return `${c}`;
const attrs = Object.assign({}, vnode.attrs, c);
return m(components.get(attrs.type), attrs);
});
if (vnode.attrs.element) return m(vnode.attrs.element, children);
else return children;
}
};
export default component;
+90
View File
@@ -0,0 +1,90 @@
"use strict";
import m from "mithril";
import * as taskQueue from "../task-queue";
import * as notifications from "../notifications";
import * as store from "../store";
const component = {
view: vnode => {
const device = vnode.attrs.device;
const buttons = [];
buttons.push(
m(
"button.primary",
{
title: "Reboot device",
onclick: () => {
taskQueue.queueTask({
name: "reboot",
device: device["DeviceID.ID"].value[0]
});
}
},
"Reboot"
)
);
buttons.push(
m(
"button.critical",
{
title: "Factory reset device",
onclick: () => {
taskQueue.queueTask({
name: "factoryReset",
device: device["DeviceID.ID"].value[0]
});
}
},
"Reset"
)
);
buttons.push(
m(
"button.critical",
{
title: "Push a firmware or a config file",
onclick: () => {
taskQueue.stageDownload({
name: "download",
device: device["DeviceID.ID"].value[0]
});
}
},
"Push file"
)
);
buttons.push(
m(
"button.primary",
{
title: "Delete device",
onclick: () => {
if (!confirm("Deleting this device. Are you sure?")) return;
const deviceId = device["DeviceID.ID"].value[0];
store
.deleteResource("devices", deviceId)
.then(() => {
notifications.push("success", `${deviceId}: Device deleted`);
m.route.set("/devices");
})
.catch(err => {
notifications.push("error", err.message);
});
}
},
"Delete"
)
);
return m(".actions-bar", buttons);
}
};
export default component;
+71
View File
@@ -0,0 +1,71 @@
"use strict";
import m from "mithril";
import * as store from "../store";
import * as notifications from "../notifications";
const component = {
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 headers = ["Channel", "Code", "Message", "Retries", "Timestamp"].map(
l => m("th", l)
);
const thead = m("thead", m("tr", headers));
const rows = [];
for (const f of faults.value) {
rows.push([
m("td", f["channel"]),
m("td", f["code"]),
m("td", f["message"]),
m("td", f["retries"]),
m("td", new Date(f["timestamp"]).toLocaleString()),
m(
"td",
m(
"button",
{
title: "Delete fault",
onclick: e => {
e.redraw = false;
store
.deleteResource("faults", f["_id"])
.then(() => {
notifications.push("success", "Fault deleted");
store.fulfill(Date.now(), Date.now());
m.redraw();
})
.catch(err => {
notifications.push("error", err.message);
store.fulfill(Date.now(), Date.now());
});
}
},
"✕"
)
)
]);
}
let tbody;
if (rows.length) {
tbody = m("tbody", rows.map(r => m("tr", r)));
} else {
tbody = m(
"tbody",
m("tr.empty", m("td", { colspan: headers.length }, "No faults"))
);
}
return m("table.table", thead, tbody);
}
};
export default component;
+29
View File
@@ -0,0 +1,29 @@
"use strict";
import m from "mithril";
import * as components from "../components";
const component = {
view: vnode => {
let deviceId;
if (vnode.attrs.device)
deviceId = vnode.attrs.device["DeviceID.ID"].value[0];
const children = Object.values(vnode.attrs.components).map(c => {
if (typeof c !== "object") return `${c}`;
const attrs = Object.assign({}, vnode.attrs, c);
return m(components.get(attrs.type), attrs);
});
if (deviceId) {
return m(
"a",
{ href: `#!/devices/${encodeURIComponent(deviceId)}` },
children
);
} else {
return children;
}
}
};
export default component;
+42
View File
@@ -0,0 +1,42 @@
"use strict";
import m from "mithril";
import config from "../config";
import * as store from "../store";
import * as expression from "../../lib/common/expression";
import memoize from "../../lib/common/memoize";
const memoizedParse = memoize(expression.parse);
const CHARTS = config.ui.overview.charts;
const component = {
view: vnode => {
const device = vnode.attrs.device;
const chart = CHARTS[vnode.attrs.chart];
for (const slice of Object.values(chart.slices)) {
const filter = memoizedParse(slice.filter);
if (store.evaluateExpression(filter, device)) {
const dot = m(
"svg",
{
width: "1em",
height: "1em",
xmlns: "http://www.w3.org/2000/svg",
"xmlns:xlink": "http://www.w3.org/1999/xlink"
},
m("circle", {
cx: "0.5em",
cy: "0.5em",
r: "0.4em",
fill: slice.color
})
);
return m("span.overview-dot", dot, `${slice.label}`);
}
}
return null;
}
};
export default component;
+37
View File
@@ -0,0 +1,37 @@
"use strict";
import m from "mithril";
import * as components from "../components";
import memoize from "../../lib/common/memoize";
const getChildAttrs = memoize((attrs, device) =>
Object.assign({}, attrs, { device: device })
);
const component = {
view: vnode => {
const device = vnode.attrs.device;
const rows = Object.values(vnode.attrs.parameters).map(parameter => {
const p = m(
components.get(parameter.type || "parameter"),
getChildAttrs(parameter, device)
);
return m(
"tr",
{
onupdate: vn => {
vn.dom.style.display = p.dom ? "" : "none";
}
},
m("th", parameter.label),
m("td", p)
);
});
return m("table.parameter-list", rows);
}
};
export default component;
+143
View File
@@ -0,0 +1,143 @@
"use strict";
import m from "mithril";
import * as components from "../components";
import * as taskQueue from "../task-queue";
import * as store from "../store";
import * as expression from "../../lib/common/expression";
import * as expressionParser from "../../lib/common/expression-parser";
import memoize from "../../lib/common/memoize";
const memoizeParse = memoize(expression.parse);
const component = {
oninit: vnode => {
const obj = expression.parse(vnode.attrs.parameter);
if (!Array.isArray(obj) || !obj[0] === "PARAM")
throw new Error("Object must be a parameter path");
vnode.state.object = obj[1];
vnode.state.parameters = Object.values(vnode.attrs.childParameters).map(
parameter => {
return Object.assign({}, parameter, {
parameter: expression.parse(parameter.parameter)
});
}
);
},
view: vnode => {
const device = vnode.attrs.device;
const object = store.evaluateExpression(vnode.state.object, device);
const parameters = vnode.state.parameters;
if (!device[object]) return null;
const instances = new Set();
const prefix = `${object}.`;
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));
}
}
const headers = Object.values(parameters).map(p => m("th", p.label));
const thead = m("thead", m("tr", headers));
const rows = [];
for (const i of instances) {
let filter =
vnode.attrs.filter != null ? memoizeParse(vnode.attrs.filter) : true;
filter = expressionParser.map(filter, e => {
if (Array.isArray(e) && e[0] === "PARAM")
return ["PARAM", ["||", i, ".", e[1]]];
return e;
});
if (!store.evaluateExpression(filter, device)) continue;
const row = parameters.map(p => {
const param = expressionParser.map(p.parameter, e => {
if (Array.isArray(e) && e[0] === "PARAM")
return ["PARAM", ["||", i, ".", e[1]]];
return e;
});
return m(
"td",
m(
components.get(p.type || "parameter"),
Object.assign({}, p, {
device: device,
parameter: expressionParser.stringify(param),
label: ""
})
)
);
});
if (device[i].writable === true) {
row.push(
m(
"td",
m(
"button",
{
title: "Delete this instance",
onclick: () => {
taskQueue.queueTask({
name: "deleteObject",
device: device["DeviceID.ID"].value[0],
objectName: i
});
}
},
"✕"
)
)
);
}
rows.push(m("tr", row));
}
if (!rows.length) {
rows.push(
m("tr.empty", m("td", { colspan: headers.length }, "No instances"))
);
}
if (device[object].writable === true) {
rows.push(
m(
"tr",
m("td", { colspan: headers.length }),
m(
"td",
m(
"button",
{
title: "Create a new instance",
onclick: () => {
taskQueue.queueTask({
name: "addObject",
device: device["DeviceID.ID"].value[0],
objectName: object
});
}
},
"🞢"
)
)
)
);
}
let label;
if (vnode.attrs.label) label = m("h2", vnode.attrs.label);
return [label, m("table.table", thead, m("tbody", rows))];
}
};
export default component;
+95
View File
@@ -0,0 +1,95 @@
"use strict";
import m from "mithril";
import * as taskQueue from "../task-queue";
import * as store from "../store";
import * as expression from "../../lib/common/expression";
import memoize from "../../lib/common/memoize";
import timeAgo from "../timeago";
import longTextComponent from "../long-text-component";
const memoizedParse = memoize(expression.parse);
const evaluateParam = memoize((exp, obj, now) => {
let timestamp = now;
const params = new Set();
const value = expression.evaluate(memoizedParse(exp), obj, now, e => {
if (Array.isArray(e)) {
if (e[0] === "PARAM") {
params.add(e[1]);
if (!Array.isArray(e[1])) {
const p = obj[e[1]];
if (p && p.valueTimestamp)
timestamp = Math.min(timestamp, p.valueTimestamp);
}
}
if (e[0] === "FUNC" && e[1] === "DATE_STRING" && !Array.isArray(e[2]))
return new Date(e[2]).toLocaleString();
}
return e;
});
let parameter = params.size === 1 ? params.values().next().value : null;
if (Array.isArray(parameter)) parameter = null;
return { value, timestamp, parameter };
});
const component = {
view: vnode => {
const device = vnode.attrs.device;
const { value, timestamp, parameter } = evaluateParam(
vnode.attrs.parameter,
vnode.attrs.device,
store.getTimestamp()
);
if (value == null) return null;
let edit;
if (device[parameter] && device[parameter].writable) {
edit = m(
"button",
{
title: "Edit parameter value",
onclick: () => {
taskQueue.stageSpv({
name: "setParameterValues",
device: device["DeviceID.ID"].value[0],
parameterValues: [
[
parameter,
device[parameter].value[0],
device[parameter].value[1]
]
]
});
}
},
"✎"
);
}
const el = m(longTextComponent, { text: `${value}` });
return m(
"span",
{
class: "parameter-value",
onmouseover: e => {
e.redraw = false;
// Don't update any child element
if (e.target === el.dom) {
const now = Date.now();
const localeString = new Date(timestamp).toLocaleString();
e.target.title = `${localeString} (${timeAgo(now - timestamp)})`;
}
}
},
el,
edit
);
}
};
export default component;
+56
View File
@@ -0,0 +1,56 @@
"use strict";
import m from "mithril";
import * as store from "../store";
const REFRESH_INTERVAL = 3000;
const component = {
oninit: vnode => {
vnode.state.timestamp = 0;
vnode.state.ping = null;
vnode.state.host = null;
vnode.state.timeout = null;
},
view: vnode => {
const refresh = () => {
const device = vnode.attrs.device;
let param =
device["InternetGatewayDevice.ManagementServer.ConnectionRequestURL"];
if (!param)
param = device["Device.ManagementServer.ConnectionRequestURL"];
if (!param || !param.value) return;
const url = new URL(param.value[0]);
vnode.state.host = url.hostname;
vnode.state.timestamp = Date.now();
store
.ping(vnode.state.host)
.then(res => {
vnode.state.ping = res;
})
.catch(() => {
// Do nothing
});
};
const t = vnode.state.timestamp + REFRESH_INTERVAL - Date.now();
clearTimeout(vnode.state.timeout);
if (t <= 0) refresh();
else vnode.state.timeout = setTimeout(refresh, t);
if (vnode.state.host) {
let status = "";
if (vnode.state.ping) {
if (vnode.state.ping.avg != null)
status = `${Math.trunc(vnode.state.ping.avg)} ms`;
else status = "Unreachable";
}
return m("div", `Pinging ${vnode.state.host}: ${status}`);
}
return null;
}
};
export default component;
+76
View File
@@ -0,0 +1,76 @@
"use strict";
import m from "mithril";
import * as taskQueue from "../task-queue";
import * as store from "../store";
import * as notifications from "../notifications";
import * as expression from "../../lib/common/expression";
import memoize from "../../lib/common/memoize";
const memoizedParse = memoize(expression.parse);
const component = {
view: vnode => {
const device = vnode.attrs.device;
return m(
"button.primary",
{
title: "Initiate session and refresh basic parameters",
onclick: e => {
e.target.disabled = true;
const params = Object.values(vnode.attrs.parameters)
.map(p => {
const exp = memoizedParse(p);
if (!Array.isArray(exp) || !exp[0] === "PARAM") return null;
return store.evaluateExpression(exp[1], device);
})
.filter(exp => !!exp);
const task = {
name: "getParameterValues",
parameterNames: params,
device: device["DeviceID.ID"].value[0]
};
taskQueue
.commit(
[task],
(deviceId, err, connectionRequestStatus, tasks2) => {
if (err) {
notifications.push("error", `${deviceId}: ${err.message}`);
return;
}
for (const t of tasks2)
if (t.status === "stale") taskQueue.deleteTask(t);
if (connectionRequestStatus !== "OK") {
notifications.push(
"error",
`${deviceId}: ${connectionRequestStatus}`
);
} else if (tasks2[0].status === "stale") {
notifications.push(
"error",
`${deviceId}: No contact from device`
);
} else if (tasks2[0].status === "fault") {
notifications.push("error", `${deviceId}: Refresh faulted`);
} else {
notifications.push("success", `${deviceId}: Summoned`);
}
}
)
.then(() => {
e.target.disabled = false;
store.fulfill(0, Date.now());
});
}
},
"Summon"
);
}
};
export default component;
+84
View File
@@ -0,0 +1,84 @@
"use strict";
import m from "mithril";
import * as notifications from "../notifications";
import * as store from "../store";
const component = {
view: vnode => {
const device = vnode.attrs.device;
let writable = true;
if (vnode.attrs.writable != null) writable = vnode.attrs.writable;
const tags = [];
for (const p of Object.keys(device))
if (p.startsWith("Tags.")) tags.push(p.slice(5));
tags.sort();
if (!writable) return m(".tags", tags.map(t => m("span.tag", t)));
return m(
".tags",
tags.map(tag =>
m(
"span.tag",
tag,
m(
"button",
{
onclick: e => {
e.target.disabled = true;
const deviceId = device["DeviceID.ID"].value[0];
store
.updateTags(deviceId, { [tag]: false })
.then(() => {
e.target.disabled = false;
notifications.push("success", `${deviceId}: Tags updated`);
store.fulfill(0, Date.now());
})
.catch(err => {
e.target.disabled = false;
notifications.push("error", `${deviceId}: ${err.message}`);
});
}
},
"✕"
)
)
),
m(
"span.tag.writable",
m(
"button",
{
onclick: e => {
e.target.disabled = true;
const deviceId = device["DeviceID.ID"].value[0];
const tag = prompt(`Enter tag to assign to device:`);
if (!tag) {
e.target.disabled = false;
return;
}
store
.updateTags(deviceId, { [tag]: true })
.then(() => {
e.target.disabled = false;
notifications.push("success", `${deviceId}: Tags updated`);
store.fulfill(0, Date.now());
})
.catch(err => {
e.target.disabled = false;
notifications.push("error", `${deviceId}: ${err.message}`);
});
}
},
"🞢"
)
)
);
}
};
export default component;