From ee8c43747ea658e1c4be6fe1fae00c0fa414fa02 Mon Sep 17 00:00:00 2001 From: Zaid Abdulla Date: Tue, 12 Feb 2019 23:11:27 -0800 Subject: [PATCH] Refactor components Implement context mechanism to pass default attributes to descendant components. --- ui/admin-menu.js | 116 +++++------ ui/app.js | 3 +- ui/components.js | 75 ++++++- ui/components/all-parameters.js | 238 +++++++++++----------- ui/components/container.js | 36 ++-- ui/components/device-actions.js | 158 +++++++-------- ui/components/device-faults.js | 132 +++++++------ ui/components/device-link.js | 47 +++-- ui/components/overview-dot.js | 58 +++--- ui/components/parameter-list.js | 58 +++--- ui/components/parameter-table.js | 214 ++++++++++---------- ui/components/parameter.js | 105 +++++----- ui/components/ping.js | 90 ++++----- ui/components/summon-button.js | 122 ++++++------ ui/components/tags.js | 114 ++++++----- ui/config-page.js | 158 +++++++-------- ui/device-page.js | 41 ++-- ui/devices-page.js | 150 +++++++------- ui/drawer-component.js | 327 ++++++++++++++++--------------- ui/error-page.js | 18 +- ui/faults-page.js | 140 +++++++------ ui/files-page.js | 128 ++++++------ ui/filter-component.js | 107 +++++----- ui/layout.js | 51 +++-- ui/login-page.js | 117 ++++++----- ui/long-text-component.js | 96 ++++----- ui/menu.js | 97 ++++----- ui/notifications.js | 6 +- ui/overview-page.js | 46 ++--- ui/pie-chart-component.js | 16 +- ui/presets-page.js | 170 ++++++++-------- ui/provisions-page.js | 130 ++++++------ ui/put-form-component.js | 155 +++++++-------- ui/user-menu.js | 85 ++++---- ui/virtual-parameters-page.js | 130 ++++++------ 35 files changed, 1903 insertions(+), 1831 deletions(-) diff --git a/ui/admin-menu.js b/ui/admin-menu.js index e94a913..0727f6a 100644 --- a/ui/admin-menu.js +++ b/ui/admin-menu.js @@ -2,63 +2,63 @@ import m from "mithril"; -const menu = { - view: vnode => { - const active = { [vnode.attrs.page]: "active" }; - const tabs = []; +export default function menu() { + return { + view: vnode => { + const active = { [vnode.attrs.page]: "active" }; + const tabs = []; - if (window.authorizer.hasAccess("presets", 1)) { - tabs.push( - m( - "li", - { class: active["presets"] }, - m("a", { href: "#!/admin/presets" }, "Presets") - ) - ); + if (window.authorizer.hasAccess("presets", 1)) { + tabs.push( + m( + "li", + { class: active["presets"] }, + m("a", { href: "#!/admin/presets" }, "Presets") + ) + ); + } + + if (window.authorizer.hasAccess("provisions", 1)) { + tabs.push( + m( + "li", + { class: active["provisions"] }, + m("a", { href: "#!/admin/provisions" }, "Provisions") + ) + ); + } + + if (window.authorizer.hasAccess("virtualParameters", 1)) { + tabs.push( + m( + "li", + { class: active["virtualParameters"] }, + m("a", { href: "#!/admin/virtualParameters" }, "Virtual Parameters") + ) + ); + } + + if (window.authorizer.hasAccess("files", 1)) { + tabs.push( + m( + "li", + { class: active["files"] }, + m("a", { href: "#!/admin/files" }, "Files") + ) + ); + } + + if (window.authorizer.hasAccess("config", 1)) { + tabs.push( + m( + "li", + { class: active["config"] }, + m("a", { href: "#!/admin/config" }, "Config") + ) + ); + } + + return m("nav#side-menu", m("ul", tabs)); } - - if (window.authorizer.hasAccess("provisions", 1)) { - tabs.push( - m( - "li", - { class: active["provisions"] }, - m("a", { href: "#!/admin/provisions" }, "Provisions") - ) - ); - } - - if (window.authorizer.hasAccess("virtualParameters", 1)) { - tabs.push( - m( - "li", - { class: active["virtualParameters"] }, - m("a", { href: "#!/admin/virtualParameters" }, "Virtual Parameters") - ) - ); - } - - if (window.authorizer.hasAccess("files", 1)) { - tabs.push( - m( - "li", - { class: active["files"] }, - m("a", { href: "#!/admin/files" }, "Files") - ) - ); - } - - if (window.authorizer.hasAccess("config", 1)) { - tabs.push( - m( - "li", - { class: active["config"] }, - m("a", { href: "#!/admin/config" }, "Config") - ) - ); - } - - return m("nav#side-menu", m("ul", tabs)); - } -}; - -export default menu; + }; +} diff --git a/ui/app.js b/ui/app.js index c78b689..3716cce 100644 --- a/ui/app.js +++ b/ui/app.js @@ -17,6 +17,7 @@ import * as filesPage from "./files-page"; import * as configPage from "./config-page"; import Authorizer from "../lib//common/authorizer"; import * as notifications from "./notifications"; +import { contextifyComponent } from "./components"; window.authorizer = new Authorizer(window.permissionSets); @@ -49,7 +50,7 @@ function pagify(pageName, page) { lastRenderTimestamp = Date.now(); let p; if (state && state.error) p = m(errorPage.component, state); - else p = m(page.component, state); + else p = m(contextifyComponent(page.component), state); fulfill(); return m(layout, { page: pageName }, p); } diff --git a/ui/components.js b/ui/components.js index 985282e..b006fd0 100644 --- a/ui/components.js +++ b/ui/components.js @@ -1,5 +1,6 @@ "use strict"; +import m from "mithril"; import parameter from "./components/parameter"; import parameterList from "./components/parameter-list"; import parameterTable from "./components/parameter-table"; @@ -12,6 +13,7 @@ import deviceActions from "./components/device-actions"; import tags from "./components/tags"; import ping from "./components/ping"; import deviceLink from "./components/device-link"; +import longTextComponent from "./long-text-component"; const comps = { parameter, @@ -25,11 +27,76 @@ const comps = { "device-actions": deviceActions, tags, ping, - "device-link": deviceLink + "device-link": deviceLink, + "long-text": longTextComponent }; -export function get(name) { - const c = comps[name]; - if (!c) throw new Error(`No such component '${name}'`); +const contextifiedComponents = new WeakMap(); +const vnodeContext = new WeakMap(); + +function contextFn(context, ...argumentsList) { + const vnode = Reflect.apply(M, undefined, argumentsList); + vnodeContext.set(vnode, context); + return vnode; +} + +const M = new Proxy(m, { + apply: (target, thisArg, argumentsList) => { + const c = argumentsList[0]; + if (typeof c !== "string") argumentsList[0] = contextifyComponent(c); + else if (comps[c]) argumentsList[0] = contextifyComponent(comps[c]); + + return Reflect.apply(target, undefined, argumentsList); + }, + get: (target, prop) => { + if (prop === "context") return contextFn; + else return Reflect.get(target, prop); + } +}); + +function applyContext(vnode, parentContext) { + if (Array.isArray(vnode)) { + for (const c of vnode) applyContext(c, parentContext); + } else if (vnode && typeof vnode === "object" && vnode.tag) { + const vc = Object.assign({}, parentContext, vnodeContext.get(vnode)); + vnodeContext.set(vnode, vc); + vnode.attrs = Object.assign({}, vc, vnode.attrs); + if (vnode.children && vnode.children.length) + for (const c of vnode.children) applyContext(c, vc); + } +} + +export function contextifyComponent(component) { + let c = contextifiedComponents.get(component); + if (!c) { + if (typeof component !== "function") { + c = Object.assign({}, component); + const view = component.view; + c.view = vnode => { + const context = vnodeContext.get(vnode) || {}; + const res = view(vnode, M); + applyContext(res, context); + return res; + }; + } else if (!component.prototype.view) { + c = initialNode => { + const state = component(initialNode); + const view = state.view; + state.view = vnode => { + const context = vnodeContext.get(vnode) || {}; + const res = view(vnode, M); + applyContext(res, context); + return res; + }; + return state; + }; + } else { + // TODO support class components + throw new Error("Class components not supported"); + } + contextifiedComponents.set(component, c); + } return c; } + +export { M as m }; diff --git a/ui/components/all-parameters.js b/ui/components/all-parameters.js index 72a8a52..8cfa226 100644 --- a/ui/components/all-parameters.js +++ b/ui/components/all-parameters.js @@ -1,9 +1,7 @@ "use strict"; -import m from "mithril"; -import * as components from "../components"; +import { m } from "../components"; import * as taskQueue from "../task-queue"; -import longTextComponent from "../long-text-component"; import { parse } from "../../lib/common/expression-parser"; import memoize from "../../lib/common/memoize"; @@ -13,125 +11,125 @@ function escapeRegExp(str) { return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&"); } -const component = { - view: vnode => { - const device = vnode.attrs.device; +export default function component() { + return { + view: vnode => { + const device = vnode.attrs.device; - const search = m("input", { - type: "text", - placeholder: "Search parameters", - oninput: e => { - vnode.state.searchString = e.target.value; - e.redraw = false; - clearTimeout(vnode.state.timeout); - vnode.state.timeout = setTimeout(m.redraw, 500); - } - }); - - const instanceRegex = /\.[0-9]+$/; - let re; - if (vnode.state.searchString) { - const keywords = vnode.state.searchString.split(" ").filter(s => s); - if (keywords.length) - re = new RegExp(keywords.map(s => escapeRegExp(s)).join(".*"), "i"); - } - - const rows = Object.keys(device) - .sort() - .map(k => { - const p = device[k]; - const val = []; - const attrs = {}; - if (re) { - const str = p.value && p.value[0] ? `${k} ${p.value[0]}` : k; - if (!re.test(str)) attrs.style = "display: none;"; + const search = m("input", { + type: "text", + placeholder: "Search parameters", + oninput: e => { + vnode.state.searchString = e.target.value; + e.redraw = false; + clearTimeout(vnode.state.timeout); + vnode.state.timeout = setTimeout(m.redraw, 500); } - - if (p.object === false) { - val.push( - m( - components.get("parameter"), - Object.assign({ device: device, parameter: memoizedParse(k) }) - ) - ); - } else if (p.object && p.writable) { - if (instanceRegex.test(k)) { - val.push( - m( - "button", - { - title: "Delete this instance", - onclick: () => { - taskQueue.queueTask({ - name: "deleteObject", - device: device["DeviceID.ID"].value[0], - objectName: k - }); - } - }, - "✕" - ) - ); - } else { - val.push( - m( - "button", - { - title: "Create a new instance", - onclick: () => { - taskQueue.queueTask({ - name: "addObject", - device: device["DeviceID.ID"].value[0], - objectName: k - }); - } - }, - "🞢" - ) - ); - } - } - - val.push( - m( - "button", - { - title: "Refresh tree", - onclick: () => { - taskQueue.queueTask({ - name: "getParameterValues", - device: device["DeviceID.ID"].value[0], - parameterNames: [k] - }); - } - }, - "↺" - ) - ); - - return m( - "tr", - attrs, - m("td.left", m(longTextComponent, { text: k })), - m("td.right", val) - ); }); - return m( - ".all-parameters", - m( - "a.download-csv", - { - href: `/api/devices/${device["DeviceID.ID"].value[0]}.csv`, - download: "", - style: "float: right;" - }, - "Download" - ), - search, - m(".parameter-list", m("table", m("tbody", rows))) - ); - } -}; + const instanceRegex = /\.[0-9]+$/; + let re; + if (vnode.state.searchString) { + const keywords = vnode.state.searchString.split(" ").filter(s => s); + if (keywords.length) + re = new RegExp(keywords.map(s => escapeRegExp(s)).join(".*"), "i"); + } -export default component; + const rows = Object.keys(device) + .sort() + .map(k => { + const p = device[k]; + const val = []; + const attrs = {}; + if (re) { + const str = p.value && p.value[0] ? `${k} ${p.value[0]}` : k; + if (!re.test(str)) attrs.style = "display: none;"; + } + + if (p.object === false) { + val.push( + m( + "parameter", + Object.assign({ device: device, parameter: memoizedParse(k) }) + ) + ); + } else if (p.object && p.writable) { + if (instanceRegex.test(k)) { + val.push( + m( + "button", + { + title: "Delete this instance", + onclick: () => { + taskQueue.queueTask({ + name: "deleteObject", + device: device["DeviceID.ID"].value[0], + objectName: k + }); + } + }, + "✕" + ) + ); + } else { + val.push( + m( + "button", + { + title: "Create a new instance", + onclick: () => { + taskQueue.queueTask({ + name: "addObject", + device: device["DeviceID.ID"].value[0], + objectName: k + }); + } + }, + "🞢" + ) + ); + } + } + + val.push( + m( + "button", + { + title: "Refresh tree", + onclick: () => { + taskQueue.queueTask({ + name: "getParameterValues", + device: device["DeviceID.ID"].value[0], + parameterNames: [k] + }); + } + }, + "↺" + ) + ); + + return m( + "tr", + attrs, + m("td.left", m("long-text", { text: k })), + m("td.right", val) + ); + }); + + return m( + ".all-parameters", + m( + "a.download-csv", + { + href: `/api/devices/${device["DeviceID.ID"].value[0]}.csv`, + download: "", + style: "float: right;" + }, + "Download" + ), + search, + m(".parameter-list", m("table", m("tbody", rows))) + ); + } + }; +} diff --git a/ui/components/container.js b/ui/components/container.js index 67a2da9..416d7ff 100644 --- a/ui/components/container.js +++ b/ui/components/container.js @@ -1,24 +1,22 @@ "use strict"; -import m from "mithril"; -import * as components from "../components"; +import { m } from "../components"; import * as store from "../store"; -const component = { - view: vnode => { - if (vnode.attrs.filter) { - if (!store.evaluateExpression(vnode.attrs.filter, vnode.attrs.device)) - return null; +export default function component() { + return { + view: vnode => { + if (vnode.attrs.filter) { + if (!store.evaluateExpression(vnode.attrs.filter, vnode.attrs.device)) + return null; + } + + const children = Object.values(vnode.attrs.components).map(c => { + if (typeof c !== "object") return `${c}`; + return m(c.type, c); + }); + if (vnode.attrs.element) return m(vnode.attrs.element, children); + else return children; } - - const children = Object.values(vnode.attrs.components).map(c => { - if (typeof c !== "object") return `${c}`; - const attrs = Object.assign({}, vnode.attrs, c); - return m(components.get(attrs.type), attrs); - }); - if (vnode.attrs.element) return m(vnode.attrs.element, children); - else return children; - } -}; - -export default component; + }; +} diff --git a/ui/components/device-actions.js b/ui/components/device-actions.js index e2df56b..f7d015d 100644 --- a/ui/components/device-actions.js +++ b/ui/components/device-actions.js @@ -1,90 +1,90 @@ "use strict"; -import m from "mithril"; +import { m } from "../components"; import * as taskQueue from "../task-queue"; import * as notifications from "../notifications"; import * as store from "../store"; -const component = { - view: vnode => { - const device = vnode.attrs.device; +export default function component() { + return { + view: vnode => { + const device = vnode.attrs.device; - const buttons = []; + const buttons = []; - buttons.push( - m( - "button.primary", - { - title: "Reboot device", - onclick: () => { - taskQueue.queueTask({ - name: "reboot", - device: device["DeviceID.ID"].value[0] - }); - } - }, - "Reboot" - ) - ); - - buttons.push( - m( - "button.critical", - { - title: "Factory reset device", - onclick: () => { - taskQueue.queueTask({ - name: "factoryReset", - device: device["DeviceID.ID"].value[0] - }); - } - }, - "Reset" - ) - ); - - buttons.push( - m( - "button.critical", - { - title: "Push a firmware or a config file", - onclick: () => { - taskQueue.stageDownload({ - name: "download", - device: device["DeviceID.ID"].value[0] - }); - } - }, - "Push file" - ) - ); - - buttons.push( - m( - "button.primary", - { - title: "Delete device", - onclick: () => { - if (!confirm("Deleting this device. Are you sure?")) return; - const deviceId = device["DeviceID.ID"].value[0]; - - store - .deleteResource("devices", deviceId) - .then(() => { - notifications.push("success", `${deviceId}: Device deleted`); - m.route.set("/devices"); - }) - .catch(err => { - notifications.push("error", err.message); + buttons.push( + m( + "button.primary", + { + title: "Reboot device", + onclick: () => { + taskQueue.queueTask({ + name: "reboot", + device: device["DeviceID.ID"].value[0] }); - } - }, - "Delete" - ) - ); + } + }, + "Reboot" + ) + ); - return m(".actions-bar", buttons); - } -}; + buttons.push( + m( + "button.critical", + { + title: "Factory reset device", + onclick: () => { + taskQueue.queueTask({ + name: "factoryReset", + device: device["DeviceID.ID"].value[0] + }); + } + }, + "Reset" + ) + ); -export default component; + buttons.push( + m( + "button.critical", + { + title: "Push a firmware or a config file", + onclick: () => { + taskQueue.stageDownload({ + name: "download", + device: device["DeviceID.ID"].value[0] + }); + } + }, + "Push file" + ) + ); + + buttons.push( + m( + "button.primary", + { + title: "Delete device", + onclick: () => { + if (!confirm("Deleting this device. Are you sure?")) return; + const deviceId = device["DeviceID.ID"].value[0]; + + store + .deleteResource("devices", deviceId) + .then(() => { + notifications.push("success", `${deviceId}: Device deleted`); + m.route.set("/devices"); + }) + .catch(err => { + notifications.push("error", err.message); + }); + } + }, + "Delete" + ) + ); + + return m(".actions-bar", buttons); + } + }; +} diff --git a/ui/components/device-faults.js b/ui/components/device-faults.js index 02576a8..0a31c0c 100644 --- a/ui/components/device-faults.js +++ b/ui/components/device-faults.js @@ -1,71 +1,75 @@ "use strict"; -import m from "mithril"; +import { m } from "../components"; import * as store from "../store"; import * as notifications from "../notifications"; -const component = { - view: vnode => { - const device = vnode.attrs.device; - const deviceId = device["DeviceID.ID"].value[0]; - const faults = store.fetch("faults", [ - "AND", - [">", ["PARAM", "_id"], `${deviceId}:`], - ["<", ["PARAM", "_id"], `${deviceId}:zzzz`] - ]); - - const headers = ["Channel", "Code", "Message", "Retries", "Timestamp"].map( - l => m("th", l) - ); - const thead = m("thead", m("tr", headers)); - - const rows = []; - for (const f of faults.value) { - rows.push([ - m("td", f["channel"]), - m("td", f["code"]), - m("td", f["message"]), - m("td", f["retries"]), - m("td", new Date(f["timestamp"]).toLocaleString()), - m( - "td", - m( - "button", - { - title: "Delete fault", - onclick: e => { - e.redraw = false; - store - .deleteResource("faults", f["_id"]) - .then(() => { - notifications.push("success", "Fault deleted"); - store.fulfill(Date.now(), Date.now()); - m.redraw(); - }) - .catch(err => { - notifications.push("error", err.message); - store.fulfill(Date.now(), Date.now()); - }); - } - }, - "✕" - ) - ) +export default function component() { + return { + view: vnode => { + const device = vnode.attrs.device; + const deviceId = device["DeviceID.ID"].value[0]; + const faults = store.fetch("faults", [ + "AND", + [">", ["PARAM", "_id"], `${deviceId}:`], + ["<", ["PARAM", "_id"], `${deviceId}:zzzz`] ]); + + const headers = [ + "Channel", + "Code", + "Message", + "Retries", + "Timestamp" + ].map(l => m("th", l)); + const thead = m("thead", m("tr", headers)); + + const rows = []; + for (const f of faults.value) { + rows.push([ + m("td", f["channel"]), + m("td", f["code"]), + m("td", f["message"]), + m("td", f["retries"]), + m("td", new Date(f["timestamp"]).toLocaleString()), + m( + "td", + m( + "button", + { + title: "Delete fault", + onclick: e => { + e.redraw = false; + store + .deleteResource("faults", f["_id"]) + .then(() => { + notifications.push("success", "Fault deleted"); + store.fulfill(Date.now(), Date.now()); + m.redraw(); + }) + .catch(err => { + notifications.push("error", err.message); + store.fulfill(Date.now(), Date.now()); + }); + } + }, + "✕" + ) + ) + ]); + } + + let tbody; + if (rows.length) { + tbody = m("tbody", rows.map(r => m("tr", r))); + } else { + tbody = m( + "tbody", + m("tr.empty", m("td", { colspan: headers.length }, "No faults")) + ); + } + + return m("table.table", thead, tbody); } - - let tbody; - if (rows.length) { - tbody = m("tbody", rows.map(r => m("tr", r))); - } else { - tbody = m( - "tbody", - m("tr.empty", m("td", { colspan: headers.length }, "No faults")) - ); - } - - return m("table.table", thead, tbody); - } -}; - -export default component; + }; +} diff --git a/ui/components/device-link.js b/ui/components/device-link.js index 6cf9f76..404d83c 100644 --- a/ui/components/device-link.js +++ b/ui/components/device-link.js @@ -1,29 +1,28 @@ "use strict"; -import m from "mithril"; -import * as components from "../components"; +import { m } from "../components"; -const component = { - view: vnode => { - let deviceId; - if (vnode.attrs.device) - deviceId = vnode.attrs.device["DeviceID.ID"].value[0]; +export default function component() { + return { + view: vnode => { + let deviceId; + if (vnode.attrs.device) + deviceId = vnode.attrs.device["DeviceID.ID"].value[0]; - const children = Object.values(vnode.attrs.components).map(c => { - if (typeof c !== "object") return `${c}`; - const attrs = Object.assign({}, vnode.attrs, c); - return m(components.get(attrs.type), attrs); - }); - if (deviceId) { - return m( - "a", - { href: `#!/devices/${encodeURIComponent(deviceId)}` }, - children - ); - } else { - return children; + const children = Object.values(vnode.attrs.components).map(c => { + if (typeof c !== "object") return `${c}`; + const attrs = Object.assign({}, vnode.attrs, c); + return m(attrs.type, attrs); + }); + if (deviceId) { + return m( + "a", + { href: `#!/devices/${encodeURIComponent(deviceId)}` }, + children + ); + } else { + return children; + } } - } -}; - -export default component; + }; +} diff --git a/ui/components/overview-dot.js b/ui/components/overview-dot.js index 3c0bffb..1f54d08 100644 --- a/ui/components/overview-dot.js +++ b/ui/components/overview-dot.js @@ -1,38 +1,38 @@ "use strict"; -import m from "mithril"; +import { m } from "../components"; import config from "../config"; import * as store from "../store"; const CHARTS = config.ui.overview.charts; -const component = { - view: vnode => { - const device = vnode.attrs.device; - const chart = CHARTS[vnode.attrs.chart]; - for (const slice of Object.values(chart.slices)) { - const filter = slice.filter; - if (store.evaluateExpression(filter, device)) { - const dot = m( - "svg", - { - width: "1em", - height: "1em", - xmlns: "http://www.w3.org/2000/svg", - "xmlns:xlink": "http://www.w3.org/1999/xlink" - }, - m("circle", { - cx: "0.5em", - cy: "0.5em", - r: "0.4em", - fill: slice.color - }) - ); - return m("span.overview-dot", dot, `${slice.label}`); +export default function component() { + return { + view: vnode => { + const device = vnode.attrs.device; + const chart = CHARTS[vnode.attrs.chart]; + for (const slice of Object.values(chart.slices)) { + const filter = slice.filter; + if (store.evaluateExpression(filter, device)) { + const dot = m( + "svg", + { + width: "1em", + height: "1em", + xmlns: "http://www.w3.org/2000/svg", + "xmlns:xlink": "http://www.w3.org/1999/xlink" + }, + m("circle", { + cx: "0.5em", + cy: "0.5em", + r: "0.4em", + fill: slice.color + }) + ); + return m("span.overview-dot", dot, `${slice.label}`); + } } + return null; } - return null; - } -}; - -export default component; + }; +} diff --git a/ui/components/parameter-list.js b/ui/components/parameter-list.js index 553d77e..fe21dd6 100644 --- a/ui/components/parameter-list.js +++ b/ui/components/parameter-list.js @@ -1,35 +1,35 @@ "use strict"; -import m from "mithril"; -import { get } from "../components"; -import memoize from "../../lib/common/memoize"; +import { m } from "../components"; -const getChildAttrs = memoize((attrs, device) => - Object.assign({}, attrs, { device: device }) -); +export default function component() { + return { + view: vnode => { + const device = vnode.attrs.device; -export default { - view: vnode => { - const device = vnode.attrs.device; + const rows = Object.values(vnode.attrs.parameters).map(parameter => { + const p = m.context( + { + device: device, + parameter: parameter.parameter + }, + parameter.type || "parameter", + parameter + ); - const rows = Object.values(vnode.attrs.parameters).map(parameter => { - const p = m( - get(parameter.type || "parameter"), - getChildAttrs(parameter, device) - ); + return m( + "tr", + { + onupdate: vn => { + vn.dom.style.display = p.dom ? "" : "none"; + } + }, + m("th", parameter.label), + m("td", p) + ); + }); - return m( - "tr", - { - onupdate: vn => { - vn.dom.style.display = p.dom ? "" : "none"; - } - }, - m("th", parameter.label), - m("td", p) - ); - }); - - return m("table.parameter-list", rows); - } -}; + return m("table.parameter-list", rows); + } + }; +} diff --git a/ui/components/parameter-table.js b/ui/components/parameter-table.js index 56ab1ab..741fb5f 100644 --- a/ui/components/parameter-table.js +++ b/ui/components/parameter-table.js @@ -1,132 +1,134 @@ "use strict"; -import m from "mithril"; -import * as components from "../components"; +import { m } from "../components"; import * as taskQueue from "../task-queue"; import * as store from "../store"; import * as expressionParser from "../../lib/common/expression-parser"; -const component = { - oninit: vnode => { - const obj = vnode.attrs.parameter; - if (!Array.isArray(obj) || !obj[0] === "PARAM") - throw new Error("Object must be a parameter path"); - vnode.state.object = obj[1]; - vnode.state.parameters = Object.values(vnode.attrs.childParameters); - }, - view: vnode => { - const device = vnode.attrs.device; - const object = store.evaluateExpression(vnode.state.object, device); - const parameters = vnode.state.parameters; +export default function component() { + return { + oninit: vnode => { + const obj = vnode.attrs.parameter; + if (!Array.isArray(obj) || !obj[0] === "PARAM") + throw new Error("Object must be a parameter path"); + vnode.state.object = obj[1]; + vnode.state.parameters = Object.values(vnode.attrs.childParameters); + }, + view: vnode => { + const device = vnode.attrs.device; + const object = store.evaluateExpression(vnode.state.object, device); + const parameters = vnode.state.parameters; - if (!device[object]) return null; + if (!device[object]) return null; - const instances = new Set(); - const prefix = `${object}.`; - for (const p in device) { - if (p.startsWith(prefix)) { - const i = p.indexOf(".", prefix.length); - if (i === -1) instances.add(p); - else instances.add(p.slice(0, i)); + const instances = new Set(); + const prefix = `${object}.`; + for (const p in device) { + if (p.startsWith(prefix)) { + const i = p.indexOf(".", prefix.length); + if (i === -1) instances.add(p); + else instances.add(p.slice(0, i)); + } } - } - const headers = Object.values(parameters).map(p => m("th", p.label)); + const headers = Object.values(parameters).map(p => m("th", p.label)); - const thead = m("thead", m("tr", headers)); + const thead = m("thead", m("tr", headers)); - const rows = []; - for (const i of instances) { - let filter = vnode.attrs.filter != null ? vnode.attrs.filter : true; + const rows = []; + for (const i of instances) { + let filter = vnode.attrs.filter != null ? vnode.attrs.filter : true; - filter = expressionParser.map(filter, e => { - if (Array.isArray(e) && e[0] === "PARAM") - return ["PARAM", ["||", i, ".", e[1]]]; - return e; - }); - - if (!store.evaluateExpression(filter, device)) continue; - - const row = parameters.map(p => { - const param = expressionParser.map(p.parameter, e => { + filter = expressionParser.map(filter, e => { if (Array.isArray(e) && e[0] === "PARAM") return ["PARAM", ["||", i, ".", e[1]]]; return e; }); - return m( - "td", - m( - components.get(p.type || "parameter"), - Object.assign({}, p, { - device: device, - parameter: param, - label: "" - }) - ) - ); - }); - if (device[i].writable === true) { - row.push( - m( + if (!store.evaluateExpression(filter, device)) continue; + + const row = parameters.map(p => { + const param = expressionParser.map(p.parameter, e => { + if (Array.isArray(e) && e[0] === "PARAM") + return ["PARAM", ["||", i, ".", e[1]]]; + return e; + }); + return m( "td", - m( - "button", + m.context( { - title: "Delete this instance", - onclick: () => { - taskQueue.queueTask({ - name: "deleteObject", - device: device["DeviceID.ID"].value[0], - objectName: i - }); - } + device: device, + parameter: param }, - "✕" + p.type || "parameter", + Object.assign({}, p, { + device: device, + parameter: param + }) + ) + ); + }); + + if (device[i].writable === true) { + row.push( + m( + "td", + m( + "button", + { + title: "Delete this instance", + onclick: () => { + taskQueue.queueTask({ + name: "deleteObject", + device: device["DeviceID.ID"].value[0], + objectName: i + }); + } + }, + "✕" + ) + ) + ); + } + rows.push(m("tr", row)); + } + + if (!rows.length) { + rows.push( + m("tr.empty", m("td", { colspan: headers.length }, "No instances")) + ); + } + + if (device[object].writable === true) { + rows.push( + m( + "tr", + m("td", { colspan: headers.length }), + m( + "td", + m( + "button", + { + title: "Create a new instance", + onclick: () => { + taskQueue.queueTask({ + name: "addObject", + device: device["DeviceID.ID"].value[0], + objectName: object + }); + } + }, + "🞢" + ) ) ) ); } - rows.push(m("tr", row)); + + let label; + if (vnode.attrs.label) label = m("h2", vnode.attrs.label); + + return [label, m("table.table", thead, m("tbody", rows))]; } - - if (!rows.length) { - rows.push( - m("tr.empty", m("td", { colspan: headers.length }, "No instances")) - ); - } - - if (device[object].writable === true) { - rows.push( - m( - "tr", - m("td", { colspan: headers.length }), - m( - "td", - m( - "button", - { - title: "Create a new instance", - onclick: () => { - taskQueue.queueTask({ - name: "addObject", - device: device["DeviceID.ID"].value[0], - objectName: object - }); - } - }, - "🞢" - ) - ) - ) - ); - } - - let label; - if (vnode.attrs.label) label = m("h2", vnode.attrs.label); - - return [label, m("table.table", thead, m("tbody", rows))]; - } -}; - -export default component; + }; +} diff --git a/ui/components/parameter.js b/ui/components/parameter.js index 9fe44dd..c4cb1e2 100644 --- a/ui/components/parameter.js +++ b/ui/components/parameter.js @@ -1,12 +1,11 @@ "use strict"; -import m from "mithril"; +import { m } from "../components"; import * as taskQueue from "../task-queue"; import * as store from "../store"; import * as expression from "../../lib/common/expression"; import memoize from "../../lib/common/memoize"; import timeAgo from "../timeago"; -import longTextComponent from "../long-text-component"; const evaluateParam = memoize((exp, obj, now) => { let timestamp = now; @@ -32,62 +31,62 @@ const evaluateParam = memoize((exp, obj, now) => { return { value, timestamp, parameter }; }); -const component = { - view: vnode => { - const device = vnode.attrs.device; +export default function component() { + return { + view: vnode => { + const device = vnode.attrs.device; - const { value, timestamp, parameter } = evaluateParam( - vnode.attrs.parameter, - vnode.attrs.device, - store.getTimestamp() - ); + const { value, timestamp, parameter } = evaluateParam( + vnode.attrs.parameter, + vnode.attrs.device, + store.getTimestamp() + ); - if (value == null) return null; + if (value == null) return null; - let edit; - if (device[parameter] && device[parameter].writable) { - edit = m( - "button", - { - title: "Edit parameter value", - onclick: () => { - taskQueue.stageSpv({ - name: "setParameterValues", - device: device["DeviceID.ID"].value[0], - parameterValues: [ - [ - parameter, - device[parameter].value[0], - device[parameter].value[1] + let edit; + if (device[parameter] && device[parameter].writable) { + edit = m( + "button", + { + title: "Edit parameter value", + onclick: () => { + taskQueue.stageSpv({ + name: "setParameterValues", + device: device["DeviceID.ID"].value[0], + parameterValues: [ + [ + parameter, + device[parameter].value[0], + device[parameter].value[1] + ] ] - ] - }); + }); + } + }, + "✎" + ); + } + + const el = m("long-text", { text: `${value}` }); + + return m( + "span", + { + class: "parameter-value", + onmouseover: e => { + e.redraw = false; + // Don't update any child element + if (e.target === el.dom) { + const now = Date.now(); + const localeString = new Date(timestamp).toLocaleString(); + e.target.title = `${localeString} (${timeAgo(now - timestamp)})`; + } } }, - "✎" + el, + edit ); } - - const el = m(longTextComponent, { text: `${value}` }); - - return m( - "span", - { - class: "parameter-value", - onmouseover: e => { - e.redraw = false; - // Don't update any child element - if (e.target === el.dom) { - const now = Date.now(); - const localeString = new Date(timestamp).toLocaleString(); - e.target.title = `${localeString} (${timeAgo(now - timestamp)})`; - } - } - }, - el, - edit - ); - } -}; - -export default component; + }; +} diff --git a/ui/components/ping.js b/ui/components/ping.js index 93146c5..1bf98ee 100644 --- a/ui/components/ping.js +++ b/ui/components/ping.js @@ -1,56 +1,56 @@ "use strict"; -import m from "mithril"; +import { m } from "../components"; import * as store from "../store"; const REFRESH_INTERVAL = 3000; -const component = { - oninit: vnode => { - vnode.state.timestamp = 0; - vnode.state.ping = null; - vnode.state.host = null; - vnode.state.timeout = null; - }, - view: vnode => { - const refresh = () => { - const device = vnode.attrs.device; - let param = - device["InternetGatewayDevice.ManagementServer.ConnectionRequestURL"]; - if (!param) - param = device["Device.ManagementServer.ConnectionRequestURL"]; - if (!param || !param.value) return; +export default function component() { + return { + oninit: vnode => { + vnode.state.timestamp = 0; + vnode.state.ping = null; + vnode.state.host = null; + vnode.state.timeout = null; + }, + view: vnode => { + const refresh = () => { + const device = vnode.attrs.device; + let param = + device["InternetGatewayDevice.ManagementServer.ConnectionRequestURL"]; + if (!param) + param = device["Device.ManagementServer.ConnectionRequestURL"]; + if (!param || !param.value) return; - const url = new URL(param.value[0]); - vnode.state.host = url.hostname; - vnode.state.timestamp = Date.now(); - store - .ping(vnode.state.host) - .then(res => { - vnode.state.ping = res; - }) - .catch(() => { - // Do nothing - }); - }; + const url = new URL(param.value[0]); + vnode.state.host = url.hostname; + vnode.state.timestamp = Date.now(); + store + .ping(vnode.state.host) + .then(res => { + vnode.state.ping = res; + }) + .catch(() => { + // Do nothing + }); + }; - const t = vnode.state.timestamp + REFRESH_INTERVAL - Date.now(); - clearTimeout(vnode.state.timeout); - if (t <= 0) refresh(); - else vnode.state.timeout = setTimeout(refresh, t); + const t = vnode.state.timestamp + REFRESH_INTERVAL - Date.now(); + clearTimeout(vnode.state.timeout); + if (t <= 0) refresh(); + else vnode.state.timeout = setTimeout(refresh, t); - if (vnode.state.host) { - let status = ""; - if (vnode.state.ping) { - if (vnode.state.ping.avg != null) - status = `${Math.trunc(vnode.state.ping.avg)} ms`; - else status = "Unreachable"; + if (vnode.state.host) { + let status = ""; + if (vnode.state.ping) { + if (vnode.state.ping.avg != null) + status = `${Math.trunc(vnode.state.ping.avg)} ms`; + else status = "Unreachable"; + } + + return m("div", `Pinging ${vnode.state.host}: ${status}`); } - - return m("div", `Pinging ${vnode.state.host}: ${status}`); + return null; } - return null; - } -}; - -export default component; + }; +} diff --git a/ui/components/summon-button.js b/ui/components/summon-button.js index d28d04c..10b6304 100644 --- a/ui/components/summon-button.js +++ b/ui/components/summon-button.js @@ -1,71 +1,71 @@ "use strict"; -import m from "mithril"; +import { m } from "../components"; import * as taskQueue from "../task-queue"; import * as store from "../store"; import * as notifications from "../notifications"; -const component = { - view: vnode => { - const device = vnode.attrs.device; +export default function component() { + return { + view: vnode => { + const device = vnode.attrs.device; - return m( - "button.primary", - { - title: "Initiate session and refresh basic parameters", - onclick: e => { - e.target.disabled = true; - const params = Object.values(vnode.attrs.parameters) - .map(exp => { - if (!Array.isArray(exp) || !exp[0] === "PARAM") return null; - return store.evaluateExpression(exp[1], device); - }) - .filter(exp => !!exp); + return m( + "button.primary", + { + title: "Initiate session and refresh basic parameters", + onclick: e => { + e.target.disabled = true; + const params = Object.values(vnode.attrs.parameters) + .map(exp => { + if (!Array.isArray(exp) || !exp[0] === "PARAM") return null; + return store.evaluateExpression(exp[1], device); + }) + .filter(exp => !!exp); - const task = { - name: "getParameterValues", - parameterNames: params, - device: device["DeviceID.ID"].value[0] - }; + const task = { + name: "getParameterValues", + parameterNames: params, + device: device["DeviceID.ID"].value[0] + }; - taskQueue - .commit( - [task], - (deviceId, err, connectionRequestStatus, tasks2) => { - if (err) { - notifications.push("error", `${deviceId}: ${err.message}`); - return; + taskQueue + .commit( + [task], + (deviceId, err, connectionRequestStatus, tasks2) => { + if (err) { + notifications.push("error", `${deviceId}: ${err.message}`); + return; + } + + for (const t of tasks2) + if (t.status === "stale") taskQueue.deleteTask(t); + + if (connectionRequestStatus !== "OK") { + notifications.push( + "error", + `${deviceId}: ${connectionRequestStatus}` + ); + } else if (tasks2[0].status === "stale") { + notifications.push( + "error", + `${deviceId}: No contact from device` + ); + } else if (tasks2[0].status === "fault") { + notifications.push("error", `${deviceId}: Refresh faulted`); + } else { + notifications.push("success", `${deviceId}: Summoned`); + } } - - for (const t of tasks2) - if (t.status === "stale") taskQueue.deleteTask(t); - - if (connectionRequestStatus !== "OK") { - notifications.push( - "error", - `${deviceId}: ${connectionRequestStatus}` - ); - } else if (tasks2[0].status === "stale") { - notifications.push( - "error", - `${deviceId}: No contact from device` - ); - } else if (tasks2[0].status === "fault") { - notifications.push("error", `${deviceId}: Refresh faulted`); - } else { - notifications.push("success", `${deviceId}: Summoned`); - } - } - ) - .then(() => { - e.target.disabled = false; - store.fulfill(0, Date.now()); - }); - } - }, - "Summon" - ); - } -}; - -export default component; + ) + .then(() => { + e.target.disabled = false; + store.fulfill(0, Date.now()); + }); + } + }, + "Summon" + ); + } + }; +} diff --git a/ui/components/tags.js b/ui/components/tags.js index 7a12881..5ff72e8 100644 --- a/ui/components/tags.js +++ b/ui/components/tags.js @@ -1,37 +1,75 @@ "use strict"; -import m from "mithril"; +import { m } from "../components"; import * as notifications from "../notifications"; import * as store from "../store"; -const component = { - view: vnode => { - const device = vnode.attrs.device; - let writable = true; - if (vnode.attrs.writable != null) writable = vnode.attrs.writable; +export default function component() { + return { + view: vnode => { + const device = vnode.attrs.device; + let writable = true; + if (vnode.attrs.writable != null) writable = vnode.attrs.writable; - const tags = []; - for (const p of Object.keys(device)) - if (p.startsWith("Tags.")) tags.push(p.slice(5)); + const tags = []; + for (const p of Object.keys(device)) + if (p.startsWith("Tags.")) tags.push(p.slice(5)); - tags.sort(); + tags.sort(); - if (!writable) return m(".tags", tags.map(t => m("span.tag", t))); + if (!writable) return m(".tags", tags.map(t => m("span.tag", t))); - return m( - ".tags", - tags.map(tag => + return m( + ".tags", + tags.map(tag => + m( + "span.tag", + tag, + m( + "button", + { + onclick: e => { + e.target.disabled = true; + const deviceId = device["DeviceID.ID"].value[0]; + store + .updateTags(deviceId, { [tag]: false }) + .then(() => { + e.target.disabled = false; + notifications.push( + "success", + `${deviceId}: Tags updated` + ); + store.fulfill(0, Date.now()); + }) + .catch(err => { + e.target.disabled = false; + notifications.push( + "error", + `${deviceId}: ${err.message}` + ); + }); + } + }, + "✕" + ) + ) + ), m( - "span.tag", - tag, + "span.tag.writable", m( "button", { onclick: e => { e.target.disabled = true; const deviceId = device["DeviceID.ID"].value[0]; + const tag = prompt(`Enter tag to assign to device:`); + if (!tag) { + e.target.disabled = false; + return; + } + store - .updateTags(deviceId, { [tag]: false }) + .updateTags(deviceId, { [tag]: true }) .then(() => { e.target.disabled = false; notifications.push("success", `${deviceId}: Tags updated`); @@ -43,42 +81,10 @@ const component = { }); } }, - "✕" + "🞢" ) ) - ), - m( - "span.tag.writable", - m( - "button", - { - onclick: e => { - e.target.disabled = true; - const deviceId = device["DeviceID.ID"].value[0]; - const tag = prompt(`Enter tag to assign to device:`); - if (!tag) { - e.target.disabled = false; - return; - } - - store - .updateTags(deviceId, { [tag]: true }) - .then(() => { - e.target.disabled = false; - notifications.push("success", `${deviceId}: Tags updated`); - store.fulfill(0, Date.now()); - }) - .catch(err => { - e.target.disabled = false; - notifications.push("error", `${deviceId}: ${err.message}`); - }); - } - }, - "🞢" - ) - ) - ); - } -}; - -export default component; + ); + } + }; +} diff --git a/ui/config-page.js b/ui/config-page.js index 58fec21..49e4b73 100644 --- a/ui/config-page.js +++ b/ui/config-page.js @@ -1,12 +1,10 @@ "use strict"; -import m from "mithril"; - +import { m } from "./components"; import * as store from "./store"; import * as notifications from "./notifications"; import putFormComponent from "./put-form-component"; import * as overlay from "./overlay"; -import longTextComponent from "./long-text-component"; import * as expressionParser from "../lib/common/expression-parser"; import { loadCodeMirror } from "./dynamic-loader"; @@ -84,7 +82,7 @@ function escapeRegExp(str) { return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&"); } -function init() { +export function init() { if (!window.authorizer.hasAccess("config", 2)) { return Promise.reject( new Error("You are not authorized to view this page") @@ -177,14 +175,10 @@ function renderTable(confsResponse, searchString) { m( "tr", attrs, - m("td.left", m(longTextComponent, { text: conf._id })), + m("td.left", m("long-text", { text: conf._id })), m( "td.right", - m("span", [ - m(longTextComponent, { text: `${conf.value}` }), - edit, - del - ]) + m("span", [m("long-text", { text: `${conf.value}` }), edit, del]) ) ) ); @@ -196,80 +190,80 @@ function renderTable(confsResponse, searchString) { return m("table", m("tbody", rows)); } -const component = { - view: vnode => { - document.title = "Config - GenieACS"; +export function component() { + return { + view: vnode => { + document.title = "Config - GenieACS"; - const search = m("input", { - type: "text", - placeholder: "Search config", - oninput: e => { - vnode.state.searchString = e.target.value; - e.redraw = false; - clearTimeout(vnode.state.timeout); - vnode.state.timeout = setTimeout(m.redraw, 250); + const search = m("input", { + type: "text", + placeholder: "Search config", + oninput: e => { + vnode.state.searchString = e.target.value; + e.redraw = false; + clearTimeout(vnode.state.timeout); + vnode.state.timeout = setTimeout(m.redraw, 250); + } + }); + + let newConfig; + if (window.authorizer.hasAccess("config", 3)) { + newConfig = m( + "button.primary", + { + title: "Create new config", + onclick: () => { + const cb = () => { + return m( + putFormComponent, + Object.assign( + { + actionHandler: (action, object) => { + return new Promise(resolve => { + putActionHandler(action, object, true) + .then(errors => { + errors = errors ? Object.values(errors) : []; + if (errors.length) { + for (const err of errors) + notifications.push("error", err); + } else { + overlay.close(cb); + } + resolve(); + }) + .catch(err => { + notifications.push("error", err.message); + resolve(); + }); + }); + } + }, + formData + ) + ); + }; + overlay.open(cb); + } + }, + "New" + ); } - }); - let newConfig; - if (window.authorizer.hasAccess("config", 3)) { - newConfig = m( - "button.primary", - { - title: "Create new config", - onclick: () => { - const cb = () => { - return m( - putFormComponent, - Object.assign( - { - actionHandler: (action, object) => { - return new Promise(resolve => { - putActionHandler(action, object, true) - .then(errors => { - errors = errors ? Object.values(errors) : []; - if (errors.length) { - for (const err of errors) - notifications.push("error", err); - } else { - overlay.close(cb); - } - resolve(); - }) - .catch(err => { - notifications.push("error", err.message); - resolve(); - }); - }); - } - }, - formData - ) - ); - }; - overlay.open(cb); - } - }, - "New" - ); - } + const confs = store.fetch("config", true); - const confs = store.fetch("config", true); - - return [ - m("h1", "Listing config"), - m( - ".all-parameters", - search, + return [ + m("h1", "Listing config"), m( - ".parameter-list", - { style: "height: 400px" }, - renderTable(confs, vnode.state.searchString) - ), - m(".actions-bar", newConfig) - ) - ]; - } -}; - -export { init, component }; + ".all-parameters", + search, + m( + ".parameter-list", + { style: "height: 400px" }, + renderTable(confs, vnode.state.searchString) + ), + m(".actions-bar", newConfig) + ) + ]; + } + }; +} diff --git a/ui/device-page.js b/ui/device-page.js index 809c2c4..63b2ffd 100644 --- a/ui/device-page.js +++ b/ui/device-page.js @@ -1,12 +1,10 @@ "use strict"; -import m from "mithril"; - +import { m } from "./components"; import config from "./config"; import * as store from "./store"; -import * as components from "./components"; -function init(args) { +export function init(args) { if (!window.authorizer.hasAccess("devices", 2)) { return Promise.reject( new Error("You are not authorized to view this page") @@ -19,27 +17,20 @@ function init(args) { }); } -const component = { - view: vnode => { - document.title = `${vnode.attrs.deviceId} - Devices - GenieACS`; +export function component() { + return { + view: vnode => { + document.title = `${vnode.attrs.deviceId} - Devices - GenieACS`; - const dev = store.fetch("devices", vnode.attrs.deviceFilter).value; - if (!dev.length) return "Loading"; - const conf = config.ui.device; - const cmps = []; + const dev = store.fetch("devices", vnode.attrs.deviceFilter).value; + if (!dev.length) return "Loading"; + const conf = config.ui.device; + const cmps = []; - for (const c of Object.values(conf)) { - cmps.push( - m(components.get(c["type"]), Object.assign({ device: dev[0] }, c)) - ); + for (const c of Object.values(conf)) + cmps.push(m.context({ device: dev[0] }, c["type"], c)); + + return [m("h1", vnode.attrs.deviceId), cmps]; } - - return [ - m("h1", vnode.attrs.deviceId), - m("span", dev[0]["InternetGatewayDevice.DeviceInfo.SerialNumber"]), - cmps - ]; - } -}; - -export { init, component }; + }; +} diff --git a/ui/devices-page.js b/ui/devices-page.js index 50c9522..14743c9 100644 --- a/ui/devices-page.js +++ b/ui/devices-page.js @@ -1,11 +1,9 @@ "use strict"; -import m from "mithril"; - +import { m } from "./components"; import config from "./config"; import filterComponent from "./filter-component"; import * as store from "./store"; -import * as components from "./components"; import * as taskQueue from "./task-queue"; import * as notifications from "./notifications"; import { evaluate, extractParams } from "../lib/common/expression"; @@ -36,10 +34,6 @@ const getDownloadUrl = memoize((filter, indexParameters) => { })}`; }); -const getChildAttrs = memoize((attrs, device) => - Object.assign({}, attrs, { device: device }) -); - const unpackSmartQuery = memoize(query => { return expressionParser.map(query, e => { if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "Q") @@ -48,7 +42,7 @@ const unpackSmartQuery = memoize(query => { }); }); -function init(args) { +export function init(args) { return new Promise((resolve, reject) => { if (!window.authorizer.hasAccess("devices", 2)) return void reject(new Error("You are not authorized to view this page")); @@ -138,8 +132,11 @@ function renderTable( }, m("td", checkbox), parameters.map(p => { - const attrs = getChildAttrs(p, device); - const comp = m(components.get(attrs.type || "parameter"), attrs); + const comp = m.context( + { device: device, parameter: p.parameter }, + p.type || "parameter", + p + ); return m("td", comp); }), m( @@ -339,71 +336,74 @@ function renderActions(selected) { return m(".actions-bar", buttons); } -const component = { - view: vnode => { - document.title = "Devices - GenieACS"; +export function component() { + return { + view: vnode => { + document.title = "Devices - GenieACS"; - function showMore() { - vnode.state.showCount = (vnode.state.showCount || PAGE_SIZE) + PAGE_SIZE; - m.redraw(); - } - - function onFilterChanged(filter) { - const ops = { filter }; - if (vnode.attrs.sort) ops.sort = vnode.attrs.sort; - m.route.set("/devices", ops); - } - - function onSortChange(sort) { - const ops = { sort }; - if (vnode.attrs.filter) ops.filter = vnode.attrs.filter; - m.route.set("/devices", ops); - } - - const sort = vnode.attrs.sort ? memoizedJsonParse(vnode.attrs.sort) : {}; - let filter = vnode.attrs.filter ? memoizedParse(vnode.attrs.filter) : true; - filter = unpackSmartQuery(filter); - - const devs = store.fetch("devices", filter, { - limit: vnode.state.showCount || PAGE_SIZE, - sort: sort - }); - const count = store.count("devices", filter); - - const selected = new Set(); - if (vnode.state.selected) { - for (const d of devs.value) { - if (vnode.state.selected.has(d["DeviceID.ID"].value[0])) - selected.add(d["DeviceID.ID"].value[0]); + function showMore() { + vnode.state.showCount = + (vnode.state.showCount || PAGE_SIZE) + PAGE_SIZE; + m.redraw(); } + + function onFilterChanged(filter) { + const ops = { filter }; + if (vnode.attrs.sort) ops.sort = vnode.attrs.sort; + m.route.set("/devices", ops); + } + + function onSortChange(sort) { + const ops = { sort }; + if (vnode.attrs.filter) ops.filter = vnode.attrs.filter; + m.route.set("/devices", ops); + } + + const sort = vnode.attrs.sort ? memoizedJsonParse(vnode.attrs.sort) : {}; + let filter = vnode.attrs.filter + ? memoizedParse(vnode.attrs.filter) + : true; + filter = unpackSmartQuery(filter); + + const devs = store.fetch("devices", filter, { + limit: vnode.state.showCount || PAGE_SIZE, + sort: sort + }); + const count = store.count("devices", filter); + + const selected = new Set(); + if (vnode.state.selected) { + for (const 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; + + const downloadUrl = getDownloadUrl( + vnode.attrs.filter, + vnode.attrs.indexParameters + ); + + return [ + m("h1", "Listing devices"), + m(filterComponent, { + resource: "devices", + filter: vnode.attrs.filter, + onChange: onFilterChanged + }), + renderTable( + devs, + vnode.attrs.indexParameters, + count.value, + selected, + showMore, + downloadUrl, + sort, + onSortChange + ), + renderActions(selected) + ]; } - vnode.state.selected = selected; - - const downloadUrl = getDownloadUrl( - vnode.attrs.filter, - vnode.attrs.indexParameters - ); - - return [ - m("h1", "Listing devices"), - m(filterComponent, { - resource: "devices", - filter: vnode.attrs.filter, - onChange: onFilterChanged - }), - renderTable( - devs, - vnode.attrs.indexParameters, - count.value, - selected, - showMore, - downloadUrl, - sort, - onSortChange - ), - renderActions(selected) - ]; - } -}; - -export { init, component }; + }; +} diff --git a/ui/drawer-component.js b/ui/drawer-component.js index ada2109..661516e 100644 --- a/ui/drawer-component.js +++ b/ui/drawer-component.js @@ -312,187 +312,188 @@ function renderNotifications(notifs) { return notificationElements; } -const component = { - view: vnode => { - const queue = taskQueue.getQueue(); - const staging = taskQueue.getStaging(); - const notifs = notifications.getNotifications(); +export default function component() { + return { + view: vnode => { + const queue = taskQueue.getQueue(); + const staging = taskQueue.getStaging(); + const notifs = notifications.getNotifications(); - let drawerElement, statusElement; - const notificationElements = renderNotifications(notifs); - const stagingElements = renderStaging(staging); - const queueElements = renderQueue(queue); + let drawerElement, statusElement; + const notificationElements = renderNotifications(notifs); + const stagingElements = renderStaging(staging); + const queueElements = renderQueue(queue); - function repositionNotifications() { - let top = 10; - for (const c of notificationElements) { - c.dom.style.top = top; - top += c.dom.offsetHeight + 10; + function repositionNotifications() { + let top = 10; + for (const 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 (const s of stagingElements) - height = Math.max(height, s.dom.offsetTop + s.dom.offsetHeight); - } else if (vnode.state.mouseIn) { - for (const c of drawerElement.children) - height = Math.max(height, c.dom.offsetTop + c.dom.offsetHeight); + function resizeDrawer() { + let height = + statusElement.dom.offsetTop + statusElement.dom.offsetHeight; + if (stagingElements.length) { + for (const s of stagingElements) + height = Math.max(height, s.dom.offsetTop + s.dom.offsetHeight); + } else if (vnode.state.mouseIn) { + for (const c of drawerElement.children) + height = Math.max(height, c.dom.offsetTop + c.dom.offsetHeight); + } + drawerElement.dom.style.height = height; } - drawerElement.dom.style.height = height; - } - if (stagingElements.length + queueElements.length) { - const statusCount = { queued: 0, pending: 0, fault: 0, stale: 0 }; - for (const t of queue) statusCount[t.status] += 1; + if (stagingElements.length + queueElements.length) { + const statusCount = { queued: 0, pending: 0, fault: 0, stale: 0 }; + for (const t of queue) statusCount[t.status] += 1; - const actions = m( - ".actions", - m( - "button.primary", - { - title: "Commit queued tasks", - disabled: !statusCount.queued, - onclick: () => { - const tasks = Array.from(taskQueue.getQueue()).filter( - t => t.status === "queued" - ); - taskQueue - .commit( - tasks, - (deviceId, err, connectionRequestStatus, tasks2) => { - if (err) { - notifications.push( - "error", - `${deviceId}: ${err.message}` - ); - return; - } - - if (connectionRequestStatus !== "OK") { - notifications.push( - "error", - `${deviceId}: ${connectionRequestStatus}` - ); - return; - } - - for (const t of tasks2) { - if (t.status === "stale") { + const actions = m( + ".actions", + m( + "button.primary", + { + title: "Commit queued tasks", + disabled: !statusCount.queued, + onclick: () => { + const tasks = Array.from(taskQueue.getQueue()).filter( + t => t.status === "queued" + ); + taskQueue + .commit( + tasks, + (deviceId, err, connectionRequestStatus, tasks2) => { + if (err) { notifications.push( "error", - `${deviceId}: No contact from device` - ); - return; - } else if (t.status === "fault") { - notifications.push( - "error", - `${deviceId}: Task(s) faulted` + `${deviceId}: ${err.message}` ); return; } - } - notifications.push( - "success", - `${deviceId}: Task(s) committed` - ); - } - ) - .then(() => { - store.fulfill(0, Date.now()); - }); + if (connectionRequestStatus !== "OK") { + notifications.push( + "error", + `${deviceId}: ${connectionRequestStatus}` + ); + return; + } + + for (const 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()); + }); + } + }, + "Commit" + ), + m( + "button", + { + title: "Clear tasks", + onclick: taskQueue.clear, + disabled: !queueElements.length + }, + "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.fault", + { class: statusCount.fault ? "active" : "" }, + `Fault: ${statusCount.fault}` + ), + 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); + }); } }, - "Commit" - ), + statusElement, + stagingElements.length ? stagingElements : m(".queue", queueElements) + ); + } + + return m( + "div.drawer-wrapper", + drawerElement, m( - "button", + "div.notifications-wrapper", { - title: "Clear tasks", - onclick: taskQueue.clear, - disabled: !queueElements.length + key: "notifications", + style: "position: relative;", + onupdate: repositionNotifications, + oncreate: repositionNotifications }, - "Clear" + notificationElements ) ); - - 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.fault", - { class: statusCount.fault ? "active" : "" }, - `Fault: ${statusCount.fault}` - ), - 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; + }; +} diff --git a/ui/error-page.js b/ui/error-page.js index afe4f49..63ee67b 100644 --- a/ui/error-page.js +++ b/ui/error-page.js @@ -1,12 +1,12 @@ "use strict"; -import m from "mithril"; +import { m } from "./components"; -const component = { - view: function(vnode) { - document.title = "Error! - GenieACS"; - return m("p.error", vnode.attrs.error); - } -}; - -export { component }; +export function component() { + return { + view: function(vnode) { + document.title = "Error! - GenieACS"; + return m("p.error", vnode.attrs.error); + } + }; +} diff --git a/ui/faults-page.js b/ui/faults-page.js index 6833d47..ad0d047 100644 --- a/ui/faults-page.js +++ b/ui/faults-page.js @@ -1,6 +1,6 @@ "use strict"; -import m from "mithril"; +import { m } from "./components"; import config from "./config"; import filterComponent from "./filter-component"; import * as store from "./store"; @@ -9,7 +9,6 @@ import memoize from "../lib/common/memoize"; import * as smartQuery from "./smart-query"; import { map, parse } from "../lib/common/expression-parser"; import { loadYaml, yaml } from "./dynamic-loader"; -import longTextComponent from "./long-text-component"; const PAGE_SIZE = config.ui.pageSize || 10; @@ -47,7 +46,7 @@ const unpackSmartQuery = memoize(query => { }); }); -function init(args) { +export function init(args) { if (!window.authorizer.hasAccess("faults", 2)) { return Promise.reject( new Error("You are not authorized to view this page") @@ -136,17 +135,13 @@ function renderTable( const tds = [m("td", checkbox)]; for (const attr of attributes) { - if (attr.id === "device") { + if (attr.id === "device") tds.push(m("td", m("a", { href: deviceHref }, f[attr.id]))); - } else if (attr.id === "timestamp") { + else if (attr.id === "timestamp") tds.push(m("td", new Date(f[attr.id]).toLocaleString())); - } else if (attr.id === "detail") { - tds.push( - m("td", m(longTextComponent, { text: yaml.stringify(f[attr.id]) })) - ); - } else { - tds.push(m("td", f[attr.id])); - } + else if (attr.id === "detail") + tds.push(m("td", m("long-text", { text: yaml.stringify(f[attr.id]) }))); + else tds.push(m("td", f[attr.id])); } rows.push( @@ -234,64 +229,67 @@ function renderTable( ]; } -const component = { - view: vnode => { - document.title = "Faults - GenieACS"; +export function component() { + return { + view: vnode => { + document.title = "Faults - GenieACS"; - function showMore() { - vnode.state.showCount = (vnode.state.showCount || PAGE_SIZE) + PAGE_SIZE; - m.redraw(); + function showMore() { + vnode.state.showCount = + (vnode.state.showCount || PAGE_SIZE) + PAGE_SIZE; + m.redraw(); + } + + function onFilterChanged(filter) { + const ops = { filter }; + if (vnode.attrs.sort) ops.sort = vnode.attrs.sort; + m.route.set("/faults", ops); + } + + function onSortChange(sort) { + const ops = { sort }; + if (vnode.attrs.filter) ops.filter = vnode.attrs.filter; + m.route.set("/faults", ops); + } + + const sort = vnode.attrs.sort ? memoizedJsonParse(vnode.attrs.sort) : {}; + let filter = vnode.attrs.filter + ? memoizedParse(vnode.attrs.filter) + : true; + filter = unpackSmartQuery(filter); + + const faults = store.fetch("faults", filter, { + limit: vnode.state.showCount || PAGE_SIZE, + sort: sort + }); + const count = store.count("faults", filter); + + const selected = new Set(); + if (vnode.state.selected) { + for (const f of faults.value) + if (vnode.state.selected.has(f["_id"])) selected.add(f["_id"]); + } + vnode.state.selected = selected; + + const downloadUrl = getDownloadUrl(vnode.attrs.filter); + + return [ + m("h1", "Listing faults"), + m(filterComponent, { + resource: "faults", + filter: vnode.attrs.filter, + onChange: onFilterChanged + }), + renderTable( + faults, + count.value, + selected, + showMore, + downloadUrl, + sort, + onSortChange + ) + ]; } - - function onFilterChanged(filter) { - const ops = { filter }; - if (vnode.attrs.sort) ops.sort = vnode.attrs.sort; - m.route.set("/faults", ops); - } - - function onSortChange(sort) { - const ops = { sort }; - if (vnode.attrs.filter) ops.filter = vnode.attrs.filter; - m.route.set("/faults", ops); - } - - const sort = vnode.attrs.sort ? memoizedJsonParse(vnode.attrs.sort) : {}; - let filter = vnode.attrs.filter ? memoizedParse(vnode.attrs.filter) : true; - filter = unpackSmartQuery(filter); - - const faults = store.fetch("faults", filter, { - limit: vnode.state.showCount || PAGE_SIZE, - sort: sort - }); - const count = store.count("faults", filter); - - const selected = new Set(); - if (vnode.state.selected) { - for (const f of faults.value) - if (vnode.state.selected.has(f["_id"])) selected.add(f["_id"]); - } - vnode.state.selected = selected; - - const downloadUrl = getDownloadUrl(vnode.attrs.filter); - - return [ - m("h1", "Listing faults"), - m(filterComponent, { - resource: "faults", - filter: vnode.attrs.filter, - onChange: onFilterChanged - }), - renderTable( - faults, - count.value, - selected, - showMore, - downloadUrl, - sort, - onSortChange - ) - ]; - } -}; - -export { init, component }; + }; +} diff --git a/ui/files-page.js b/ui/files-page.js index 020ee47..ac47ab6 100644 --- a/ui/files-page.js +++ b/ui/files-page.js @@ -1,7 +1,6 @@ "use strict"; -import m from "mithril"; - +import { m } from "./components"; import config from "./config"; import filterComponent from "./filter-component"; import * as store from "./store"; @@ -110,7 +109,7 @@ const getDownloadUrl = memoize(filter => { })}`; }); -function init(args) { +export function init(args) { if (!window.authorizer.hasAccess("files", 2)) { return Promise.reject( new Error("You are not authorized to view this page") @@ -319,65 +318,68 @@ function renderTable( ]; } -const component = { - view: vnode => { - document.title = "Files - GenieACS"; +export function component() { + return { + view: vnode => { + document.title = "Files - GenieACS"; - function showMore() { - vnode.state.showCount = (vnode.state.showCount || PAGE_SIZE) + PAGE_SIZE; - m.redraw(); + function showMore() { + vnode.state.showCount = + (vnode.state.showCount || PAGE_SIZE) + PAGE_SIZE; + m.redraw(); + } + + function onFilterChanged(filter) { + const ops = { filter }; + if (vnode.attrs.sort) ops.sort = vnode.attrs.sort; + m.route.set(m.route.get(), ops); + } + + function onSortChange(sort) { + const ops = { sort }; + if (vnode.attrs.filter) ops.filter = vnode.attrs.filter; + m.route.set(m.route.get(), ops); + } + + const sort = vnode.attrs.sort ? memoizedJsonParse(vnode.attrs.sort) : {}; + let filter = vnode.attrs.filter + ? memoizedParse(vnode.attrs.filter) + : true; + filter = unpackSmartQuery(filter); + + const files = store.fetch("files", filter, { + limit: vnode.state.showCount || PAGE_SIZE, + sort: sort + }); + + const count = store.count("files", filter); + + const selected = new Set(); + if (vnode.state.selected) { + for (const file of files.value) + if (vnode.state.selected.has(file["_id"])) selected.add(file["_id"]); + } + vnode.state.selected = selected; + + const downloadUrl = getDownloadUrl(vnode.attrs.filter); + + return [ + m("h1", "Listing files"), + m(filterComponent, { + resource: "files", + filter: vnode.attrs.filter, + onChange: onFilterChanged + }), + renderTable( + files, + count.value, + selected, + showMore, + downloadUrl, + sort, + onSortChange + ) + ]; } - - function onFilterChanged(filter) { - const ops = { filter }; - if (vnode.attrs.sort) ops.sort = vnode.attrs.sort; - m.route.set(m.route.get(), ops); - } - - function onSortChange(sort) { - const ops = { sort }; - if (vnode.attrs.filter) ops.filter = vnode.attrs.filter; - m.route.set(m.route.get(), ops); - } - - const sort = vnode.attrs.sort ? memoizedJsonParse(vnode.attrs.sort) : {}; - let filter = vnode.attrs.filter ? memoizedParse(vnode.attrs.filter) : true; - filter = unpackSmartQuery(filter); - - const files = store.fetch("files", filter, { - limit: vnode.state.showCount || PAGE_SIZE, - sort: sort - }); - - const count = store.count("files", filter); - - const selected = new Set(); - if (vnode.state.selected) { - for (const file of files.value) - if (vnode.state.selected.has(file["_id"])) selected.add(file["_id"]); - } - vnode.state.selected = selected; - - const downloadUrl = getDownloadUrl(vnode.attrs.filter); - - return [ - m("h1", "Listing files"), - m(filterComponent, { - resource: "files", - filter: vnode.attrs.filter, - onChange: onFilterChanged - }), - renderTable( - files, - count.value, - selected, - showMore, - downloadUrl, - sort, - onSortChange - ) - ]; - } -}; - -export { init, component }; + }; +} diff --git a/ui/filter-component.js b/ui/filter-component.js index 5c28ae6..e0211fe 100644 --- a/ui/filter-component.js +++ b/ui/filter-component.js @@ -56,60 +56,63 @@ const splitFilter = memoize(filter => { return list; }); -const component = { - view: vnode => { - if (!vnode.state.filterList || vnode.attrs.filter !== vnode.state.filter) { - vnode.state.filterInvalid = 0; - vnode.state.filter = vnode.attrs.filter; - vnode.state.filterList = splitFilter(vnode.attrs.filter); - } +export default function component() { + return { + view: vnode => { + if ( + !vnode.state.filterList || + vnode.attrs.filter !== vnode.state.filter + ) { + vnode.state.filterInvalid = 0; + vnode.state.filter = vnode.attrs.filter; + vnode.state.filterList = splitFilter(vnode.attrs.filter); + } - function onChange() { - vnode.state.filterInvalid = 0; - vnode.state.filterList = vnode.state.filterList.filter(f => f); - let filter = vnode.state.filterList.map((f, idx) => { - try { - return parseFilter(vnode.attrs.resource, f); - } catch (err) { - vnode.state.filterInvalid |= 1 << idx; - } - return null; - }); - vnode.state.filterList.push(""); + function onChange() { + vnode.state.filterInvalid = 0; + vnode.state.filterList = vnode.state.filterList.filter(f => f); + let filter = vnode.state.filterList.map((f, idx) => { + try { + return parseFilter(vnode.attrs.resource, f); + } catch (err) { + vnode.state.filterInvalid |= 1 << idx; + } + return null; + }); + vnode.state.filterList.push(""); - if (!vnode.state.filterInvalid) { - delete vnode.state.filter; - if (filter.length === 0) { - vnode.attrs.onChange(""); - } else { - if (filter.length > 1) filter = ["AND"].concat(filter); - else filter = filter[0]; - vnode.attrs.onChange(stringify(filter)); + if (!vnode.state.filterInvalid) { + delete vnode.state.filter; + if (filter.length === 0) { + vnode.attrs.onChange(""); + } else { + if (filter.length > 1) filter = ["AND"].concat(filter); + else filter = filter[0]; + vnode.attrs.onChange(stringify(filter)); + } } } + + return m( + "div.filter", + [m("b", "Filter")].concat( + vnode.state.filterList.map((fltr, idx) => { + return m("input", { + type: "text", + class: `${(vnode.state.filterInvalid >> idx) & 1 ? "error" : ""}`, + value: fltr, + onchange: e => { + vnode.state.filterList = vnode.state.filterList.slice(); + vnode.state.filterList[idx] = e.target.value.trim(); + onChange(); + }, + oncreate: vn => { + getAutocomplete(vnode.attrs.resource).attach(vn.dom); + } + }); + }) + ) + ); } - - return m( - "div.filter", - [m("b", "Filter")].concat( - vnode.state.filterList.map((fltr, idx) => { - return m("input", { - type: "text", - class: `${(vnode.state.filterInvalid >> idx) & 1 ? "error" : ""}`, - value: fltr, - onchange: e => { - vnode.state.filterList = vnode.state.filterList.slice(); - vnode.state.filterList[idx] = e.target.value.trim(); - onChange(); - }, - oncreate: vn => { - getAutocomplete(vnode.attrs.resource).attach(vn.dom); - } - }); - }) - ) - ); - } -}; - -export default component; + }; +} diff --git a/ui/layout.js b/ui/layout.js index 84d6966..9162980 100644 --- a/ui/layout.js +++ b/ui/layout.js @@ -1,7 +1,6 @@ "use strict"; import m from "mithril"; - import menu from "./menu"; import drawerComponent from "./drawer-component"; import userMenu from "./user-menu"; @@ -16,29 +15,29 @@ const adminPages = [ "config" ]; -const layout = { - view: vnode => { - let sideMenu, group; - if (adminPages.includes(vnode.attrs.page)) { - group = "admin"; - sideMenu = m(adminMenu, { page: vnode.attrs.page }); +export default function layout() { + return { + view: vnode => { + let sideMenu, group; + if (adminPages.includes(vnode.attrs.page)) { + group = "admin"; + sideMenu = m(adminMenu, { page: vnode.attrs.page }); + } + + return [ + m("#header", [ + m("img.logo", { src: "logo.svg" }), + m(userMenu), + m(menu, { page: group || vnode.attrs.page }), + m(drawerComponent) + ]), + m( + "#content-wrapper", + sideMenu, + m("#content", { class: `page-${vnode.attrs.page}` }, [vnode.children]) + ), + overlay.render() + ]; } - - return [ - m("#header", [ - m("img.logo", { src: "logo.svg" }), - m(userMenu), - m(menu, { page: group || vnode.attrs.page }), - m(drawerComponent) - ]), - m( - "#content-wrapper", - sideMenu, - m("#content", { class: `page-${vnode.attrs.page}` }, [vnode.children]) - ), - overlay.render() - ]; - } -}; - -export default layout; + }; +} diff --git a/ui/login-page.js b/ui/login-page.js index ee12d77..9306ca3 100644 --- a/ui/login-page.js +++ b/ui/login-page.js @@ -1,72 +1,71 @@ "use strict"; -import m from "mithril"; - +import { m } from "./components"; import * as store from "./store"; import * as notifications from "./notifications"; -function init(args) { +export function init(args) { return Promise.resolve(args); } -const component = { - view: vnode => { - if (window.username) m.route.set(vnode.attrs.continue || "/"); +export function component() { + return { + view: vnode => { + if (window.username) m.route.set(vnode.attrs.continue || "/"); - document.title = "Login - GenieACS"; - return [ - m("h1", "Log in"), - m( - "form", + document.title = "Login - GenieACS"; + return [ + m("h1", "Log in"), m( - "p", - m("label", { for: "username" }, "Username"), - m("input", { - name: "username", - type: "text", - value: vnode.state.username, - oncreate: vnode2 => { - vnode2.dom.focus(); - }, - oninput: m.withAttr("value", v => (vnode.state.username = v)) - }) - ), - m( - "p", - m("label", { for: "password" }, "Password"), - m("input", { - name: "password", - type: "password", - value: vnode.state.password, - oninput: m.withAttr("value", v => (vnode.state.password = v)) - }) - ), - m( - "p", + "form", m( - "button.primary", - { - type: "submit", - onclick: e => { - e.target.disabled = true; - store - .logIn(vnode.state.username, vnode.state.password) - .then(() => { - location.reload(); - }) - .catch(err => { - notifications.push("error", err.message); - e.target.disabled = false; - }); - return false; - } - }, - "Login" + "p", + m("label", { for: "username" }, "Username"), + m("input", { + name: "username", + type: "text", + value: vnode.state.username, + oncreate: vnode2 => { + vnode2.dom.focus(); + }, + oninput: m.withAttr("value", v => (vnode.state.username = v)) + }) + ), + m( + "p", + m("label", { for: "password" }, "Password"), + m("input", { + name: "password", + type: "password", + value: vnode.state.password, + oninput: m.withAttr("value", v => (vnode.state.password = v)) + }) + ), + m( + "p", + m( + "button.primary", + { + type: "submit", + onclick: e => { + e.target.disabled = true; + store + .logIn(vnode.state.username, vnode.state.password) + .then(() => { + location.reload(); + }) + .catch(err => { + notifications.push("error", err.message); + e.target.disabled = false; + }); + return false; + } + }, + "Login" + ) ) ) - ) - ]; - } -}; - -export { init, component }; + ]; + } + }; +} diff --git a/ui/long-text-component.js b/ui/long-text-component.js index 07afb32..451ad33 100644 --- a/ui/long-text-component.js +++ b/ui/long-text-component.js @@ -3,54 +3,54 @@ import m from "mithril"; import * as overlay from "./overlay"; -const component = { - view: vnode => { - const text = vnode.attrs.text; - const element = vnode.attrs.element || "span"; +export default function component() { + return { + view: vnode => { + const text = vnode.attrs.text; + const element = vnode.attrs.element || "span"; - function overflowed(_vnode) { - if (_vnode.dom.classList.contains("long-text-overflowed")) return; - _vnode.dom.classList.add("long-text-overflowed"); - _vnode.dom.onclick = e => { - overlay.open(() => { - return m("textarea.long-text", { - value: text, - cols: 80, - rows: 24, - readonly: "", - oncreate: vnode2 => { - vnode2.dom.focus(); - vnode2.dom.select(); - } + function overflowed(_vnode) { + if (_vnode.dom.classList.contains("long-text-overflowed")) return; + _vnode.dom.classList.add("long-text-overflowed"); + _vnode.dom.onclick = e => { + overlay.open(() => { + return m("textarea.long-text", { + value: text, + cols: 80, + rows: 24, + readonly: "", + oncreate: vnode2 => { + vnode2.dom.focus(); + vnode2.dom.select(); + } + }); }); - }); - // prevent index page selection - e.stopPropagation(); - m.redraw(); - }; + // prevent index page selection + e.stopPropagation(); + m.redraw(); + }; + } + + return m( + element, + { + oncreate: vnode2 => { + if (vnode2.dom.clientWidth !== vnode2.dom.scrollWidth) + overflowed(vnode2); + }, + onupdate: vnode2 => { + if (vnode2.dom.clientWidth === vnode2.dom.scrollWidth) { + vnode2.dom.classList.remove("long-text-overflowed"); + vnode2.dom.onclick = null; + } else { + overflowed(vnode2); + } + }, + class: "long-text", + title: text + }, + text + ); } - - return m( - element, - { - oncreate: vnode2 => { - if (vnode2.dom.clientWidth !== vnode2.dom.scrollWidth) - overflowed(vnode2); - }, - onupdate: vnode2 => { - if (vnode2.dom.clientWidth === vnode2.dom.scrollWidth) { - vnode2.dom.classList.remove("long-text-overflowed"); - vnode2.dom.onclick = null; - } else { - overflowed(vnode2); - } - }, - class: "long-text", - title: text - }, - text - ); - } -}; - -export default component; + }; +} diff --git a/ui/menu.js b/ui/menu.js index 2a2e196..2963477 100644 --- a/ui/menu.js +++ b/ui/menu.js @@ -2,57 +2,62 @@ import m from "mithril"; -const menu = { - view: vnode => { - const active = { [vnode.attrs.page]: "active" }; +export default function menu() { + return { + view: vnode => { + const active = { [vnode.attrs.page]: "active" }; - const tabs = []; - if (window.authorizer.hasAccess("devices", 1)) { - tabs.push( - m( - "li", - { class: active["overview"] }, - m("a", { href: "#!/overview" }, "Overview") - ) - ); - } - - if (window.authorizer.hasAccess("devices", 2)) { - tabs.push( - m( - "li", - { class: active["devices"] }, - m("a", { href: "#!/devices" }, "Devices") - ) - ); - } - - if (window.authorizer.hasAccess("faults", 2)) { - tabs.push( - m( - "li", - { class: active["faults"] }, - m("a", { href: "#!/faults" }, "Faults") - ) - ); - } - - const adminPages = ["presets", "provisions", "virtualParameters", "files"]; - for (const page of adminPages) { - if (window.authorizer.hasAccess(page, 2)) { + const tabs = []; + if (window.authorizer.hasAccess("devices", 1)) { tabs.push( m( "li", - { class: active["admin"] }, - m("a", { href: "#!/admin" }, "Admin") + { class: active["overview"] }, + m("a", { href: "#!/overview" }, "Overview") ) ); - break; } + + if (window.authorizer.hasAccess("devices", 2)) { + tabs.push( + m( + "li", + { class: active["devices"] }, + m("a", { href: "#!/devices" }, "Devices") + ) + ); + } + + if (window.authorizer.hasAccess("faults", 2)) { + tabs.push( + m( + "li", + { class: active["faults"] }, + m("a", { href: "#!/faults" }, "Faults") + ) + ); + } + + const adminPages = [ + "presets", + "provisions", + "virtualParameters", + "files" + ]; + for (const page of adminPages) { + if (window.authorizer.hasAccess(page, 2)) { + tabs.push( + m( + "li", + { class: active["admin"] }, + m("a", { href: "#!/admin" }, "Admin") + ) + ); + break; + } + } + + return m("nav", m("ul", tabs)); } - - return m("nav", m("ul", tabs)); - } -}; - -export default menu; + }; +} diff --git a/ui/notifications.js b/ui/notifications.js index 48c3827..d05e7f1 100644 --- a/ui/notifications.js +++ b/ui/notifications.js @@ -4,7 +4,7 @@ import m from "mithril"; const notifications = new Set(); -function push(type, message) { +export function push(type, message) { const n = { type: type, message: message, timestamp: Date.now() }; notifications.add(n); m.redraw(); @@ -14,8 +14,6 @@ function push(type, message) { }, 4000); } -function getNotifications() { +export function getNotifications() { return notifications; } - -export { push, getNotifications }; diff --git a/ui/overview-page.js b/ui/overview-page.js index ae367aa..b8fddb2 100644 --- a/ui/overview-page.js +++ b/ui/overview-page.js @@ -1,6 +1,6 @@ "use strict"; -import m from "mithril"; +import { m } from "./components"; import config from "./config"; import * as store from "./store"; import pieChartComponent from "./pie-chart-component"; @@ -26,7 +26,7 @@ function queryCharts(charts) { return charts; } -const init = function() { +export function init() { if (!window.authorizer.hasAccess("devices", 1)) { return Promise.reject( new Error("You are not authorized to view this page") @@ -34,31 +34,31 @@ const init = function() { } return Promise.resolve({ charts: queryCharts(CHARTS) }); -}; +} -const component = { - view: vnode => { - document.title = "Overview - GenieACS"; - const children = []; - for (const group of Object.values(GROUPS)) { - if (group.label) children.push(m("h1", group.label)); +export function component() { + return { + view: vnode => { + document.title = "Overview - GenieACS"; + const children = []; + for (const group of Object.values(GROUPS)) { + if (group.label) children.push(m("h1", group.label)); - const groupChildren = []; - for (const chartName of Object.values(group.charts)) { - const chart = vnode.attrs.charts[chartName]; - const chartChildren = []; - if (chart.label) chartChildren.push(m("h2", chart.label)); + const groupChildren = []; + for (const chartName of Object.values(group.charts)) { + const chart = vnode.attrs.charts[chartName]; + const chartChildren = []; + if (chart.label) chartChildren.push(m("h2", chart.label)); - chartChildren.push(m(pieChartComponent, { chart: chart })); + chartChildren.push(m(pieChartComponent, { chart: chart })); - groupChildren.push(m(".overview-chart", chartChildren)); + groupChildren.push(m(".overview-chart", chartChildren)); + } + + children.push(m(".overview-chart-group", groupChildren)); } - children.push(m(".overview-chart-group", groupChildren)); + return children; } - - return children; - } -}; - -export { init, component }; + }; +} diff --git a/ui/pie-chart-component.js b/ui/pie-chart-component.js index f7942ab..1b99065 100644 --- a/ui/pie-chart-component.js +++ b/ui/pie-chart-component.js @@ -1,6 +1,6 @@ "use strict"; -import m from "mithril"; +import { m } from "./components"; import { stringify } from "../lib/common/expression-parser"; import memoize from "../lib/common/memoize"; @@ -115,10 +115,10 @@ const drawChart = function(chartData) { ]); }; -const component = { - view: vnode => { - return drawChart(vnode.attrs.chart); - } -}; - -export default component; +export default function component() { + return { + view: vnode => { + return drawChart(vnode.attrs.chart); + } + }; +} diff --git a/ui/presets-page.js b/ui/presets-page.js index 00d4f35..f2c8581 100644 --- a/ui/presets-page.js +++ b/ui/presets-page.js @@ -1,7 +1,6 @@ "use strict"; -import m from "mithril"; - +import { m } from "./components"; import config from "./config"; import filterComponent from "./filter-component"; import * as overlay from "./overlay"; @@ -108,7 +107,7 @@ const getDownloadUrl = memoize(filter => { })}`; }); -function init(args) { +export function init(args) { if (!window.authorizer.hasAccess("presets", 2)) { return Promise.reject( new Error("You are not authorized to view this page") @@ -383,87 +382,90 @@ function renderTable( ]; } -const component = { - view: vnode => { - document.title = "Presets - GenieACS"; +export function component() { + return { + view: vnode => { + document.title = "Presets - GenieACS"; - function showMore() { - vnode.state.showCount = (vnode.state.showCount || PAGE_SIZE) + PAGE_SIZE; - m.redraw(); - } - - function onFilterChanged(filter) { - const ops = { filter }; - if (vnode.attrs.sort) ops.sort = vnode.attrs.sort; - m.route.set(m.route.get(), ops); - } - - function onSortChange(sort) { - const ops = { sort }; - if (vnode.attrs.filter) ops.filter = vnode.attrs.filter; - m.route.set(m.route.get(), ops); - } - - const sort = vnode.attrs.sort ? memoizedJsonParse(vnode.attrs.sort) : {}; - let filter = vnode.attrs.filter ? memoizedParse(vnode.attrs.filter) : true; - filter = unpackSmartQuery(filter); - - const presets = store.fetch("presets", filter, { - limit: vnode.state.showCount || PAGE_SIZE, - sort: sort - }); - const count = store.count("presets", filter); - - const provisions = store.fetch("provisions", true); - if (provisions.fulfilled) { - const provisionAttr = attributes.find(attr => { - return attr.id === "provision"; - }); - - const provisionIds = new Set([ - "refresh", - "value", - "tag", - "reboot", - "reset", - "download", - "instances" - ]); - - for (const p of provisions.value) provisionIds.add(p["_id"]); - - provisionAttr.options = Array.from(provisionIds); - } - - const selected = new Set(); - if (vnode.state.selected) { - for (const preset of presets.value) { - if (vnode.state.selected.has(preset["_id"])) - selected.add(preset["_id"]); + function showMore() { + vnode.state.showCount = + (vnode.state.showCount || PAGE_SIZE) + PAGE_SIZE; + m.redraw(); } + + function onFilterChanged(filter) { + const ops = { filter }; + if (vnode.attrs.sort) ops.sort = vnode.attrs.sort; + m.route.set(m.route.get(), ops); + } + + function onSortChange(sort) { + const ops = { sort }; + if (vnode.attrs.filter) ops.filter = vnode.attrs.filter; + m.route.set(m.route.get(), ops); + } + + const sort = vnode.attrs.sort ? memoizedJsonParse(vnode.attrs.sort) : {}; + let filter = vnode.attrs.filter + ? memoizedParse(vnode.attrs.filter) + : true; + filter = unpackSmartQuery(filter); + + const presets = store.fetch("presets", filter, { + limit: vnode.state.showCount || PAGE_SIZE, + sort: sort + }); + const count = store.count("presets", filter); + + const provisions = store.fetch("provisions", true); + if (provisions.fulfilled) { + const provisionAttr = attributes.find(attr => { + return attr.id === "provision"; + }); + + const provisionIds = new Set([ + "refresh", + "value", + "tag", + "reboot", + "reset", + "download", + "instances" + ]); + + for (const p of provisions.value) provisionIds.add(p["_id"]); + + provisionAttr.options = Array.from(provisionIds); + } + + const selected = new Set(); + if (vnode.state.selected) { + for (const preset of presets.value) { + if (vnode.state.selected.has(preset["_id"])) + selected.add(preset["_id"]); + } + } + vnode.state.selected = selected; + + const downloadUrl = getDownloadUrl(vnode.attrs.filter); + + return [ + m("h1", "Listing presets"), + m(filterComponent, { + resource: "presets", + filter: vnode.attrs.filter, + onChange: onFilterChanged + }), + renderTable( + presets, + count.value, + selected, + showMore, + downloadUrl, + sort, + onSortChange + ) + ]; } - vnode.state.selected = selected; - - const downloadUrl = getDownloadUrl(vnode.attrs.filter); - - return [ - m("h1", "Listing presets"), - m(filterComponent, { - resource: "presets", - filter: vnode.attrs.filter, - onChange: onFilterChanged - }), - renderTable( - presets, - count.value, - selected, - showMore, - downloadUrl, - sort, - onSortChange - ) - ]; - } -}; - -export { init, component }; + }; +} diff --git a/ui/provisions-page.js b/ui/provisions-page.js index 9bb668c..1ad52d3 100644 --- a/ui/provisions-page.js +++ b/ui/provisions-page.js @@ -1,7 +1,6 @@ "use strict"; -import m from "mithril"; - +import { m } from "./components"; import config from "./config"; import filterComponent from "./filter-component"; import * as store from "./store"; @@ -98,7 +97,7 @@ const getDownloadUrl = memoize(filter => { })}`; }); -function init(args) { +export function init(args) { if (!window.authorizer.hasAccess("provisions", 2)) { return Promise.reject( new Error("You are not authorized to view this page") @@ -371,67 +370,70 @@ function renderTable( ]; } -const component = { - view: vnode => { - document.title = "Provisions - GenieACS"; +export function component() { + return { + view: vnode => { + document.title = "Provisions - GenieACS"; - function showMore() { - vnode.state.showCount = (vnode.state.showCount || PAGE_SIZE) + PAGE_SIZE; - m.redraw(); - } - - function onFilterChanged(filter) { - const ops = { filter }; - if (vnode.attrs.sort) ops.sort = vnode.attrs.sort; - m.route.set(m.route.get(), ops); - } - - function onSortChange(sort) { - const ops = { sort }; - if (vnode.attrs.filter) ops.filter = vnode.attrs.filter; - m.route.set(m.route.get(), ops); - } - - const sort = vnode.attrs.sort ? memoizedJsonParse(vnode.attrs.sort) : {}; - let filter = vnode.attrs.filter ? memoizedParse(vnode.attrs.filter) : true; - filter = unpackSmartQuery(filter); - - const provisions = store.fetch("provisions", filter, { - limit: vnode.state.showCount || PAGE_SIZE, - sort: sort - }); - - const count = store.count("provisions", filter); - - const selected = new Set(); - if (vnode.state.selected) { - for (const provision of provisions.value) { - if (vnode.state.selected.has(provision["_id"])) - selected.add(provision["_id"]); + function showMore() { + vnode.state.showCount = + (vnode.state.showCount || PAGE_SIZE) + PAGE_SIZE; + m.redraw(); } + + function onFilterChanged(filter) { + const ops = { filter }; + if (vnode.attrs.sort) ops.sort = vnode.attrs.sort; + m.route.set(m.route.get(), ops); + } + + function onSortChange(sort) { + const ops = { sort }; + if (vnode.attrs.filter) ops.filter = vnode.attrs.filter; + m.route.set(m.route.get(), ops); + } + + const sort = vnode.attrs.sort ? memoizedJsonParse(vnode.attrs.sort) : {}; + let filter = vnode.attrs.filter + ? memoizedParse(vnode.attrs.filter) + : true; + filter = unpackSmartQuery(filter); + + const provisions = store.fetch("provisions", filter, { + limit: vnode.state.showCount || PAGE_SIZE, + sort: sort + }); + + const count = store.count("provisions", filter); + + const selected = new Set(); + if (vnode.state.selected) { + for (const provision of provisions.value) { + if (vnode.state.selected.has(provision["_id"])) + selected.add(provision["_id"]); + } + } + vnode.state.selected = selected; + + const downloadUrl = getDownloadUrl(vnode.attrs.filter); + + return [ + m("h1", "Listing provisions"), + m(filterComponent, { + resource: "provisions", + filter: vnode.attrs.filter, + onChange: onFilterChanged + }), + renderTable( + provisions, + count.value, + selected, + showMore, + downloadUrl, + sort, + onSortChange + ) + ]; } - vnode.state.selected = selected; - - const downloadUrl = getDownloadUrl(vnode.attrs.filter); - - return [ - m("h1", "Listing provisions"), - m(filterComponent, { - resource: "provisions", - filter: vnode.attrs.filter, - onChange: onFilterChanged - }), - renderTable( - provisions, - count.value, - selected, - showMore, - downloadUrl, - sort, - onSortChange - ) - ]; - } -}; - -export { init, component }; + }; +} diff --git a/ui/put-form-component.js b/ui/put-form-component.js index 2b66c2f..02997db 100644 --- a/ui/put-form-component.js +++ b/ui/put-form-component.js @@ -1,6 +1,6 @@ "use strict"; -import m from "mithril"; +import { m } from "./components"; import { codeMirror } from "./dynamic-loader"; const singular = { @@ -93,91 +93,92 @@ function createField(current, attr, focus) { }); } -const component = { - view: vnode => { - const actionHandler = vnode.attrs.actionHandler; - const attributes = vnode.attrs.attributes; - const resource = vnode.attrs.resource; - const base = vnode.attrs.base || {}; - if (!vnode.state.current) { - vnode.state.current = { - isNew: !base["_id"], - object: Object.assign({}, base) - }; - } +export default function component() { + return { + view: vnode => { + const actionHandler = vnode.attrs.actionHandler; + const attributes = vnode.attrs.attributes; + const resource = vnode.attrs.resource; + const base = vnode.attrs.base || {}; + if (!vnode.state.current) { + vnode.state.current = { + isNew: !base["_id"], + object: Object.assign({}, base) + }; + } - const current = vnode.state.current; + const current = vnode.state.current; - const form = []; - let focused = false; - for (const attr of attributes) { - let focus = false; - if (!focused && (current.isNew || attr.id !== "_id")) - focus = focused = true; + const form = []; + let focused = false; + for (const attr of attributes) { + let focus = false; + if (!focused && (current.isNew || attr.id !== "_id")) + focus = focused = true; - form.push( + form.push( + m( + "p", + m("label", { for: attr.id }, attr.label || attr.id), + m("br"), + createField(current, attr, focus) + ) + ); + } + + const submit = m("button.primary", { type: "submit" }, "Save"); + const buttons = [submit]; + + if (!current.isNew) { + buttons.push( + m( + "button.primary", + { + type: "button", + title: `Delete ${singular[resource] || resource}`, + onclick: e => { + e.redraw = false; + e.target.disabled = true; + actionHandler("delete", current.object).then(() => { + e.target.disabled = false; + }); + } + }, + "Delete" + ) + ); + } + + form.push(m(".actions-bar", buttons)); + + const children = [ m( - "p", - m("label", { for: attr.id }, attr.label || attr.id), - m("br"), - createField(current, attr, focus) - ) - ); - } - - const submit = m("button.primary", { type: "submit" }, "Save"); - const buttons = [submit]; - - if (!current.isNew) { - buttons.push( + "h1", + `${current.isNew ? "New" : "Editing"} ${singular[resource] || + resource}` + ), m( - "button.primary", + "form", { - type: "button", - title: `Delete ${singular[resource] || resource}`, - onclick: e => { + onsubmit: e => { e.redraw = false; - e.target.disabled = true; - actionHandler("delete", current.object).then(() => { - e.target.disabled = false; + // const onsubmit = e.target.onsubmit; + e.preventDefault(); + // e.target.onsubmit = null; + submit.dom.disabled = true; + // submit.dom.textContent = "Loading ..."; + actionHandler("save", current.object).then(() => { + // submit.dom.textContent = "Save"; + // e.target.onsubmit = onsubmit; + submit.dom.disabled = false; }); } }, - "Delete" + form ) - ); + ]; + + return m("div.put-form", children); } - - form.push(m(".actions-bar", buttons)); - - const children = [ - m( - "h1", - `${current.isNew ? "New" : "Editing"} ${singular[resource] || resource}` - ), - m( - "form", - { - onsubmit: e => { - e.redraw = false; - // const onsubmit = e.target.onsubmit; - e.preventDefault(); - // e.target.onsubmit = null; - submit.dom.disabled = true; - // submit.dom.textContent = "Loading ..."; - actionHandler("save", current.object).then(() => { - // submit.dom.textContent = "Save"; - // e.target.onsubmit = onsubmit; - submit.dom.disabled = false; - }); - } - }, - form - ) - ]; - - return m("div.put-form", children); - } -}; - -export default component; + }; +} diff --git a/ui/user-menu.js b/ui/user-menu.js index 3a01528..7eb1c31 100644 --- a/ui/user-menu.js +++ b/ui/user-menu.js @@ -5,46 +5,47 @@ import m from "mithril"; import * as store from "./store"; import * as notifications from "./notifications"; -const userMenu = { - view: () => { - if (window.username) { - return m( - "div.user-menu", - window.username, - m( - "button", - { - onclick: e => { - e.target.disabled = true; - store - .logOut() - .then(() => { - location.hash = ""; - location.reload(); - }) - .catch(err => { - e.target.disabled = false; - notifications.push("error", err.message); - }); - return false; - } - }, - "Log out" - ) - ); - } else { - return m( - "div.user-menu", - m( - "a", - { - href: "#!/login?" + m.buildQueryString({ continue: m.route.get() }) - }, - "Log in" - ) - ); +export default function userMenu() { + return { + view: () => { + if (window.username) { + return m( + "div.user-menu", + window.username, + m( + "button", + { + onclick: e => { + e.target.disabled = true; + store + .logOut() + .then(() => { + location.hash = ""; + location.reload(); + }) + .catch(err => { + e.target.disabled = false; + notifications.push("error", err.message); + }); + return false; + } + }, + "Log out" + ) + ); + } else { + return m( + "div.user-menu", + m( + "a", + { + href: + "#!/login?" + m.buildQueryString({ continue: m.route.get() }) + }, + "Log in" + ) + ); + } } - } -}; - -export default userMenu; + }; +} diff --git a/ui/virtual-parameters-page.js b/ui/virtual-parameters-page.js index 81cf278..20354d8 100644 --- a/ui/virtual-parameters-page.js +++ b/ui/virtual-parameters-page.js @@ -1,7 +1,6 @@ "use strict"; -import m from "mithril"; - +import { m } from "./components"; import config from "./config"; import filterComponent from "./filter-component"; import * as store from "./store"; @@ -98,7 +97,7 @@ const getDownloadUrl = memoize(filter => { })}`; }); -function init(args) { +export function init(args) { if (!window.authorizer.hasAccess("virtualParameters", 2)) { return Promise.reject( new Error("You are not authorized to view this page") @@ -381,67 +380,70 @@ function renderTable( ]; } -const component = { - view: vnode => { - document.title = "Virtual Parameters - GenieACS"; +export function component() { + return { + view: vnode => { + document.title = "Virtual Parameters - GenieACS"; - function showMore() { - vnode.state.showCount = (vnode.state.showCount || PAGE_SIZE) + PAGE_SIZE; - m.redraw(); - } - - function onFilterChanged(filter) { - const ops = { filter }; - if (vnode.attrs.sort) ops.sort = vnode.attrs.sort; - m.route.set(m.route.get(), ops); - } - - function onSortChange(sort) { - const ops = { sort }; - if (vnode.attrs.filter) ops.filter = vnode.attrs.filter; - m.route.set(m.route.get(), ops); - } - - const sort = vnode.attrs.sort ? memoizedJsonParse(vnode.attrs.sort) : {}; - let filter = vnode.attrs.filter ? memoizedParse(vnode.attrs.filter) : true; - filter = unpackSmartQuery(filter); - - const virtualParameters = store.fetch("virtualParameters", filter, { - limit: vnode.state.showCount || PAGE_SIZE, - sort: sort - }); - - const count = store.count("virtualParameters", filter); - - const selected = new Set(); - if (vnode.state.selected) { - for (const virtualParameter of virtualParameters.value) { - if (vnode.state.selected.has(virtualParameter["_id"])) - selected.add(virtualParameter["_id"]); + function showMore() { + vnode.state.showCount = + (vnode.state.showCount || PAGE_SIZE) + PAGE_SIZE; + m.redraw(); } + + function onFilterChanged(filter) { + const ops = { filter }; + if (vnode.attrs.sort) ops.sort = vnode.attrs.sort; + m.route.set(m.route.get(), ops); + } + + function onSortChange(sort) { + const ops = { sort }; + if (vnode.attrs.filter) ops.filter = vnode.attrs.filter; + m.route.set(m.route.get(), ops); + } + + const sort = vnode.attrs.sort ? memoizedJsonParse(vnode.attrs.sort) : {}; + let filter = vnode.attrs.filter + ? memoizedParse(vnode.attrs.filter) + : true; + filter = unpackSmartQuery(filter); + + const virtualParameters = store.fetch("virtualParameters", filter, { + limit: vnode.state.showCount || PAGE_SIZE, + sort: sort + }); + + const count = store.count("virtualParameters", filter); + + const selected = new Set(); + if (vnode.state.selected) { + for (const virtualParameter of virtualParameters.value) { + if (vnode.state.selected.has(virtualParameter["_id"])) + selected.add(virtualParameter["_id"]); + } + } + vnode.state.selected = selected; + + const downloadUrl = getDownloadUrl(vnode.attrs.filter); + + return [ + m("h1", "Listing virtual parameters"), + m(filterComponent, { + resource: "virtualParameters", + filter: vnode.attrs.filter, + onChange: onFilterChanged + }), + renderTable( + virtualParameters, + count.value, + selected, + showMore, + downloadUrl, + sort, + onSortChange + ) + ]; } - vnode.state.selected = selected; - - const downloadUrl = getDownloadUrl(vnode.attrs.filter); - - return [ - m("h1", "Listing virtual parameters"), - m(filterComponent, { - resource: "virtualParameters", - filter: vnode.attrs.filter, - onChange: onFilterChanged - }), - renderTable( - virtualParameters, - count.value, - selected, - showMore, - downloadUrl, - sort, - onSortChange - ) - ]; - } -}; - -export { init, component }; + }; +}