Implement task queue and notifications
This commit is contained in:
1 parent
9b827c5f5e
commit
b24c3626ac
20 files changed
+1000
-88
No files matched your search
@@ -4,12 +4,16 @@ import parameter from "./components/parameter";
|
||||
import parameterList from "./components/parameter-list";
|
||||
import parameterTable from "./components/parameter-table";
|
||||
import overviewDot from "./components/overview-dot";
|
||||
import container from "./components/container";
|
||||
import summonButton from "./components/summon-button";
|
||||
|
||||
function get(name) {
|
||||
if (name === "parameter") return parameter;
|
||||
else if (name === "parameter-list") return parameterList;
|
||||
else if (name === "parameter-table") return parameterTable;
|
||||
else if (name === "overview-dot") return overviewDot;
|
||||
else if (name === "container") return container;
|
||||
else if (name === "summon-button") return summonButton;
|
||||
else throw new Error(`No such component '${name}'`);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
"use strict";
|
||||
|
||||
import m from "mithril";
|
||||
import * as components from "../components";
|
||||
|
||||
const component = {
|
||||
view: vnode => {
|
||||
let children = Object.values(vnode.attrs.components).map(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;
|
||||
@@ -16,7 +16,10 @@ const component = {
|
||||
m("th", p.label),
|
||||
m(
|
||||
"td",
|
||||
m(components.get("parameter"), Object.assign({ device: device }, p))
|
||||
m(
|
||||
components.get(p.type || "parameter"),
|
||||
Object.assign({ device: device }, p)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import m from "mithril";
|
||||
import * as components from "../components";
|
||||
import * as taskQueue from "../task-queue";
|
||||
|
||||
const component = {
|
||||
view: vnode => {
|
||||
@@ -12,14 +13,18 @@ const component = {
|
||||
if (!device[object]) return null;
|
||||
|
||||
const instances = new Set();
|
||||
const prefix = `${object}.`;
|
||||
for (let p in device)
|
||||
if (p.startsWith(object))
|
||||
instances.add(p.slice(0, p.indexOf(".", object.length + 1)));
|
||||
if (p.startsWith(prefix)) {
|
||||
let 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 thead = m(
|
||||
"thead",
|
||||
m("tr", Object.values(parameters).map(p => m("th", p.label)))
|
||||
);
|
||||
const rows = [];
|
||||
for (let i of instances) {
|
||||
const row = Object.values(parameters).map(p =>
|
||||
@@ -33,8 +38,50 @@ const component = {
|
||||
)
|
||||
)
|
||||
);
|
||||
if (device[i].writable === true)
|
||||
row.push(
|
||||
m(
|
||||
"td",
|
||||
m(
|
||||
"a.delete",
|
||||
{
|
||||
onclick: () => {
|
||||
taskQueue.queueTask({
|
||||
name: "deleteObject",
|
||||
device: device["DeviceID.ID"].value[0],
|
||||
objectName: i
|
||||
});
|
||||
}
|
||||
},
|
||||
"−"
|
||||
)
|
||||
)
|
||||
);
|
||||
rows.push(m("tr", row));
|
||||
}
|
||||
if (device[object].writable === true)
|
||||
rows.push(
|
||||
m(
|
||||
"tr",
|
||||
m("td", { colspan: headers.length }),
|
||||
m(
|
||||
"td",
|
||||
m(
|
||||
"a.add",
|
||||
{
|
||||
onclick: () => {
|
||||
taskQueue.queueTask({
|
||||
name: "addObject",
|
||||
device: device["DeviceID.ID"].value[0],
|
||||
objectName: object
|
||||
});
|
||||
}
|
||||
},
|
||||
"+"
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
return m("table.parameter-table", thead, m("tbody", rows));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use strict";
|
||||
|
||||
import m from "mithril";
|
||||
import * as components from "../components";
|
||||
import * as taskQueue from "../task-queue";
|
||||
|
||||
const component = {
|
||||
view: vnode => {
|
||||
@@ -11,19 +11,30 @@ const component = {
|
||||
if (param.value[1] === "xsd:dateTime")
|
||||
value = new Date(value).toISOString();
|
||||
|
||||
let meta;
|
||||
if (vnode.attrs.meta) {
|
||||
const metaAttrs = Object.assign(
|
||||
{ device: vnode.attrs.device, parameter: vnode.attrs.parameter },
|
||||
vnode.attrs.meta
|
||||
let edit;
|
||||
if (param.writable)
|
||||
edit = m(
|
||||
"a.edit",
|
||||
{
|
||||
onclick: () => {
|
||||
taskQueue.stageSpv({
|
||||
name: "setParameterValues",
|
||||
device: vnode.attrs.device["DeviceID.ID"].value[0],
|
||||
parameterValues: [
|
||||
[vnode.attrs.parameter, param.value[0], param.value[1]]
|
||||
]
|
||||
});
|
||||
}
|
||||
},
|
||||
"✎"
|
||||
);
|
||||
meta = m("span", [
|
||||
" (",
|
||||
m(components.get(metaAttrs.type), metaAttrs),
|
||||
")"
|
||||
]);
|
||||
}
|
||||
return [m("span", { title: param.valueTimestamp }, value), meta];
|
||||
|
||||
return m(
|
||||
"span.parameter-value",
|
||||
{ title: param.valueTimestamp },
|
||||
value,
|
||||
edit
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
"use strict";
|
||||
|
||||
import m from "mithril";
|
||||
import * as taskQueue from "../task-queue";
|
||||
import * as store from "../store";
|
||||
import * as notifications from "../notifications";
|
||||
|
||||
const component = {
|
||||
view: vnode => {
|
||||
return m(
|
||||
"a.summon",
|
||||
{
|
||||
onclick: () => {
|
||||
const params = Object.values(vnode.attrs.parameters);
|
||||
const task = {
|
||||
name: "getParameterValues",
|
||||
parameterNames: params,
|
||||
device: vnode.attrs.device["DeviceID.ID"].value[0]
|
||||
};
|
||||
|
||||
taskQueue
|
||||
.commit([task], (deviceId, connectionRequestStatus, tasks2) => {
|
||||
for (let 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(() => {
|
||||
store.fulfill(Date.now(), Date.now());
|
||||
});
|
||||
}
|
||||
},
|
||||
"Summon"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default component;
|
||||
+169
-15
@@ -45,12 +45,157 @@ i {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
#header {
|
||||
background-color: #f2eddf;
|
||||
border-bottom: 1px solid var(--color1);
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
|
||||
& > .drawer-wrapper {
|
||||
position: fixed;
|
||||
width: 720px;
|
||||
margin-left: -360px;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
& > .drawer {
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
border-bottom-left-radius: 6px;
|
||||
border-bottom-right-radius: 6px;
|
||||
border-bottom: 1px solid var(--color1);
|
||||
border-left: 1px solid var(--color1);
|
||||
border-right: 1px solid var(--color1);
|
||||
margin-top: -1px;
|
||||
box-shadow: 0 0 10px #ccc;
|
||||
overflow: hidden;
|
||||
transition: height var(--fade), opacity var(--fade);
|
||||
|
||||
& > div {
|
||||
padding: 0.3em;
|
||||
}
|
||||
|
||||
& > .status {
|
||||
& > span {
|
||||
margin-right: 1em;
|
||||
}
|
||||
& > .pending {
|
||||
color: #770;
|
||||
&.active {
|
||||
animation: 0.5s linear 0s infinite alternate blink;
|
||||
}
|
||||
}
|
||||
& > .stale {
|
||||
color: #777;
|
||||
}
|
||||
& > .faulty {
|
||||
color: #700;
|
||||
}
|
||||
& > .active {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
& > .actions {
|
||||
float: right;
|
||||
& a {
|
||||
float: right;
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
& > .commit {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& > .staging {
|
||||
& .parameter {
|
||||
font-style: italic;
|
||||
max-width: 500px;
|
||||
text-overflow: ellipsis;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
|
||||
& input {
|
||||
width: 100%;
|
||||
padding: 0.3em;
|
||||
margin-top: 0.3em;
|
||||
margin-bottom: 0.3em;
|
||||
}
|
||||
& .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 {
|
||||
color: #700;
|
||||
}
|
||||
& .parameter {
|
||||
font-style: italic;
|
||||
max-width: 400px;
|
||||
text-overflow: ellipsis;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
& .value {
|
||||
max-width: 200px;
|
||||
text-overflow: ellipsis;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
vertical-align: text-bottom;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
& > .notifications-wrapper {
|
||||
& > .notification {
|
||||
padding: 0.3em;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
border: 1px solid var(--color1);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 0 10px #ccc;
|
||||
transition: opacity var(--fade), top var(--fade);
|
||||
|
||||
&.error {
|
||||
background-color: rgba(255, 200, 200, 0.8);
|
||||
border: 1px solid rgb(255, 200, 200);
|
||||
}
|
||||
&.success {
|
||||
background-color: rgba(200, 255, 200, 0.8);
|
||||
border: 1px solid rgb(200, 255, 200);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
& > nav {
|
||||
display: inline-block;
|
||||
overflow: visible;
|
||||
@@ -206,26 +351,29 @@ button {
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
display: block;
|
||||
border: 1px solid var(--color1);
|
||||
background-color: var(--color3);
|
||||
border-radius: 4px;
|
||||
transition: border-color var(--fade), background-color var(--fade);
|
||||
&.error {
|
||||
background-color: #efcbcf;
|
||||
border-color: #e6b2b7;
|
||||
}
|
||||
&:focus,
|
||||
&:hover {
|
||||
border-color: var(--color4);
|
||||
}
|
||||
}
|
||||
|
||||
.filter {
|
||||
margin-bottom: 0.5em;
|
||||
& > input {
|
||||
padding: 2px;
|
||||
width: 30em;
|
||||
font-size: 90%;
|
||||
display: block;
|
||||
border: 1px solid var(--color1);
|
||||
background-color: var(--color3);
|
||||
width: 30em;
|
||||
margin: 2px;
|
||||
border-radius: 4px;
|
||||
transition: border-color var(--fade), background-color var(--fade);
|
||||
&.error {
|
||||
background-color: #efcbcf;
|
||||
border-color: #e6b2b7;
|
||||
}
|
||||
&:focus,
|
||||
&:hover {
|
||||
border-color: var(--color4);
|
||||
}
|
||||
padding: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,6 +428,12 @@ table.parameter-table {
|
||||
}
|
||||
}
|
||||
|
||||
span.inform {
|
||||
& > * {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.pie-chart {
|
||||
margin: 20px;
|
||||
text-align: center;
|
||||
|
||||
@@ -19,7 +19,6 @@ function init(args) {
|
||||
function renderTable(devices, parameters, total, showMoreCallback) {
|
||||
let labels = [];
|
||||
for (let param of parameters) labels.push(m("th", param.label));
|
||||
labels.push(m("th"));
|
||||
|
||||
let rows = [];
|
||||
for (let device of devices)
|
||||
@@ -29,7 +28,7 @@ function renderTable(devices, parameters, total, showMoreCallback) {
|
||||
parameters
|
||||
.map(p => {
|
||||
const attrs = Object.assign({ device: device }, p);
|
||||
const comp = m(components.get("parameter"), attrs);
|
||||
const comp = m(components.get(attrs.type || "parameter"), attrs);
|
||||
return m("td", comp);
|
||||
})
|
||||
.concat(
|
||||
@@ -49,14 +48,16 @@ function renderTable(devices, parameters, total, showMoreCallback) {
|
||||
)
|
||||
);
|
||||
|
||||
let footerElements = [`${devices.length}/${total}`];
|
||||
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 }, "Show more"));
|
||||
footerElements.push(m("a", { onclick: showMoreCallback }, "More"));
|
||||
|
||||
let tfoot = m(
|
||||
"tfoot",
|
||||
m("tr", m("td", { colspan: labels.length + 1 }, footerElements))
|
||||
m("tr", m("td", { colspan: labels.length }, footerElements))
|
||||
);
|
||||
|
||||
return m("table.table", [
|
||||
|
||||
@@ -0,0 +1,344 @@
|
||||
"use strict";
|
||||
|
||||
import m from "mithril";
|
||||
|
||||
import * as taskQueue from "./task-queue";
|
||||
import * as store from "./store";
|
||||
import * as notifications from "./notifications";
|
||||
|
||||
function mparam(param) {
|
||||
return m("span.parameter", { title: param }, `${param}`);
|
||||
}
|
||||
|
||||
function mval(val) {
|
||||
return m("span.value", { title: val }, `${val}`);
|
||||
}
|
||||
|
||||
function renderStaging(staging) {
|
||||
let elements = [];
|
||||
for (let s of staging) {
|
||||
const queueFunc = () => {
|
||||
staging.delete(s);
|
||||
taskQueue.queueTask(s);
|
||||
};
|
||||
const cancelFunc = () => {
|
||||
staging.delete(s);
|
||||
};
|
||||
|
||||
const input = m("input", {
|
||||
value: s.parameterValues[0][1],
|
||||
oninput: e => {
|
||||
e.redraw = false;
|
||||
s.parameterValues[0][1] = input.dom.value;
|
||||
},
|
||||
onkeydown: e => {
|
||||
if (e.keyCode === 13) queueFunc();
|
||||
else if (e.keyCode === 27) cancelFunc();
|
||||
else e.redraw = false;
|
||||
},
|
||||
oncreate: vnode => {
|
||||
vnode.dom.focus();
|
||||
vnode.dom.select();
|
||||
// Need to prevent scrolling on focus because
|
||||
// we're animating height and using overflow: hidden
|
||||
vnode.dom.parentNode.parentNode.scrollTop = 0;
|
||||
}
|
||||
});
|
||||
const queue = m("a", { onclick: queueFunc }, "Queue");
|
||||
|
||||
const cancel = m("a", { onclick: cancelFunc }, "Cancel");
|
||||
|
||||
elements.push(
|
||||
m(
|
||||
".staging",
|
||||
m("span", "Editing ", mparam(s.parameterValues[0][0])),
|
||||
input,
|
||||
m("div.actions", queue, cancel)
|
||||
)
|
||||
);
|
||||
}
|
||||
return elements;
|
||||
}
|
||||
|
||||
function renderQueue(queue) {
|
||||
let details = [];
|
||||
let devices = {};
|
||||
for (let t of queue) {
|
||||
devices[t.device] = devices[t.device] || [];
|
||||
devices[t.device].push(t);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
},
|
||||
"✕"
|
||||
)
|
||||
];
|
||||
if (t.status === "fault" || t.status === "stale")
|
||||
actions.push(
|
||||
m(
|
||||
"a.retry",
|
||||
{
|
||||
onclick: () => {
|
||||
taskQueue.queueTask(t);
|
||||
}
|
||||
},
|
||||
"↺"
|
||||
)
|
||||
);
|
||||
|
||||
if (t.name === "setParameterValues")
|
||||
details.push(
|
||||
m(
|
||||
`div.${t.status}`,
|
||||
actions,
|
||||
"Set ",
|
||||
mparam(t.parameterValues[0][0]),
|
||||
" to '",
|
||||
mval(t.parameterValues[0][1]),
|
||||
"'"
|
||||
)
|
||||
);
|
||||
else if (t.name === "refreshObject")
|
||||
details.push(
|
||||
m(`div.${t.status}`, actions, "Refresh ", mparam(t.parameterName))
|
||||
);
|
||||
else if (t.name === "reboot")
|
||||
details.push(m(`div.${t.status}`, actions, "Reboot"));
|
||||
else if (t.name === "factoryReset")
|
||||
details.push(m(`div.${t.status}`, actions, "Factory reset"));
|
||||
else if (t.name === "addObject")
|
||||
details.push(
|
||||
m(`div.${t.status}`, actions, "Add ", mparam(t.objectName))
|
||||
);
|
||||
else if (t.name === "deleteObject")
|
||||
details.push(
|
||||
m(`div.${t.status}`, actions, "Delete ", mparam(t.objectName))
|
||||
);
|
||||
else if (t.name === "getParameterValues")
|
||||
details.push(
|
||||
m(
|
||||
`div.${t.status}`,
|
||||
actions,
|
||||
`Refresh ${t.parameterNames.length} parameters`
|
||||
)
|
||||
);
|
||||
else details.push(m(`div.${t.status}`, actions, t.name));
|
||||
}
|
||||
}
|
||||
|
||||
return details;
|
||||
}
|
||||
|
||||
function renderNotifications(notifs) {
|
||||
const notificationElements = [];
|
||||
|
||||
for (let n of notifs)
|
||||
notificationElements.push(
|
||||
m(
|
||||
"div.notification",
|
||||
{
|
||||
class: n.type,
|
||||
style: "position: absolute;opacity: 0",
|
||||
oncreate: vnode => {
|
||||
vnode.dom.style.opacity = 1;
|
||||
},
|
||||
onbeforeremove: vnode => {
|
||||
vnode.dom.style.opacity = 0;
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, 500);
|
||||
});
|
||||
},
|
||||
key: n.timestamp
|
||||
},
|
||||
n.message
|
||||
)
|
||||
);
|
||||
return notificationElements;
|
||||
}
|
||||
|
||||
const component = {
|
||||
view: vnode => {
|
||||
const queue = taskQueue.getQueue();
|
||||
const staging = taskQueue.getStaging();
|
||||
const notifs = notifications.getNotifications();
|
||||
|
||||
let drawerElement, statusElement;
|
||||
let notificationElements = renderNotifications(notifs);
|
||||
let stagingElements = renderStaging(staging);
|
||||
let queueElements = renderQueue(queue);
|
||||
|
||||
function repositionNotifications() {
|
||||
let top = 10;
|
||||
for (let c of notificationElements) {
|
||||
c.dom.style.top = top;
|
||||
top += c.dom.offsetHeight + 10;
|
||||
}
|
||||
}
|
||||
|
||||
function resizeDrawer() {
|
||||
let height = statusElement.dom.offsetTop + statusElement.dom.offsetHeight;
|
||||
if (stagingElements.length)
|
||||
for (let s of stagingElements)
|
||||
height = Math.max(height, s.dom.offsetTop + s.dom.offsetHeight);
|
||||
else if (vnode.state.mouseIn)
|
||||
for (let c of drawerElement.children)
|
||||
height = Math.max(height, c.dom.offsetTop + c.dom.offsetHeight);
|
||||
drawerElement.dom.style.height = height;
|
||||
}
|
||||
|
||||
if (stagingElements.length + queueElements.length) {
|
||||
const statusCount = { queued: 0, pending: 0, faulty: 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;
|
||||
}
|
||||
|
||||
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")
|
||||
);
|
||||
}
|
||||
|
||||
statusElement = m(
|
||||
".status",
|
||||
m(
|
||||
"span.queued",
|
||||
{ class: statusCount.queued ? "active" : "" },
|
||||
`Queued: ${statusCount.queued}`
|
||||
),
|
||||
m(
|
||||
"span.pending",
|
||||
{ class: statusCount.pending ? "active" : "" },
|
||||
`Pending: ${statusCount.pending}`
|
||||
),
|
||||
m(
|
||||
"span.faulty",
|
||||
{ class: statusCount.faulty ? "active" : "" },
|
||||
`Faulty: ${statusCount.faulty}`
|
||||
),
|
||||
m(
|
||||
"span.stale",
|
||||
{ class: statusCount.stale ? "active" : "" },
|
||||
`Stale: ${statusCount.stale}`
|
||||
),
|
||||
actions
|
||||
);
|
||||
|
||||
drawerElement = m(
|
||||
".drawer",
|
||||
{
|
||||
key: "drawer",
|
||||
style: "opacity: 0;height: 0;",
|
||||
oncreate: vnode2 => {
|
||||
vnode.state.mouseIn = false;
|
||||
vnode2.dom.style.opacity = 1;
|
||||
resizeDrawer();
|
||||
},
|
||||
onmouseover: e => {
|
||||
vnode.state.mouseIn = true;
|
||||
resizeDrawer();
|
||||
e.redraw = false;
|
||||
},
|
||||
onmouseleave: e => {
|
||||
vnode.state.mouseIn = false;
|
||||
resizeDrawer();
|
||||
e.redraw = false;
|
||||
},
|
||||
onupdate: resizeDrawer,
|
||||
onbeforeremove: vnode2 => {
|
||||
vnode2.dom.onmouseover = vnode2.dom.onmouseleave = null;
|
||||
vnode2.dom.style.opacity = 0;
|
||||
vnode2.dom.style.height = 0;
|
||||
return new Promise(resolve => {
|
||||
setTimeout(resolve, 500);
|
||||
});
|
||||
}
|
||||
},
|
||||
statusElement,
|
||||
stagingElements.length ? stagingElements : m(".queue", queueElements)
|
||||
);
|
||||
}
|
||||
|
||||
return m(
|
||||
"div.drawer-wrapper",
|
||||
drawerElement,
|
||||
m(
|
||||
"div.notifications-wrapper",
|
||||
{
|
||||
key: "notifications",
|
||||
style: "position: relative;",
|
||||
onupdate: repositionNotifications,
|
||||
oncreate: repositionNotifications
|
||||
},
|
||||
notificationElements
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default component;
|
||||
@@ -54,7 +54,7 @@ const component = {
|
||||
[m("b", "Filter"), a].concat(
|
||||
vnode.state.filterList.map((fltr, idx) => {
|
||||
return m("input", {
|
||||
type: "string",
|
||||
type: "text",
|
||||
list: "filters",
|
||||
class: `${(vnode.state.filterInvalid >> idx) & 1 ? "error" : ""}`,
|
||||
value: fltr,
|
||||
|
||||
+3
-1
@@ -3,13 +3,15 @@
|
||||
import m from "mithril";
|
||||
|
||||
import menu from "./menu";
|
||||
import drawerComponent from "./drawer-component";
|
||||
|
||||
const layout = {
|
||||
view: vnode => {
|
||||
return [
|
||||
m("#header", [
|
||||
m("img.logo", { src: "logo.svg" }),
|
||||
m(menu, { page: vnode.attrs.page })
|
||||
m(menu, { page: vnode.attrs.page }),
|
||||
m(drawerComponent)
|
||||
]),
|
||||
m("#content", { class: `page-${vnode.attrs.page}` }, [vnode.children])
|
||||
];
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
"use strict";
|
||||
|
||||
import m from "mithril";
|
||||
|
||||
const notifications = new Set();
|
||||
|
||||
function push(type, message) {
|
||||
const n = { type: type, message: message, timestamp: Date.now() };
|
||||
notifications.add(n);
|
||||
m.redraw();
|
||||
setTimeout(() => {
|
||||
notifications.delete(n);
|
||||
m.redraw();
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
function getNotifications() {
|
||||
return notifications;
|
||||
}
|
||||
|
||||
export { push, getNotifications };
|
||||
+41
-1
@@ -304,4 +304,44 @@ function getTimestamp() {
|
||||
return fulfillTimestamp;
|
||||
}
|
||||
|
||||
export { count, fetch, fulfill, unpackFilter, getTimestamp };
|
||||
function postTasks(tasks, callback) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let devices = {};
|
||||
for (let t of tasks) {
|
||||
t.status = "pending";
|
||||
devices[t.device] = devices[t.device] || [];
|
||||
devices[t.device].push(t);
|
||||
}
|
||||
|
||||
let promises = [];
|
||||
for (let [deviceId, tasks2] of Object.entries(devices))
|
||||
promises.push(
|
||||
m.request({
|
||||
method: "POST",
|
||||
url: `/api/devices/${encodeURIComponent(deviceId)}/tasks`,
|
||||
data: tasks2,
|
||||
extract: xhr => {
|
||||
const connectionRequestStatus = xhr.getResponseHeader(
|
||||
"Connection-Request"
|
||||
);
|
||||
let st = JSON.parse(xhr.response);
|
||||
for (let [i, t] of st.entries()) {
|
||||
tasks2[i]._id = t._id;
|
||||
tasks2[i].status = t.status;
|
||||
tasks2[i].fault = t.fault;
|
||||
}
|
||||
if (callback) callback(deviceId, connectionRequestStatus, tasks2);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
Promise.all(promises)
|
||||
.then(() => resolve(tasks))
|
||||
.catch(err => {
|
||||
callback = null;
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export { count, fetch, fulfill, unpackFilter, getTimestamp, postTasks };
|
||||
@@ -0,0 +1,62 @@
|
||||
"use strict";
|
||||
|
||||
import * as store from "./store";
|
||||
|
||||
const queue = new Set();
|
||||
const staging = new Set();
|
||||
|
||||
function queueTask(task) {
|
||||
staging.delete(task);
|
||||
task.status = "queued";
|
||||
queue.add(task);
|
||||
}
|
||||
|
||||
function deleteTask(task) {
|
||||
staging.delete(task);
|
||||
queue.delete(task);
|
||||
}
|
||||
|
||||
function getQueue() {
|
||||
return queue;
|
||||
}
|
||||
|
||||
function clear() {
|
||||
queue.clear();
|
||||
}
|
||||
|
||||
function getStaging() {
|
||||
return staging;
|
||||
}
|
||||
|
||||
function clearStaging() {
|
||||
staging.clear();
|
||||
}
|
||||
|
||||
function stageSpv(task) {
|
||||
staging.add(task);
|
||||
}
|
||||
|
||||
function commit(tasks, callback) {
|
||||
for (let t of tasks) {
|
||||
t.status = "pending";
|
||||
queue.add(t);
|
||||
}
|
||||
|
||||
return store.postTasks(tasks, (deviceId, connectionRequestStatus, tasks2) => {
|
||||
for (let t of tasks2)
|
||||
if (t.status === "pending") t.status = "stale";
|
||||
else if (t.status === "done") queue.delete(t);
|
||||
if (callback) callback(deviceId, connectionRequestStatus, tasks2);
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
queueTask,
|
||||
deleteTask,
|
||||
clear,
|
||||
getQueue,
|
||||
getStaging,
|
||||
clearStaging,
|
||||
stageSpv,
|
||||
commit
|
||||
};
|
||||
+46
-33
@@ -25,8 +25,11 @@
|
||||
"ui.index.5.parameter": "InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.SSID",
|
||||
"ui.index.6.label": "Last inform",
|
||||
"ui.index.6.parameter": "Events.Inform",
|
||||
"ui.index.6.meta.type": "overview-dot",
|
||||
"ui.index.6.meta.chart": "online",
|
||||
"ui.index.6.type": "container",
|
||||
"ui.index.6.element": "span.inform",
|
||||
"ui.index.6.components.1.type": "parameter",
|
||||
"ui.index.6.components.2.type": "overview-dot",
|
||||
"ui.index.6.components.2.chart": "online",
|
||||
"ui.overview.groups.online.label": "",
|
||||
"ui.overview.groups.online.charts.1": "online",
|
||||
"ui.overview.charts.online.label": "Online status",
|
||||
@@ -40,43 +43,53 @@
|
||||
"ui.overview.charts.online.slices.others.color": "#f7fcb9",
|
||||
"ui.overview.charts.online.slices.others.filter": "Events.Inform < (NOW() - 5 * 60 * 1000) - (24 * 60 * 60 * 1000)",
|
||||
"ui.device.1.type": "parameter-list",
|
||||
"ui.device.1.parameters.1.type": "container",
|
||||
"ui.device.1.parameters.1.label": "Last inform",
|
||||
"ui.device.1.parameters.1.element": "span.inform",
|
||||
"ui.device.1.parameters.1.parameter": "Events.Inform",
|
||||
"ui.device.1.parameters.1.meta.type": "overview-dot",
|
||||
"ui.device.1.parameters.1.meta.chart": "online",
|
||||
"ui.device.1.parameters.1.components.1.type": "parameter",
|
||||
"ui.device.1.parameters.1.components.2.type": "overview-dot",
|
||||
"ui.device.1.parameters.1.components.2.chart": "online",
|
||||
"ui.device.1.parameters.1.components.3.type": "summon-button",
|
||||
"ui.device.1.parameters.1.components.3.parameters.1": "InternetGatewayDevice.DeviceInfo.HardwareVersion",
|
||||
"ui.device.1.parameters.1.components.3.parameters.2": "InternetGatewayDevice.DeviceInfo.SoftwareVersion",
|
||||
"ui.device.1.parameters.1.components.3.parameters.3": "InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.MACAddress",
|
||||
"ui.device.1.parameters.1.components.3.parameters.4": "InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.ExternalIPAddress",
|
||||
"ui.device.1.parameters.1.components.3.parameters.5": "InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.SSID",
|
||||
"ui.device.1.parameters.1.components.3.parameters.6": "InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.KeyPassphrase",
|
||||
"ui.device.1.parameters.1.components.3.parameters.7": "InternetGatewayDevice.LANDevice.1.Hosts.Host",
|
||||
"ui.device.1.parameters.2.label": "Last boot",
|
||||
"ui.device.1.parameters.2.parameter": "Events.1_BOOT",
|
||||
"ui.device.1.parameters.3.label": "Last bootstrap",
|
||||
"ui.device.1.parameters.3.parameter": "Events.0_BOOTSTRAP",
|
||||
"ui.device.1.parameters.4.label": "Registered",
|
||||
"ui.device.1.parameters.4.parameter": "Events.Registered",
|
||||
"ui.device.2.type": "parameter-list",
|
||||
"ui.device.2.parameters.1.label": "Serial number",
|
||||
"ui.device.2.parameters.1.parameter": "InternetGatewayDevice.DeviceInfo.SerialNumber",
|
||||
"ui.device.2.parameters.2.label": "Product class",
|
||||
"ui.device.2.parameters.2.parameter": "InternetGatewayDevice.DeviceInfo.ProductClass",
|
||||
"ui.device.2.parameters.3.label": "OUI",
|
||||
"ui.device.2.parameters.3.parameter": "InternetGatewayDevice.DeviceInfo.ManufacturerOUI",
|
||||
"ui.device.2.parameters.4.label": "Manufacturer",
|
||||
"ui.device.2.parameters.4.parameter": "InternetGatewayDevice.DeviceInfo.Manufacturer",
|
||||
"ui.device.2.parameters.5.label": "Hardware version",
|
||||
"ui.device.2.parameters.5.parameter": "InternetGatewayDevice.DeviceInfo.HardwareVersion",
|
||||
"ui.device.2.parameters.6.label": "Software version",
|
||||
"ui.device.2.parameters.6.parameter": "InternetGatewayDevice.DeviceInfo.SoftwareVersion",
|
||||
"ui.device.2.parameters.7.label": "MAC",
|
||||
"ui.device.2.parameters.7.parameter": "InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.MACAddress",
|
||||
"ui.device.2.parameters.8.label": "IP",
|
||||
"ui.device.2.parameters.8.parameter": "InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.ExternalIPAddress",
|
||||
"ui.device.2.parameters.8.label": "WLAN SSID",
|
||||
"ui.device.2.parameters.8.parameter": "InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.SSID",
|
||||
"ui.device.2.parameters.9.label": "WLAN passphrase",
|
||||
"ui.device.2.parameters.9.parameter": "InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.KeyPassphrase",
|
||||
"ui.device.3.type": "parameter-table",
|
||||
"ui.device.3.object": "InternetGatewayDevice.LANDevice.1.Hosts.Host",
|
||||
"ui.device.3.parameters.1.label": "Host name",
|
||||
"ui.device.3.parameters.1.parameter": "HostName",
|
||||
"ui.device.3.parameters.2.label": "IP address",
|
||||
"ui.device.3.parameters.2.parameter": "IPAddress",
|
||||
"ui.device.3.parameters.3.label": "MAC address",
|
||||
"ui.device.3.parameters.3.parameter": "MACAddress"
|
||||
"ui.device.1.parameters.2.label": "Serial number",
|
||||
"ui.device.1.parameters.2.parameter": "InternetGatewayDevice.DeviceInfo.SerialNumber",
|
||||
"ui.device.1.parameters.3.label": "Product class",
|
||||
"ui.device.1.parameters.3.parameter": "InternetGatewayDevice.DeviceInfo.ProductClass",
|
||||
"ui.device.1.parameters.4.label": "OUI",
|
||||
"ui.device.1.parameters.4.parameter": "InternetGatewayDevice.DeviceInfo.ManufacturerOUI",
|
||||
"ui.device.1.parameters.5.label": "Manufacturer",
|
||||
"ui.device.1.parameters.5.parameter": "InternetGatewayDevice.DeviceInfo.Manufacturer",
|
||||
"ui.device.1.parameters.6.label": "Hardware version",
|
||||
"ui.device.1.parameters.6.parameter": "InternetGatewayDevice.DeviceInfo.HardwareVersion",
|
||||
"ui.device.1.parameters.7.label": "Software version",
|
||||
"ui.device.1.parameters.7.parameter": "InternetGatewayDevice.DeviceInfo.SoftwareVersion",
|
||||
"ui.device.1.parameters.8.label": "MAC",
|
||||
"ui.device.1.parameters.8.parameter": "InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.MACAddress",
|
||||
"ui.device.1.parameters.9.label": "IP",
|
||||
"ui.device.1.parameters.9.parameter": "InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANIPConnection.1.ExternalIPAddress",
|
||||
"ui.device.1.parameters.10.label": "WLAN SSID",
|
||||
"ui.device.1.parameters.10.parameter": "InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.SSID",
|
||||
"ui.device.1.parameters.11.label": "WLAN passphrase",
|
||||
"ui.device.1.parameters.11.parameter": "InternetGatewayDevice.LANDevice.1.WLANConfiguration.1.KeyPassphrase",
|
||||
"ui.device.2.type": "parameter-table",
|
||||
"ui.device.2.object": "InternetGatewayDevice.LANDevice.1.Hosts.Host",
|
||||
"ui.device.2.parameters.1.label": "Host name",
|
||||
"ui.device.2.parameters.1.parameter": "HostName",
|
||||
"ui.device.2.parameters.2.label": "IP address",
|
||||
"ui.device.2.parameters.2.parameter": "IPAddress",
|
||||
"ui.device.2.parameters.3.label": "MAC address",
|
||||
"ui.device.2.parameters.3.parameter": "MACAddress"
|
||||
}
|
||||
+11
-9
@@ -10,20 +10,22 @@
|
||||
},
|
||||
"author": "Zaid Abdulla <zaid@genieacs.com>",
|
||||
"dependencies": {
|
||||
"koa": "^2.4.1",
|
||||
"koa": "^2.5.0",
|
||||
"koa-body": "^2.5.0",
|
||||
"koa-bodyparser": "^4.2.0",
|
||||
"koa-router": "^7.3.0",
|
||||
"koa-compress": "^2.0.0",
|
||||
"koa-router": "^7.4.0",
|
||||
"koa-static": "^4.0.2",
|
||||
"mithril": "^1.1.5",
|
||||
"parsimmon": "^1.6.2"
|
||||
"mithril": "^1.1.6",
|
||||
"parsimmon": "^1.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cssnano": "^3.10.0",
|
||||
"eslint": "^4.12.1",
|
||||
"eslint": "^4.19.1",
|
||||
"postcss-cli": "^4.1.1",
|
||||
"postcss-cssnext": "^3.0.2",
|
||||
"postcss-import": "^11.0.0",
|
||||
"prettier": "^1.8.2",
|
||||
"webpack": "^3.9.1"
|
||||
"postcss-cssnext": "^3.1.0",
|
||||
"postcss-import": "^11.1.0",
|
||||
"prettier": "^1.11.1",
|
||||
"webpack": "^3.11.0"
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ const querystring = require("querystring");
|
||||
const config = require("./config");
|
||||
const device = require("./device");
|
||||
const filterParser = require("../common/filter-parser");
|
||||
const Filter = require("../common/filter");
|
||||
|
||||
const commaAscii = ",".charCodeAt(0);
|
||||
const newLineAscii = "\n".charCodeAt(0);
|
||||
@@ -205,5 +206,130 @@ function query(resource, filter, limit, skip, projection, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function deleteResource(resource, id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let options = url.parse(
|
||||
`${config.get("server.nbi")}${resource}/${encodeURIComponent(id)}`
|
||||
);
|
||||
|
||||
options.method = "DELETE";
|
||||
|
||||
let _http = options.protocol === "https:" ? https : http;
|
||||
|
||||
_http
|
||||
.request(options, res => {
|
||||
res.resume();
|
||||
if (res.statusCode === 200) resolve(true);
|
||||
else if (res.statusCode === 404) resolve(false);
|
||||
else reject(new Error(`Unexpected status code ${res.statusCode}`));
|
||||
})
|
||||
.end();
|
||||
});
|
||||
}
|
||||
|
||||
function postTask(task, connectionRequest, callback) {
|
||||
let options = url.parse(
|
||||
`${config.get("server.nbi")}devices/${task.device}/tasks${
|
||||
connectionRequest ? "?connection_request" : ""
|
||||
}`
|
||||
);
|
||||
options.method = "POST";
|
||||
let _http = options.protocol === "https:" ? https : http;
|
||||
|
||||
_http
|
||||
.request(options, res => {
|
||||
if (res.statusCode !== 200 && res.statusCode !== 202) {
|
||||
callback(new Error(`Unexpected status code ${res.statusCode}`));
|
||||
res.resume();
|
||||
return;
|
||||
}
|
||||
const chunks = [];
|
||||
let bytes = 0;
|
||||
res.on("data", chunk => {
|
||||
chunks.push(chunk);
|
||||
bytes += chunk.length;
|
||||
});
|
||||
|
||||
res.on("end", () => {
|
||||
let buf = new Buffer(bytes);
|
||||
let o = 0;
|
||||
for (let c of chunks) {
|
||||
c.copy(buf, o, 0, c.length);
|
||||
o += c.length;
|
||||
}
|
||||
try {
|
||||
let t = JSON.parse(buf);
|
||||
let connectionRequestStatus;
|
||||
if (connectionRequest) {
|
||||
connectionRequestStatus = "OK";
|
||||
if (
|
||||
res.statusCode === 202 &&
|
||||
res.statusMessage !== "Task queued but not processed" &&
|
||||
res.statusMessage !== "Task faulte"
|
||||
)
|
||||
connectionRequestStatus = res.statusMessage;
|
||||
}
|
||||
callback(null, t, connectionRequestStatus);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
});
|
||||
})
|
||||
.end(JSON.stringify(task));
|
||||
}
|
||||
|
||||
function postTasks(deviceId, tasks) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let connectionRequestStatus;
|
||||
let promises = [];
|
||||
for (let [idx, task] of tasks.entries()) {
|
||||
task.device = deviceId;
|
||||
promises.push(
|
||||
new Promise((res, rej) => {
|
||||
let conReq = idx === tasks.length - 1;
|
||||
delete task._id;
|
||||
task.expiry = 5;
|
||||
postTask(task, conReq, (err, t, crs) => {
|
||||
if (err) return rej(err);
|
||||
if (conReq) connectionRequestStatus = crs;
|
||||
res({ _id: t._id, status: "pending" });
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
Promise.all(promises)
|
||||
.then(statuses => {
|
||||
if (connectionRequestStatus !== "OK")
|
||||
return resolve({
|
||||
connectionRequest: connectionRequestStatus,
|
||||
tasks: statuses
|
||||
});
|
||||
Promise.all(
|
||||
statuses.map(s => query("tasks", new Filter(["=", "_id", 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);
|
||||
}
|
||||
|
||||
resolve({
|
||||
connectionRequest: connectionRequestStatus,
|
||||
tasks: statuses
|
||||
});
|
||||
})
|
||||
.catch(reject);
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
exports.query = query;
|
||||
exports.count = count;
|
||||
exports.postTasks = postTasks;
|
||||
+9
-1
@@ -12,7 +12,8 @@ const resources = {
|
||||
presets: true,
|
||||
provisions: true,
|
||||
files: true,
|
||||
virtual_parameters: true
|
||||
virtual_parameters: true,
|
||||
tasks: false
|
||||
};
|
||||
|
||||
for (let [resource, update] of Object.entries(resources)) {
|
||||
@@ -60,4 +61,11 @@ for (let [resource, update] of Object.entries(resources)) {
|
||||
}
|
||||
}
|
||||
|
||||
router.post("/devices/:id/tasks", async ctx => {
|
||||
let res = await apiFunctions.postTasks(ctx.params.id, ctx.request.body);
|
||||
// TODO 404 if no such device
|
||||
ctx.set("Connection-Request", res.connectionRequest);
|
||||
ctx.body = res.tasks;
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
+2
-2
@@ -114,12 +114,12 @@ function recursive(input, root, output, timestamp) {
|
||||
attrs["valueTimestamp"] = childrenTimestamp;
|
||||
attrs["object"] = false;
|
||||
attrs["objectTimestamp"] = childrenTimestamp;
|
||||
} else if (tree["_object"]) {
|
||||
} else if (tree["_object"] != null) {
|
||||
attrs["object"] = tree["_object"];
|
||||
attrs["objectTimestamp"] = childrenTimestamp;
|
||||
}
|
||||
|
||||
if (tree["_writable"]) {
|
||||
if (tree["_writable"] != null) {
|
||||
attrs["writable"] = tree["_writable"];
|
||||
attrs["writableTimestamp"] = childrenTimestamp;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
const Koa = require("koa");
|
||||
const Router = require("koa-router");
|
||||
const koaStatic = require("koa-static");
|
||||
const koaCompress = require("koa-compress");
|
||||
const koaBody = require("koa-body");
|
||||
|
||||
const config = require("./config");
|
||||
const api = require("./api");
|
||||
@@ -10,6 +12,7 @@ const api = require("./api");
|
||||
const koa = new Koa();
|
||||
const router = new Router();
|
||||
|
||||
koa.use(koaBody());
|
||||
router.use("/api", api.routes(), api.allowedMethods());
|
||||
|
||||
router.get("/", async ctx => {
|
||||
@@ -30,6 +33,11 @@ router.get("/", async ctx => {
|
||||
`;
|
||||
});
|
||||
|
||||
koa.use(
|
||||
koaCompress({
|
||||
flush: require("zlib").Z_SYNC_FLUSH
|
||||
})
|
||||
);
|
||||
koa.use(router.routes());
|
||||
koa.use(koaStatic("./public"));
|
||||
|
||||
|
||||
Reference in new issue
Block a user