Implement faults page
This commit is contained in:
1 parent
b24c3626ac
commit
3cfac28fb1
16 files changed
+643
-246
No files matched your search
+3
-1
@@ -8,6 +8,7 @@ import * as overviewPage from "./overview-page";
|
||||
import * as devicesPage from "./devices-page";
|
||||
import * as devicePage from "./device-page";
|
||||
import * as errorPage from "./error-page";
|
||||
import * as faultsPage from "./faults-page";
|
||||
|
||||
let state;
|
||||
|
||||
@@ -65,5 +66,6 @@ function pagify(pageName, page) {
|
||||
m.route(document.body, "/overview", {
|
||||
"/overview": pagify("overview", overviewPage),
|
||||
"/devices": pagify("devices", devicesPage),
|
||||
"/devices/:id": pagify("devices", devicePage)
|
||||
"/devices/:id": pagify("devices", devicePage),
|
||||
"/faults": pagify("faults", faultsPage)
|
||||
});
|
||||
@@ -6,6 +6,7 @@ import parameterTable from "./components/parameter-table";
|
||||
import overviewDot from "./components/overview-dot";
|
||||
import container from "./components/container";
|
||||
import summonButton from "./components/summon-button";
|
||||
import deviceFaults from "./components/device-faults";
|
||||
|
||||
function get(name) {
|
||||
if (name === "parameter") return parameter;
|
||||
@@ -14,6 +15,7 @@ function get(name) {
|
||||
else if (name === "overview-dot") return overviewDot;
|
||||
else if (name === "container") return container;
|
||||
else if (name === "summon-button") return summonButton;
|
||||
else if (name === "device-faults") return deviceFaults;
|
||||
else throw new Error(`No such component '${name}'`);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -43,8 +43,9 @@ const component = {
|
||||
m(
|
||||
"td",
|
||||
m(
|
||||
"a.delete",
|
||||
"button",
|
||||
{
|
||||
title: "Delete this instance",
|
||||
onclick: () => {
|
||||
taskQueue.queueTask({
|
||||
name: "deleteObject",
|
||||
@@ -53,12 +54,18 @@ const component = {
|
||||
});
|
||||
}
|
||||
},
|
||||
"−"
|
||||
"✕"
|
||||
)
|
||||
)
|
||||
);
|
||||
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(
|
||||
@@ -67,8 +74,9 @@ const component = {
|
||||
m(
|
||||
"td",
|
||||
m(
|
||||
"a.add",
|
||||
"button",
|
||||
{
|
||||
title: "Create a new instance",
|
||||
onclick: () => {
|
||||
taskQueue.queueTask({
|
||||
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));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -14,8 +14,9 @@ const component = {
|
||||
let edit;
|
||||
if (param.writable)
|
||||
edit = m(
|
||||
"a.edit",
|
||||
"button",
|
||||
{
|
||||
title: "Edit parameter value",
|
||||
onclick: () => {
|
||||
taskQueue.stageSpv({
|
||||
name: "setParameterValues",
|
||||
|
||||
@@ -8,9 +8,11 @@ import * as notifications from "../notifications";
|
||||
const component = {
|
||||
view: vnode => {
|
||||
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 task = {
|
||||
name: "getParameterValues",
|
||||
@@ -37,7 +39,8 @@ const component = {
|
||||
else notifications.push("success", `${deviceId}: Summoned`);
|
||||
})
|
||||
.then(() => {
|
||||
store.fulfill(Date.now(), Date.now());
|
||||
e.target.disabled = false;
|
||||
store.fulfill(0, Date.now());
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
+48
-79
@@ -11,6 +11,8 @@
|
||||
--color5: #002f2f;
|
||||
|
||||
--fade: 0.2s;
|
||||
|
||||
--disabled: #666;
|
||||
}
|
||||
|
||||
body,
|
||||
@@ -83,6 +85,7 @@ i {
|
||||
}
|
||||
|
||||
& > .status {
|
||||
display: flex;
|
||||
& > span {
|
||||
margin-right: 1em;
|
||||
}
|
||||
@@ -95,7 +98,7 @@ i {
|
||||
& > .stale {
|
||||
color: #777;
|
||||
}
|
||||
& > .faulty {
|
||||
& > .fault {
|
||||
color: #700;
|
||||
}
|
||||
& > .active {
|
||||
@@ -103,14 +106,8 @@ i {
|
||||
}
|
||||
|
||||
& > .actions {
|
||||
float: right;
|
||||
& a {
|
||||
float: right;
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
& > .commit {
|
||||
font-weight: bold;
|
||||
}
|
||||
flex-grow: 1;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,28 +129,17 @@ i {
|
||||
}
|
||||
& .actions {
|
||||
text-align: right;
|
||||
& a {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& > .queue {
|
||||
& a.retry {
|
||||
float: right;
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
& a.cancel {
|
||||
float: right;
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
& > .pending {
|
||||
color: #770;
|
||||
}
|
||||
& > .stale {
|
||||
color: #777;
|
||||
}
|
||||
& > .faulty {
|
||||
& > .fault {
|
||||
color: #700;
|
||||
}
|
||||
& .parameter {
|
||||
@@ -172,6 +158,13 @@ i {
|
||||
vertical-align: text-bottom;
|
||||
white-space: nowrap;
|
||||
}
|
||||
& > div {
|
||||
display: flex;
|
||||
& > .actions {
|
||||
flex-grow: 1;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
& > .notifications-wrapper {
|
||||
@@ -263,24 +256,29 @@ table.table {
|
||||
& td {
|
||||
margin: 0;
|
||||
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 {
|
||||
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 {
|
||||
@@ -314,40 +312,28 @@ input::-moz-focus-inner {
|
||||
|
||||
input[type="button"],
|
||||
button {
|
||||
display: inline-block;
|
||||
min-width: 6em;
|
||||
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;
|
||||
|
||||
background: transparent;
|
||||
border: 0;
|
||||
color: var(--color4);
|
||||
border-radius: 4px;
|
||||
transition: background-color var(--fade), color var(--fade);
|
||||
|
||||
&:link,
|
||||
&:visited {
|
||||
color: var(--color4);
|
||||
background-color: var(--color1);
|
||||
&.primary {
|
||||
border: 1px solid var(--color1);
|
||||
background-color: var(--color3);
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
color: var(--color1);
|
||||
&.critical {
|
||||
border: 1px solid var(--color1);
|
||||
background-color: var(--color3);
|
||||
font-weight: bold;
|
||||
color: #900;
|
||||
}
|
||||
&:hover:enabled {
|
||||
background-color: var(--color4);
|
||||
color: var(--color3);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: var(--color1);
|
||||
color: var(--color2);
|
||||
color: var(--disabled);
|
||||
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 {
|
||||
& > svg {
|
||||
margin-bottom: -0.125em;
|
||||
|
||||
+86
-26
@@ -16,51 +16,102 @@ function init(args) {
|
||||
});
|
||||
}
|
||||
|
||||
function renderTable(devices, parameters, total, showMoreCallback) {
|
||||
let labels = [];
|
||||
function renderTable(
|
||||
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));
|
||||
|
||||
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(
|
||||
m(
|
||||
"tr",
|
||||
parameters
|
||||
.map(p => {
|
||||
const attrs = Object.assign({ device: device }, p);
|
||||
const comp = m(components.get(attrs.type || "parameter"), attrs);
|
||||
return m("td", comp);
|
||||
})
|
||||
.concat(
|
||||
m(
|
||||
"td.table-row-links",
|
||||
m(
|
||||
"a",
|
||||
{
|
||||
href: `#!/devices/${encodeURIComponent(
|
||||
device["DeviceID.ID"].value[0]
|
||||
)}`
|
||||
},
|
||||
"Show"
|
||||
)
|
||||
)
|
||||
{
|
||||
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 comp = m(components.get(attrs.type || "parameter"), attrs);
|
||||
return m("td", comp);
|
||||
}),
|
||||
m(
|
||||
"td.table-row-links",
|
||||
m(
|
||||
"a",
|
||||
{
|
||||
href: `#!/devices/${encodeURIComponent(
|
||||
device["DeviceID.ID"].value[0]
|
||||
)}`
|
||||
},
|
||||
"Show"
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!rows.length)
|
||||
rows.push(
|
||||
m("tr.empty", m("td", { colspan: parameters.length + 1 }, "No devices"))
|
||||
);
|
||||
|
||||
let footerElements = [];
|
||||
if (total != null) footerElements.push(`${devices.length}/${total}`);
|
||||
else footerElements.push(`${devices.length}`);
|
||||
|
||||
if (devices.length < total)
|
||||
footerElements.push(m("a", { onclick: showMoreCallback }, "More"));
|
||||
footerElements.push(
|
||||
m(
|
||||
"button",
|
||||
{
|
||||
title: "Show more devices",
|
||||
onclick: showMoreCallback,
|
||||
disabled: devices.length >= total || !devicesResponse.fulfilled
|
||||
},
|
||||
"More"
|
||||
)
|
||||
);
|
||||
|
||||
let tfoot = m(
|
||||
"tfoot",
|
||||
m("tr", m("td", { colspan: labels.length }, footerElements))
|
||||
);
|
||||
|
||||
return m("table.table", [
|
||||
return m("table.table.highlight", [
|
||||
m("thead", m("tr", labels)),
|
||||
m("tbody", rows),
|
||||
tfoot
|
||||
@@ -89,16 +140,25 @@ const component = {
|
||||
);
|
||||
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 [
|
||||
m("h1", "Listing devices"),
|
||||
m(filterComponent, {
|
||||
predefined: Object.values(config.get("ui.filters")),
|
||||
filter: vnode.attrs.filter ? vnode.attrs.filter.ast : null,
|
||||
onChange: onFilterChanged
|
||||
}),
|
||||
renderTable(
|
||||
devs.value,
|
||||
devs,
|
||||
Object.values(config.get("ui.index")),
|
||||
count.value,
|
||||
selected,
|
||||
showMore
|
||||
)
|
||||
];
|
||||
|
||||
+123
-93
@@ -44,9 +44,17 @@ function renderStaging(staging) {
|
||||
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(
|
||||
m(
|
||||
@@ -71,22 +79,14 @@ function renderQueue(queue) {
|
||||
for (let [k, v] of Object.entries(devices)) {
|
||||
details.push(m("strong", k));
|
||||
for (let t of v) {
|
||||
let actions = [
|
||||
m(
|
||||
"a.cancel",
|
||||
{
|
||||
onclick: () => {
|
||||
taskQueue.deleteTask(t);
|
||||
}
|
||||
},
|
||||
"✕"
|
||||
)
|
||||
];
|
||||
let actions = [];
|
||||
|
||||
if (t.status === "fault" || t.status === "stale")
|
||||
actions.push(
|
||||
m(
|
||||
"a.retry",
|
||||
"button",
|
||||
{
|
||||
title: "Retry this task",
|
||||
onclick: () => {
|
||||
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")
|
||||
details.push(
|
||||
m(
|
||||
`div.${t.status}`,
|
||||
actions,
|
||||
"Set ",
|
||||
mparam(t.parameterValues[0][0]),
|
||||
" to '",
|
||||
mval(t.parameterValues[0][1]),
|
||||
"'"
|
||||
m(
|
||||
"span",
|
||||
"Set ",
|
||||
mparam(t.parameterValues[0][0]),
|
||||
" to '",
|
||||
mval(t.parameterValues[0][1]),
|
||||
"'"
|
||||
),
|
||||
m(".actions", actions)
|
||||
)
|
||||
);
|
||||
else if (t.name === "refreshObject")
|
||||
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")
|
||||
details.push(m(`div.${t.status}`, actions, "Reboot"));
|
||||
details.push(m(`div.${t.status}`, "Reboot", m(".actions", actions)));
|
||||
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")
|
||||
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")
|
||||
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")
|
||||
details.push(
|
||||
m(
|
||||
`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,71 +228,71 @@ const component = {
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
let actions;
|
||||
if (queueElements.length)
|
||||
if (statusCount.queued) {
|
||||
actions = m(
|
||||
".actions",
|
||||
m("a.clear", { onclick: taskQueue.clear }, "Clear"),
|
||||
m(
|
||||
"a.commit",
|
||||
{
|
||||
onclick: () => {
|
||||
let tasks = Array.from(taskQueue.getQueue()).filter(
|
||||
t => t.status === "queued"
|
||||
);
|
||||
taskQueue
|
||||
.commit(
|
||||
tasks,
|
||||
(deviceId, connectionRequestStatus, tasks2) => {
|
||||
if (connectionRequestStatus !== "OK")
|
||||
notifications.push(
|
||||
"error",
|
||||
`${deviceId}: ${connectionRequestStatus}`
|
||||
);
|
||||
else
|
||||
for (let t of tasks2)
|
||||
if (t.status === "stale") {
|
||||
notifications.push(
|
||||
"error",
|
||||
`${deviceId}: No contact from device`
|
||||
);
|
||||
return;
|
||||
} else if (t.status === "fault") {
|
||||
notifications.push(
|
||||
"error",
|
||||
`${deviceId}: Task(s) faulted`
|
||||
);
|
||||
return;
|
||||
}
|
||||
let actions = m(
|
||||
".actions",
|
||||
m(
|
||||
"button.primary",
|
||||
{
|
||||
title: "Commit queued tasks",
|
||||
disabled: !statusCount.queued,
|
||||
onclick: () => {
|
||||
let tasks = Array.from(taskQueue.getQueue()).filter(
|
||||
t => t.status === "queued"
|
||||
);
|
||||
taskQueue
|
||||
.commit(tasks, (deviceId, connectionRequestStatus, tasks2) => {
|
||||
if (connectionRequestStatus !== "OK") {
|
||||
notifications.push(
|
||||
"error",
|
||||
`${deviceId}: ${connectionRequestStatus}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
notifications.push(
|
||||
"success",
|
||||
`${deviceId}: Task(s) committed`
|
||||
);
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
store.fulfill(Date.now(), Date.now());
|
||||
})
|
||||
.catch(err => {
|
||||
notifications.push("error", err.message);
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
},
|
||||
"Commit"
|
||||
)
|
||||
);
|
||||
} else {
|
||||
actions = m(
|
||||
".actions",
|
||||
m("a.clear", { onclick: taskQueue.clear }, "Clear")
|
||||
);
|
||||
}
|
||||
for (let t of tasks2)
|
||||
if (t.status === "stale") {
|
||||
notifications.push(
|
||||
"error",
|
||||
`${deviceId}: No contact from device`
|
||||
);
|
||||
return;
|
||||
} else if (t.status === "fault") {
|
||||
notifications.push(
|
||||
"error",
|
||||
`${deviceId}: Task(s) faulted`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
notifications.push(
|
||||
"success",
|
||||
`${deviceId}: Task(s) committed`
|
||||
);
|
||||
})
|
||||
.then(() => {
|
||||
store.fulfill(0, Date.now());
|
||||
})
|
||||
.catch(err => {
|
||||
notifications.push("error", err.message);
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
},
|
||||
"Commit"
|
||||
),
|
||||
m(
|
||||
"button",
|
||||
{
|
||||
title: "Clear tasks",
|
||||
onclick: taskQueue.clear,
|
||||
disabled: !queueElements.length
|
||||
},
|
||||
"Clear"
|
||||
)
|
||||
);
|
||||
|
||||
statusElement = m(
|
||||
".status",
|
||||
@@ -277,9 +307,9 @@ const component = {
|
||||
`Pending: ${statusCount.pending}`
|
||||
),
|
||||
m(
|
||||
"span.faulty",
|
||||
{ class: statusCount.faulty ? "active" : "" },
|
||||
`Faulty: ${statusCount.faulty}`
|
||||
"span.fault",
|
||||
{ class: statusCount.fault ? "active" : "" },
|
||||
`Fault: ${statusCount.fault}`
|
||||
),
|
||||
m(
|
||||
"span.stale",
|
||||
|
||||
@@ -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,16 +2,14 @@
|
||||
|
||||
import m from "mithril";
|
||||
|
||||
import * as config from "./config";
|
||||
import * as filterParser from "../common/filter-parser";
|
||||
|
||||
const component = {
|
||||
view: vnode => {
|
||||
const predefined = vnode.attrs.predefined || [];
|
||||
let a = m(
|
||||
"datalist#filters",
|
||||
Object.values(config.get("ui.filters")).map(f =>
|
||||
m("option", { value: `${f.parameter} = ` })
|
||||
)
|
||||
predefined.map(f => m("option", { value: `${f.parameter} = ` }))
|
||||
);
|
||||
|
||||
if (vnode.attrs.filter !== vnode.state.filter) {
|
||||
|
||||
@@ -18,6 +18,11 @@ const menu = {
|
||||
"li",
|
||||
{ class: active["devices"] },
|
||||
m("a", { href: "#!/devices" }, "Devices")
|
||||
),
|
||||
m(
|
||||
"li",
|
||||
{ class: active["faults"] },
|
||||
m("a", { href: "#!/faults" }, "Faults")
|
||||
)
|
||||
])
|
||||
);
|
||||
|
||||
+49
-19
@@ -20,7 +20,7 @@ const queries = {
|
||||
};
|
||||
|
||||
const resources = {};
|
||||
for (let r of ["devices"])
|
||||
for (let r of ["devices", "faults"])
|
||||
resources[r] = {
|
||||
objects: new Map(),
|
||||
count: new Map(),
|
||||
@@ -127,12 +127,15 @@ function fulfill(accessTimestamp, _fulfillTimestamp) {
|
||||
const allPromises = [];
|
||||
|
||||
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.accessed.get(queryResponse) >= accessTimestamp &&
|
||||
!(fulfillTimestamp <= queries.fulfilled.get(queryResponse))
|
||||
) {
|
||||
|
||||
if (!(fulfillTimestamp <= queries.fulfilled.get(queryResponse))) {
|
||||
queries.fulfilling.add(queryResponse);
|
||||
allPromises.push(
|
||||
new Promise((resolve, reject) => {
|
||||
@@ -165,13 +168,15 @@ function fulfill(accessTimestamp, _fulfillTimestamp) {
|
||||
let toFetchAll = {};
|
||||
|
||||
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.accessed.get(queryResponse) >= accessTimestamp &&
|
||||
!(fulfillTimestamp <= queries.fulfilled.get(queryResponse))
|
||||
) {
|
||||
if (!(fulfillTimestamp <= queries.fulfilled.get(queryResponse))) {
|
||||
queries.fulfilling.add(queryResponse);
|
||||
toFetchAll[resourceType] = toFetchAll[resourceType] || [];
|
||||
toFetchAll[resourceType].push(queryResponse);
|
||||
@@ -244,6 +249,9 @@ function fulfill(accessTimestamp, _fulfillTimestamp) {
|
||||
if (!combinedFilter) return resolve(updated);
|
||||
|
||||
updated = true;
|
||||
let deleted = new Set();
|
||||
if (!resources[resourceType].combinedFilter)
|
||||
deleted = new Set(resources[resourceType].objects.keys());
|
||||
let combinedFilterDiff = combinedFilter;
|
||||
if (resources[resourceType].combinedFilter)
|
||||
combinedFilterDiff = combinedFilterDiff.and(
|
||||
@@ -265,13 +273,20 @@ function fulfill(accessTimestamp, _fulfillTimestamp) {
|
||||
})
|
||||
})
|
||||
.then(res => {
|
||||
for (let r of res)
|
||||
if (resourceType === "devices")
|
||||
resources.devices.objects.set(
|
||||
r["DeviceID.ID"].value[0],
|
||||
r
|
||||
);
|
||||
else resources[resourceType].objects.set(r["_id"], r);
|
||||
for (let r of res) {
|
||||
const id =
|
||||
resourceType === "devices"
|
||||
? r["DeviceID.ID"].value[0]
|
||||
: r["_id"];
|
||||
resources[resourceType].objects.set(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) {
|
||||
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
|
||||
};
|
||||
Reference in new issue
Block a user