Implement faults page

This commit is contained in:
Zaid Abdulla committed 2018-12-26 00:22:07 -08:00
1 parent b24c3626ac
commit 3cfac28fb1
16 files changed
+583 -186

No files matched your search

+3 -1
View File
@@ -8,6 +8,7 @@ import * as overviewPage from "./overview-page";
import * as devicesPage from "./devices-page"; import * as devicesPage from "./devices-page";
import * as devicePage from "./device-page"; import * as devicePage from "./device-page";
import * as errorPage from "./error-page"; import * as errorPage from "./error-page";
import * as faultsPage from "./faults-page";
let state; let state;
@@ -65,5 +66,6 @@ function pagify(pageName, page) {
m.route(document.body, "/overview", { m.route(document.body, "/overview", {
"/overview": pagify("overview", overviewPage), "/overview": pagify("overview", overviewPage),
"/devices": pagify("devices", devicesPage), "/devices": pagify("devices", devicesPage),
"/devices/:id": pagify("devices", devicePage) "/devices/:id": pagify("devices", devicePage),
"/faults": pagify("faults", faultsPage)
}); });
+2
View File
@@ -6,6 +6,7 @@ import parameterTable from "./components/parameter-table";
import overviewDot from "./components/overview-dot"; import overviewDot from "./components/overview-dot";
import container from "./components/container"; import container from "./components/container";
import summonButton from "./components/summon-button"; import summonButton from "./components/summon-button";
import deviceFaults from "./components/device-faults";
function get(name) { function get(name) {
if (name === "parameter") return parameter; if (name === "parameter") return parameter;
@@ -14,6 +15,7 @@ function get(name) {
else if (name === "overview-dot") return overviewDot; else if (name === "overview-dot") return overviewDot;
else if (name === "container") return container; else if (name === "container") return container;
else if (name === "summon-button") return summonButton; else if (name === "summon-button") return summonButton;
else if (name === "device-faults") return deviceFaults;
else throw new Error(`No such component '${name}'`); else throw new Error(`No such component '${name}'`);
} }
+71
View File
@@ -0,0 +1,71 @@
"use strict";
import m from "mithril";
import * as store from "../store";
import Filter from "../../common/filter";
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",
new Filter([
"AND",
[">", "_id", `${deviceId}:`],
["<", "_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 (let f of faults.value)
rows.push([
m("td", f["channel"]),
m("td", f["code"]),
m("td", f["message"]),
m("td", f["retries"]),
m("td", f["timestamp"]),
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);
});
}
},
"✕"
)
)
]);
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("h2", "Faults"), m("table.table", thead, tbody)];
}
};
export default component;
+13 -5
View File
@@ -43,8 +43,9 @@ const component = {
m( m(
"td", "td",
m( m(
"a.delete", "button",
{ {
title: "Delete this instance",
onclick: () => { onclick: () => {
taskQueue.queueTask({ taskQueue.queueTask({
name: "deleteObject", name: "deleteObject",
@@ -53,12 +54,18 @@ const component = {
}); });
} }
}, },
"−" "✕"
) )
) )
); );
rows.push(m("tr", row)); 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) if (device[object].writable === true)
rows.push( rows.push(
m( m(
@@ -67,8 +74,9 @@ const component = {
m( m(
"td", "td",
m( m(
"a.add", "button",
{ {
title: "Create a new instance",
onclick: () => { onclick: () => {
taskQueue.queueTask({ taskQueue.queueTask({
name: "addObject", name: "addObject",
@@ -77,12 +85,12 @@ const component = {
}); });
} }
}, },
"+" "🞢"
) )
) )
) )
); );
return m("table.parameter-table", thead, m("tbody", rows)); return m("table.table", thead, m("tbody", rows));
} }
}; };
+2 -1
View File
@@ -14,8 +14,9 @@ const component = {
let edit; let edit;
if (param.writable) if (param.writable)
edit = m( edit = m(
"a.edit", "button",
{ {
title: "Edit parameter value",
onclick: () => { onclick: () => {
taskQueue.stageSpv({ taskQueue.stageSpv({
name: "setParameterValues", name: "setParameterValues",
+6 -3
View File
@@ -8,9 +8,11 @@ import * as notifications from "../notifications";
const component = { const component = {
view: vnode => { view: vnode => {
return m( return m(
"a.summon", "button.primary",
{ {
onclick: () => { title: "Initiate session and refresh basic parameters",
onclick: e => {
e.target.disabled = true;
const params = Object.values(vnode.attrs.parameters); const params = Object.values(vnode.attrs.parameters);
const task = { const task = {
name: "getParameterValues", name: "getParameterValues",
@@ -37,7 +39,8 @@ const component = {
else notifications.push("success", `${deviceId}: Summoned`); else notifications.push("success", `${deviceId}: Summoned`);
}) })
.then(() => { .then(() => {
store.fulfill(Date.now(), Date.now()); e.target.disabled = false;
store.fulfill(0, Date.now());
}); });
} }
}, },
+48 -79
View File
@@ -11,6 +11,8 @@
--color5: #002f2f; --color5: #002f2f;
--fade: 0.2s; --fade: 0.2s;
--disabled: #666;
} }
body, body,
@@ -83,6 +85,7 @@ i {
} }
& > .status { & > .status {
display: flex;
& > span { & > span {
margin-right: 1em; margin-right: 1em;
} }
@@ -95,7 +98,7 @@ i {
& > .stale { & > .stale {
color: #777; color: #777;
} }
& > .faulty { & > .fault {
color: #700; color: #700;
} }
& > .active { & > .active {
@@ -103,14 +106,8 @@ i {
} }
& > .actions { & > .actions {
float: right; flex-grow: 1;
& a { text-align: right;
float: right;
margin-left: 0.5em;
}
& > .commit {
font-weight: bold;
}
} }
} }
@@ -132,28 +129,17 @@ i {
} }
& .actions { & .actions {
text-align: right; text-align: right;
& a {
margin-left: 0.5em;
}
} }
} }
& > .queue { & > .queue {
& a.retry {
float: right;
margin-left: 0.5em;
}
& a.cancel {
float: right;
margin-left: 0.5em;
}
& > .pending { & > .pending {
color: #770; color: #770;
} }
& > .stale { & > .stale {
color: #777; color: #777;
} }
& > .faulty { & > .fault {
color: #700; color: #700;
} }
& .parameter { & .parameter {
@@ -172,6 +158,13 @@ i {
vertical-align: text-bottom; vertical-align: text-bottom;
white-space: nowrap; white-space: nowrap;
} }
& > div {
display: flex;
& > .actions {
flex-grow: 1;
text-align: right;
}
}
} }
} }
& > .notifications-wrapper { & > .notifications-wrapper {
@@ -263,24 +256,29 @@ table.table {
& td { & td {
margin: 0; margin: 0;
padding: 0.2em 0.5em; padding: 0.2em 0.5em;
border-bottom: 1px dotted var(--color1);
& .action a {
visibility: hidden;
}
}
& tbody tr:hover td {
& .action {
visibility: visible;
}
transition: background-color var(--fade);
background-color: var(--color3);
} }
& tfoot td > a { & tfoot td > a {
margin-left: 1em; margin-left: 1em;
} }
&.highlight > tbody > tr {
& > td {
border-bottom: 1px dotted var(--color1);
}
transition: background-color var(--fade);
&:hover {
background-color: var(--color3);
}
}
& tbody > tr.empty {
text-align: center;
color: var(--disabled);
&:hover {
background-color: initial;
}
}
} }
h1 { h1 {
@@ -314,40 +312,28 @@ input::-moz-focus-inner {
input[type="button"], input[type="button"],
button { button {
display: inline-block; background: transparent;
min-width: 6em; border: 0;
position: relative;
top: 0px;
vertical-align: baseline;
margin: 0 0.1em;
outline: none;
cursor: pointer;
text-align: center;
text-decoration: none;
padding: 0.1em 0.6em;
border-top-left-radius: 1em;
border-bottom-right-radius: 1em;
border-top-right-radius: 0.2em;
border-bottom-left-radius: 0.2em;
color: var(--color4); color: var(--color4);
border-radius: 4px;
transition: background-color var(--fade), color var(--fade); transition: background-color var(--fade), color var(--fade);
&.primary {
&:link, border: 1px solid var(--color1);
&:visited { background-color: var(--color3);
color: var(--color4);
background-color: var(--color1);
} }
&.critical {
&:hover, border: 1px solid var(--color1);
&:active { background-color: var(--color3);
color: var(--color1); font-weight: bold;
color: #900;
}
&:hover:enabled {
background-color: var(--color4); background-color: var(--color4);
color: var(--color3);
} }
&:disabled { &:disabled {
background-color: var(--color1); color: var(--disabled);
color: var(--color2); cursor: not-allowed;
} }
} }
@@ -399,23 +385,6 @@ table.parameter-list {
} }
} }
table.parameter-table {
border-spacing: 0;
text-align: left;
& th {
color: var(--color4);
border-bottom: 2px solid var(--color4);
margin: 0;
padding: 0.2em 0.5em;
}
& td {
margin: 0;
padding: 0.2em 0.5em;
}
}
.overview-dot { .overview-dot {
& > svg { & > svg {
margin-bottom: -0.125em; margin-bottom: -0.125em;
+72 -12
View File
@@ -16,22 +16,59 @@ function init(args) {
}); });
} }
function renderTable(devices, parameters, total, showMoreCallback) { function renderTable(
let labels = []; devicesResponse,
parameters,
total,
selected,
showMoreCallback
) {
const devices = devicesResponse.value;
const selectAll = m("input", {
type: "checkbox",
checked: devices.length && selected.size === devices.length,
onchange: e => {
for (let d of devices)
if (e.target.checked) selected.add(d["DeviceID.ID"].value[0]);
else selected.delete(d["DeviceID.ID"].value[0]);
},
disabled: !total
});
let labels = [m("th", selectAll)];
for (let param of parameters) labels.push(m("th", param.label)); for (let param of parameters) labels.push(m("th", param.label));
let rows = []; let rows = [];
for (let device of devices) for (let device of devices) {
let checkbox = m("input", {
type: "checkbox",
checked: selected.has(device["DeviceID.ID"].value[0]),
onchange: e => {
if (e.target.checked) selected.add(device["DeviceID.ID"].value[0]);
else selected.delete(device["DeviceID.ID"].value[0]);
}
});
rows.push( rows.push(
m( m(
"tr", "tr",
parameters {
.map(p => { onclick: e => {
if (["INPUT", "BUTTON", "A"].includes(e.target.nodeName)) {
e.redraw = false;
return;
}
if (!selected.delete(device["DeviceID.ID"].value[0]))
selected.add(device["DeviceID.ID"].value[0]);
}
},
m("td", checkbox),
parameters.map(p => {
const attrs = Object.assign({ device: device }, p); const attrs = Object.assign({ device: device }, p);
const comp = m(components.get(attrs.type || "parameter"), attrs); const comp = m(components.get(attrs.type || "parameter"), attrs);
return m("td", comp); return m("td", comp);
}) }),
.concat(
m( m(
"td.table-row-links", "td.table-row-links",
m( m(
@@ -45,22 +82,36 @@ function renderTable(devices, parameters, total, showMoreCallback) {
) )
) )
) )
) );
}
if (!rows.length)
rows.push(
m("tr.empty", m("td", { colspan: parameters.length + 1 }, "No devices"))
); );
let footerElements = []; let footerElements = [];
if (total != null) footerElements.push(`${devices.length}/${total}`); if (total != null) footerElements.push(`${devices.length}/${total}`);
else footerElements.push(`${devices.length}`); else footerElements.push(`${devices.length}`);
if (devices.length < total) footerElements.push(
footerElements.push(m("a", { onclick: showMoreCallback }, "More")); m(
"button",
{
title: "Show more devices",
onclick: showMoreCallback,
disabled: devices.length >= total || !devicesResponse.fulfilled
},
"More"
)
);
let tfoot = m( let tfoot = m(
"tfoot", "tfoot",
m("tr", m("td", { colspan: labels.length }, footerElements)) m("tr", m("td", { colspan: labels.length }, footerElements))
); );
return m("table.table", [ return m("table.table.highlight", [
m("thead", m("tr", labels)), m("thead", m("tr", labels)),
m("tbody", rows), m("tbody", rows),
tfoot tfoot
@@ -89,16 +140,25 @@ const component = {
); );
let count = store.count("devices", vnode.attrs.filter); let count = store.count("devices", vnode.attrs.filter);
let selected = new Set();
if (vnode.state.selected)
for (let d of devs.value)
if (vnode.state.selected.has(d["DeviceID.ID"].value[0]))
selected.add(d["DeviceID.ID"].value[0]);
vnode.state.selected = selected;
return [ return [
m("h1", "Listing devices"), m("h1", "Listing devices"),
m(filterComponent, { m(filterComponent, {
predefined: Object.values(config.get("ui.filters")),
filter: vnode.attrs.filter ? vnode.attrs.filter.ast : null, filter: vnode.attrs.filter ? vnode.attrs.filter.ast : null,
onChange: onFilterChanged onChange: onFilterChanged
}), }),
renderTable( renderTable(
devs.value, devs,
Object.values(config.get("ui.index")), Object.values(config.get("ui.index")),
count.value, count.value,
selected,
showMore showMore
) )
]; ];
+77 -47
View File
@@ -44,9 +44,17 @@ function renderStaging(staging) {
vnode.dom.parentNode.parentNode.scrollTop = 0; vnode.dom.parentNode.parentNode.scrollTop = 0;
} }
}); });
const queue = m("a", { onclick: queueFunc }, "Queue");
const cancel = m("a", { onclick: cancelFunc }, "Cancel"); const queue = m(
"button.primary",
{ title: "Queue task", onclick: queueFunc },
"Queue"
);
const cancel = m(
"button",
{ title: "Cancel edit", onclick: cancelFunc },
"Cancel"
);
elements.push( elements.push(
m( m(
@@ -71,22 +79,14 @@ function renderQueue(queue) {
for (let [k, v] of Object.entries(devices)) { for (let [k, v] of Object.entries(devices)) {
details.push(m("strong", k)); details.push(m("strong", k));
for (let t of v) { for (let t of v) {
let actions = [ let actions = [];
m(
"a.cancel",
{
onclick: () => {
taskQueue.deleteTask(t);
}
},
"✕"
)
];
if (t.status === "fault" || t.status === "stale") if (t.status === "fault" || t.status === "stale")
actions.push( actions.push(
m( m(
"a.retry", "button",
{ {
title: "Retry this task",
onclick: () => { onclick: () => {
taskQueue.queueTask(t); taskQueue.queueTask(t);
} }
@@ -95,43 +95,73 @@ function renderQueue(queue) {
) )
); );
actions.push(
m(
"button",
{
title: "Remove this task",
onclick: () => {
taskQueue.deleteTask(t);
}
},
"✕"
)
);
if (t.name === "setParameterValues") if (t.name === "setParameterValues")
details.push( details.push(
m( m(
`div.${t.status}`, `div.${t.status}`,
actions, m(
"span",
"Set ", "Set ",
mparam(t.parameterValues[0][0]), mparam(t.parameterValues[0][0]),
" to '", " to '",
mval(t.parameterValues[0][1]), mval(t.parameterValues[0][1]),
"'" "'"
),
m(".actions", actions)
) )
); );
else if (t.name === "refreshObject") else if (t.name === "refreshObject")
details.push( details.push(
m(`div.${t.status}`, actions, "Refresh ", mparam(t.parameterName)) m(
`div.${t.status}`,
m("span", "Refresh ", mparam(t.parameterName)),
m(".actions", actions)
)
); );
else if (t.name === "reboot") else if (t.name === "reboot")
details.push(m(`div.${t.status}`, actions, "Reboot")); details.push(m(`div.${t.status}`, "Reboot", m(".actions", actions)));
else if (t.name === "factoryReset") else if (t.name === "factoryReset")
details.push(m(`div.${t.status}`, actions, "Factory reset")); details.push(
m(`div.${t.status}`, "Factory reset", m(".actions", actions))
);
else if (t.name === "addObject") else if (t.name === "addObject")
details.push( details.push(
m(`div.${t.status}`, actions, "Add ", mparam(t.objectName)) m(
`div.${t.status}`,
m("span", "Add ", mparam(t.objectName)),
m(".actions", actions)
)
); );
else if (t.name === "deleteObject") else if (t.name === "deleteObject")
details.push( details.push(
m(`div.${t.status}`, actions, "Delete ", mparam(t.objectName)) m(
`div.${t.status}`,
m("span", "Delete ", mparam(t.objectName)),
m(".actions", actions)
)
); );
else if (t.name === "getParameterValues") else if (t.name === "getParameterValues")
details.push( details.push(
m( m(
`div.${t.status}`, `div.${t.status}`,
actions, `Refresh ${t.parameterNames.length} parameters`,
`Refresh ${t.parameterNames.length} parameters` m(".actions", actions)
) )
); );
else details.push(m(`div.${t.status}`, actions, t.name)); else details.push(m(`div.${t.status}`, t.name, m(".actions", actions)));
} }
} }
@@ -198,32 +228,30 @@ const component = {
} }
if (stagingElements.length + queueElements.length) { if (stagingElements.length + queueElements.length) {
const statusCount = { queued: 0, pending: 0, faulty: 0, stale: 0 }; const statusCount = { queued: 0, pending: 0, fault: 0, stale: 0 };
for (let t of queue) statusCount[t.status] += 1; for (let t of queue) statusCount[t.status] += 1;
let actions; let actions = m(
if (queueElements.length)
if (statusCount.queued) {
actions = m(
".actions", ".actions",
m("a.clear", { onclick: taskQueue.clear }, "Clear"),
m( m(
"a.commit", "button.primary",
{ {
title: "Commit queued tasks",
disabled: !statusCount.queued,
onclick: () => { onclick: () => {
let tasks = Array.from(taskQueue.getQueue()).filter( let tasks = Array.from(taskQueue.getQueue()).filter(
t => t.status === "queued" t => t.status === "queued"
); );
taskQueue taskQueue
.commit( .commit(tasks, (deviceId, connectionRequestStatus, tasks2) => {
tasks, if (connectionRequestStatus !== "OK") {
(deviceId, connectionRequestStatus, tasks2) => {
if (connectionRequestStatus !== "OK")
notifications.push( notifications.push(
"error", "error",
`${deviceId}: ${connectionRequestStatus}` `${deviceId}: ${connectionRequestStatus}`
); );
else return;
}
for (let t of tasks2) for (let t of tasks2)
if (t.status === "stale") { if (t.status === "stale") {
notifications.push( notifications.push(
@@ -243,10 +271,9 @@ const component = {
"success", "success",
`${deviceId}: Task(s) committed` `${deviceId}: Task(s) committed`
); );
} })
)
.then(() => { .then(() => {
store.fulfill(Date.now(), Date.now()); store.fulfill(0, Date.now());
}) })
.catch(err => { .catch(err => {
notifications.push("error", err.message); notifications.push("error", err.message);
@@ -255,14 +282,17 @@ const component = {
} }
}, },
"Commit" "Commit"
),
m(
"button",
{
title: "Clear tasks",
onclick: taskQueue.clear,
disabled: !queueElements.length
},
"Clear"
) )
); );
} else {
actions = m(
".actions",
m("a.clear", { onclick: taskQueue.clear }, "Clear")
);
}
statusElement = m( statusElement = m(
".status", ".status",
@@ -277,9 +307,9 @@ const component = {
`Pending: ${statusCount.pending}` `Pending: ${statusCount.pending}`
), ),
m( m(
"span.faulty", "span.fault",
{ class: statusCount.faulty ? "active" : "" }, { class: statusCount.fault ? "active" : "" },
`Faulty: ${statusCount.faulty}` `Fault: ${statusCount.fault}`
), ),
m( m(
"span.stale", "span.stale",
+188
View File
@@ -0,0 +1,188 @@
"use strict";
import m from "mithril";
import filterComponent from "./filter-component";
import * as filterParser from "../common/filter-parser";
import Filter from "../common/filter";
import * as store from "./store";
import * as notifications from "./notifications";
function init(args) {
return new Promise(resolve => {
let filter = new Filter(args.filter);
resolve({ filter: filter });
});
}
function renderTable(faultsResponse, total, selected, showMoreCallback) {
const faults = faultsResponse.value;
const selectAll = m("input", {
type: "checkbox",
checked: faults.length && selected.size === faults.length,
onchange: e => {
for (let f of faults)
if (e.target.checked) selected.add(f["_id"]);
else selected.delete(f["_id"]);
},
disabled: !total
});
const labels = [
selectAll,
"Device",
"Channel",
"Code",
"Message",
"Retries",
"Timestamp"
].map(l => m("th", l));
let rows = [];
for (let f of faults) {
let checkbox = m("input", {
type: "checkbox",
checked: selected.has(f["_id"]),
onchange: e => {
if (e.target.checked) selected.add(f["_id"]);
else selected.delete(f["_id"]);
},
onclick: e => {
e.stopPropagation();
e.redraw = false;
}
});
const deviceHref = `#!/devices/${encodeURIComponent(f["device"])}`;
rows.push(
m(
"tr",
{
onclick: e => {
if (["INPUT", "BUTTON", "A"].includes(e.target.nodeName)) {
e.redraw = false;
return;
}
if (!selected.delete(f["_id"])) selected.add(f["_id"]);
}
},
m("td", checkbox),
m("td", m("a", { href: deviceHref }, f["device"])),
m("td", f["channel"]),
m("td", f["code"]),
m("td", f["message"]),
m("td", f["retries"]),
m("td", f["timestamp"])
)
);
}
if (!rows.length)
rows.push(m("tr.empty", m("td", { colspan: 7 }, "No faults")));
let footerElements = [];
if (total != null) footerElements.push(`${faults.length}/${total}`);
else footerElements.push(`${faults.length}`);
footerElements.push(
m(
"button",
{
title: "Show more faults",
onclick: showMoreCallback,
disabled: faults.length >= total || !faultsResponse.fulfilled
},
"More"
)
);
let tfoot = m(
"tfoot",
m("tr", m("td", { colspan: labels.length }, footerElements))
);
const buttons = [
m(
"button.primary",
{
title: "Delete selected faults",
disabled: !selected.size,
onclick: e => {
e.redraw = false;
e.target.disabled = true;
Promise.all(
Array.from(selected).map(id => store.deleteResource("faults", id))
)
.then(res => {
notifications.push("success", `${res.length} faults deleted`);
store.fulfill(0, Date.now());
})
.catch(err => {
notifications.push("error", err.message);
});
}
},
"Delete"
)
];
return [
m(
"table.table.highlight",
m("thead", m("tr", labels)),
m("tbody", rows),
tfoot
),
buttons
];
}
const component = {
view: vnode => {
document.title = "Faults - GenieACS";
function showMore() {
vnode.state.showCount = (vnode.state.showCount || 10) + 10;
m.redraw();
}
function onFilterChanged(filter) {
let params = {};
if (filter) params.filter = filterParser.stringify(filter);
m.route.set("/faults", params);
}
let faults = store.fetch(
"faults",
vnode.attrs.filter,
vnode.state.showCount || 10
);
let count = store.count("faults", vnode.attrs.filter);
let selected = new Set();
if (vnode.state.selected)
for (let f of faults.value)
if (vnode.state.selected.has(f["_id"])) selected.add(f["_id"]);
vnode.state.selected = selected;
return [
m("h1", "Listing faults"),
m(filterComponent, {
predefined: [
{ parameter: "device" },
{ parameter: "channel" },
{ parameter: "code" },
{ parameter: "retries" },
{ parameter: "timestamp" }
],
filter: vnode.attrs.filter ? vnode.attrs.filter.ast : null,
onChange: onFilterChanged
}),
renderTable(faults, count.value, selected, showMore)
];
}
};
export { init, component };
+2 -4
View File
@@ -2,16 +2,14 @@
import m from "mithril"; import m from "mithril";
import * as config from "./config";
import * as filterParser from "../common/filter-parser"; import * as filterParser from "../common/filter-parser";
const component = { const component = {
view: vnode => { view: vnode => {
const predefined = vnode.attrs.predefined || [];
let a = m( let a = m(
"datalist#filters", "datalist#filters",
Object.values(config.get("ui.filters")).map(f => predefined.map(f => m("option", { value: `${f.parameter} = ` }))
m("option", { value: `${f.parameter} = ` })
)
); );
if (vnode.attrs.filter !== vnode.state.filter) { if (vnode.attrs.filter !== vnode.state.filter) {
+5
View File
@@ -18,6 +18,11 @@ const menu = {
"li", "li",
{ class: active["devices"] }, { class: active["devices"] },
m("a", { href: "#!/devices" }, "Devices") m("a", { href: "#!/devices" }, "Devices")
),
m(
"li",
{ class: active["faults"] },
m("a", { href: "#!/faults" }, "Faults")
) )
]) ])
); );
+49 -19
View File
@@ -20,7 +20,7 @@ const queries = {
}; };
const resources = {}; const resources = {};
for (let r of ["devices"]) for (let r of ["devices", "faults"])
resources[r] = { resources[r] = {
objects: new Map(), objects: new Map(),
count: new Map(), count: new Map(),
@@ -127,12 +127,15 @@ function fulfill(accessTimestamp, _fulfillTimestamp) {
const allPromises = []; const allPromises = [];
for (let [resourceType, resource] of Object.entries(resources)) for (let [resourceType, resource] of Object.entries(resources))
for (let queryResponse of resource.count.values()) { for (let [queryResponseKey, queryResponse] of resource.count) {
if (!(queries.accessed.get(queryResponse) >= accessTimestamp)) {
resource.count.delete(queryResponseKey);
continue;
}
if (queries.fulfilling.has(queryResponse)) continue; if (queries.fulfilling.has(queryResponse)) continue;
if (
queries.accessed.get(queryResponse) >= accessTimestamp && if (!(fulfillTimestamp <= queries.fulfilled.get(queryResponse))) {
!(fulfillTimestamp <= queries.fulfilled.get(queryResponse))
) {
queries.fulfilling.add(queryResponse); queries.fulfilling.add(queryResponse);
allPromises.push( allPromises.push(
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
@@ -165,13 +168,15 @@ function fulfill(accessTimestamp, _fulfillTimestamp) {
let toFetchAll = {}; let toFetchAll = {};
for (let [resourceType, resource] of Object.entries(resources)) for (let [resourceType, resource] of Object.entries(resources))
for (let queryResponse of resource.fetch.values()) { for (let [queryResponseKey, queryResponse] of resource.fetch) {
if (!(queries.accessed.get(queryResponse) >= accessTimestamp)) {
resource.fetch.delete(queryResponseKey);
continue;
}
if (queries.fulfilling.has(queryResponse)) continue; if (queries.fulfilling.has(queryResponse)) continue;
if ( if (!(fulfillTimestamp <= queries.fulfilled.get(queryResponse))) {
queries.accessed.get(queryResponse) >= accessTimestamp &&
!(fulfillTimestamp <= queries.fulfilled.get(queryResponse))
) {
queries.fulfilling.add(queryResponse); queries.fulfilling.add(queryResponse);
toFetchAll[resourceType] = toFetchAll[resourceType] || []; toFetchAll[resourceType] = toFetchAll[resourceType] || [];
toFetchAll[resourceType].push(queryResponse); toFetchAll[resourceType].push(queryResponse);
@@ -244,6 +249,9 @@ function fulfill(accessTimestamp, _fulfillTimestamp) {
if (!combinedFilter) return resolve(updated); if (!combinedFilter) return resolve(updated);
updated = true; updated = true;
let deleted = new Set();
if (!resources[resourceType].combinedFilter)
deleted = new Set(resources[resourceType].objects.keys());
let combinedFilterDiff = combinedFilter; let combinedFilterDiff = combinedFilter;
if (resources[resourceType].combinedFilter) if (resources[resourceType].combinedFilter)
combinedFilterDiff = combinedFilterDiff.and( combinedFilterDiff = combinedFilterDiff.and(
@@ -265,13 +273,20 @@ function fulfill(accessTimestamp, _fulfillTimestamp) {
}) })
}) })
.then(res => { .then(res => {
for (let r of res) for (let r of res) {
if (resourceType === "devices") const id =
resources.devices.objects.set( resourceType === "devices"
r["DeviceID.ID"].value[0], ? r["DeviceID.ID"].value[0]
r : r["_id"];
); resources[resourceType].objects.set(id, r);
else resources[resourceType].objects.set(r["_id"], r); deleted.delete(id);
}
for (let d of deleted) {
const obj = resources[resourceType].objects.get(d);
if (combinedFilterDiff.test(obj))
resources[resourceType].objects.delete(d);
}
for (let queryResponse of toFetch) { for (let queryResponse of toFetch) {
let filter = queries.filter.get(queryResponse); let filter = queries.filter.get(queryResponse);
@@ -344,4 +359,19 @@ function postTasks(tasks, callback) {
}); });
} }
export { count, fetch, fulfill, unpackFilter, getTimestamp, postTasks }; function deleteResource(resourceType, id) {
return m.request({
method: "DELETE",
url: `/api/${resourceType}/${encodeURIComponent(id)}`
});
}
export {
count,
fetch,
fulfill,
unpackFilter,
getTimestamp,
postTasks,
deleteResource
};
+2 -1
View File
@@ -91,5 +91,6 @@
"ui.device.2.parameters.2.label": "IP address", "ui.device.2.parameters.2.label": "IP address",
"ui.device.2.parameters.2.parameter": "IPAddress", "ui.device.2.parameters.2.parameter": "IPAddress",
"ui.device.2.parameters.3.label": "MAC address", "ui.device.2.parameters.3.label": "MAC address",
"ui.device.2.parameters.3.parameter": "MACAddress" "ui.device.2.parameters.3.parameter": "MACAddress",
"ui.device.3.type": "device-faults"
} }
+22 -11
View File
@@ -265,7 +265,7 @@ function postTask(task, connectionRequest, callback) {
if ( if (
res.statusCode === 202 && res.statusCode === 202 &&
res.statusMessage !== "Task queued but not processed" && res.statusMessage !== "Task queued but not processed" &&
res.statusMessage !== "Task faulte" res.statusMessage !== "Task faulted"
) )
connectionRequestStatus = res.statusMessage; connectionRequestStatus = res.statusMessage;
} }
@@ -305,18 +305,28 @@ function postTasks(deviceId, tasks) {
connectionRequest: connectionRequestStatus, connectionRequest: connectionRequestStatus,
tasks: statuses tasks: statuses
}); });
Promise.all(
statuses.map(s => query("tasks", new Filter(["=", "_id", s._id]))) let promises2 = [];
for (let s of statuses) {
promises2.push(query("tasks", new Filter(["=", "_id", s._id])));
promises2.push(
query(
"faults",
new Filter(["=", "_id", `${deviceId}:task_${s._id}`])
) )
.then(res => { );
for (let [i, r] of res.entries()) {
if (!r[0]) {
statuses[i].status = "done";
} else if (r[0].fault) {
statuses[i].status = "fault";
statuses[i].fault = r[0].fault;
} }
deleteResource("tasks", statuses[i]._id);
Promise.all(promises2)
.then(res => {
for (let [i, r] of statuses.entries()) {
if (res[i * 2].length === 0) {
r.status = "done";
} else if (res[i * 2 + 1].length === 1) {
r.status = "fault";
r.fault = res[i * 2 + 1][0];
}
deleteResource("tasks", r._id);
} }
resolve({ resolve({
@@ -333,3 +343,4 @@ function postTasks(deviceId, tasks) {
exports.query = query; exports.query = query;
exports.count = count; exports.count = count;
exports.postTasks = postTasks; exports.postTasks = postTasks;
exports.deleteResource = deleteResource;
+21 -3
View File
@@ -13,6 +13,7 @@ const resources = {
provisions: true, provisions: true,
files: true, files: true,
virtual_parameters: true, virtual_parameters: true,
faults: true,
tasks: false tasks: false
}; };
@@ -22,6 +23,12 @@ for (let [resource, update] of Object.entries(resources)) {
if (ctx.request.query.filter) filter = new Filter(ctx.request.query.filter); if (ctx.request.query.filter) filter = new Filter(ctx.request.query.filter);
if (ctx.request.query.limit) limit = +ctx.request.query.limit; if (ctx.request.query.limit) limit = +ctx.request.query.limit;
// Exclude temporary tasks and faults
if (resource === "tasks" || resource === "faults") {
let f = new Filter(["NOT", ["<", "expiry", Date.now() + 60000]]);
filter = f.and(filter);
}
let count = await apiFunctions.count(resource, filter, limit); let count = await apiFunctions.count(resource, filter, limit);
ctx.set("X-Total-Count", count); ctx.set("X-Total-Count", count);
ctx.body = ""; ctx.body = "";
@@ -33,6 +40,12 @@ for (let [resource, update] of Object.entries(resources)) {
if (ctx.request.query.limit) limit = +ctx.request.query.limit; if (ctx.request.query.limit) limit = +ctx.request.query.limit;
if (ctx.request.query.skip) skip = +ctx.request.query.skip; if (ctx.request.query.skip) skip = +ctx.request.query.skip;
// Exclude temporary tasks and faults
if (resource === "tasks" || resource === "faults") {
let f = new Filter(["NOT", ["<", "expiry", Date.now() + 60000]]);
filter = f.and(filter);
}
ctx.body = await apiFunctions.query( ctx.body = await apiFunctions.query(
resource, resource,
filter, filter,
@@ -56,9 +69,14 @@ for (let [resource, update] of Object.entries(resources)) {
ctx.body = res[0]; ctx.body = res[0];
}); });
if (update) { // TODO add PUT, PATCH routes
// TODO add PUT, PATCH, DELETE routes if (update)
} router.delete(`/${resource}/:id`, async ctx => {
let found = await apiFunctions.deleteResource(resource, ctx.params.id);
if (found) ctx.status = 200;
else ctx.status = 404;
ctx.body = "";
});
} }
router.post("/devices/:id/tasks", async ctx => { router.post("/devices/:id/tasks", async ctx => {