From 5a8d93423906adace9592674f4c1a3b33509325b Mon Sep 17 00:00:00 2001 From: Shweelan Samson Date: Wed, 15 Aug 2018 22:10:28 +0300 Subject: [PATCH] Add presets page --- client/app.js | 4 +- client/menu.js | 9 ++ client/presets-page.js | 232 ++++++++++++++++++++++++++++++++++++ client/store.js | 2 +- config.json | 9 ++ server/db.js | 4 + server/mongodb-functions.js | 93 +++++++++++++++ server/server.js | 9 +- 8 files changed, 359 insertions(+), 3 deletions(-) create mode 100644 client/presets-page.js diff --git a/client/app.js b/client/app.js index 11069ab..68d93a7 100644 --- a/client/app.js +++ b/client/app.js @@ -10,6 +10,7 @@ import * as devicesPage from "./devices-page"; import * as devicePage from "./device-page"; import * as errorPage from "./error-page"; import * as faultsPage from "./faults-page"; +import * as presetsPage from "./presets-page"; import Authorizer from "../common/authorizer"; import * as notifications from "./notifications"; @@ -78,5 +79,6 @@ m.route(document.body, "/overview", { "/overview": pagify("overview", overviewPage), "/devices": pagify("devices", devicesPage), "/devices/:id": pagify("devices", devicePage), - "/faults": pagify("faults", faultsPage) + "/faults": pagify("faults", faultsPage), + "/presets": pagify("presets", presetsPage) }); diff --git a/client/menu.js b/client/menu.js index 05b3b94..2f23854 100644 --- a/client/menu.js +++ b/client/menu.js @@ -34,6 +34,15 @@ const menu = { ) ); + if (window.authorizer.hasAccess("presets", 2)) + tabs.push( + m( + "li", + { class: active["presets"] }, + m("a", { href: "#!/presets" }, "Presets") + ) + ); + return m("nav", m("ul", tabs)); } }; diff --git a/client/presets-page.js b/client/presets-page.js new file mode 100644 index 0000000..7e3bd7e --- /dev/null +++ b/client/presets-page.js @@ -0,0 +1,232 @@ +"use strict"; + +import m from "mithril"; + +import config from "./config"; +import filterComponent from "./filter-component"; +import * as store from "./store"; +import * as expression from "../common/expression"; +import * as notifications from "./notifications"; +import memoize from "../common/memoize"; + +const PAGE_SIZE = config.ui.pageSize || 10; + +const memoizedParse = memoize(expression.parse); + +const getDownloadUrl = memoize(filter => { + return `/api/presets.csv?${m.buildQueryString({ + filter: filter, + columns: JSON.stringify({ + Name: "_id", + Channel: "channel", + Weight: "weight", + Schedule: "schedule", + Events: "events", + Precondition: "precondition", + Provision: "provision", + Arguments: "provision_args" + }) + })}`; +}); + +function init(args) { + if (!window.authorizer.hasAccess("presets", 2)) + return Promise.reject( + new Error("You are not authorized to view this page") + ); + + const filter = args.filter; + return Promise.resolve({ filter }); +} + +function renderTable( + presetsResponse, + total, + selected, + showMoreCallback, + downloadUrl +) { + const presets = presetsResponse.value; + const selectAll = m("input", { + type: "checkbox", + checked: presets.length && selected.size === presets.length, + onchange: e => { + for (let preset of presets) + if (e.target.checked) selected.add(preset["_id"]); + else selected.delete(preset["_id"]); + }, + disabled: !total + }); + + const labels = [ + selectAll, + "Name", + "Channel", + "Weight", + "Schedule", + "Events", + "Precondition", + "Provision", + "Arguments", + "" + ].map(l => m("th", l)); + + let rows = []; + for (let preset of presets) { + let checkbox = m("input", { + type: "checkbox", + checked: selected.has(preset["_id"]), + onchange: e => { + if (e.target.checked) selected.add(preset["_id"]); + else selected.delete(preset["_id"]); + }, + onclick: e => { + e.stopPropagation(); + e.redraw = false; + } + }); + + let devicesUrl = "/#!/devices"; + if (preset["precondition"].length) + devicesUrl += `?${m.buildQueryString({ + filter: preset["precondition"] + })}`; + + rows.push( + m( + "tr", + { + onclick: e => { + if (["INPUT", "BUTTON", "A"].includes(e.target.nodeName)) { + e.redraw = false; + return; + } + + if (!selected.delete(preset["_id"])) selected.add(preset["_id"]); + } + }, + m("td", checkbox), + m("td", preset["_id"]), + m("td", preset["channel"]), + m("td", preset["weight"]), + m("td", preset["schedule"]), + m("td", preset["events"]), + m("td", { title: preset["precondition"] }, preset["precondition"]), + m("td", preset["provision"]), + m("td", preset["provision_args"]), + m("td", m("a", { href: devicesUrl }, "Devices")) + ) + ); + } + + if (!rows.length) + rows.push(m("tr.empty", m("td", { colspan: labels.length }, "No presets"))); + + let footerElements = []; + if (total != null) footerElements.push(`${presets.length}/${total}`); + else footerElements.push(`${presets.length}`); + + footerElements.push( + m( + "button", + { + title: "Show more presets", + onclick: showMoreCallback, + disabled: presets.length >= total || !presetsResponse.fulfilled + }, + "More" + ) + ); + + if (downloadUrl) + footerElements.push(m("a.download-csv", { href: downloadUrl }, "Download")); + + let tfoot = m( + "tfoot", + m("tr", m("td", { colspan: labels.length }, footerElements)) + ); + + const buttons = [ + m( + "button.primary", + { + title: "Delete selected presets", + disabled: !selected.size, + onclick: e => { + e.redraw = false; + e.target.disabled = true; + Promise.all( + Array.from(selected).map(id => store.deleteResource("presets", id)) + ) + .then(res => { + notifications.push("success", `${res.length} presets deleted`); + store.fulfill(0, Date.now()); + }) + .catch(err => { + notifications.push("error", err.message); + }); + } + }, + "Delete" + ) + ]; + + return [ + m( + "table.table.highlight", + m("thead", m("tr", labels)), + m("tbody", rows), + tfoot + ), + buttons + ]; +} + +const component = { + view: vnode => { + document.title = "Presets - GenieACS"; + + function showMore() { + vnode.state.showCount = (vnode.state.showCount || PAGE_SIZE) + PAGE_SIZE; + m.redraw(); + } + + function onFilterChanged(filter) { + m.route.set("/presets", { filter }); + } + + const filter = vnode.attrs.filter + ? memoizedParse(vnode.attrs.filter) + : true; + + let presets = store.fetch("presets", filter, { + limit: vnode.state.showCount || PAGE_SIZE + }); + let count = store.count("presets", filter); + + let selected = new Set(); + if (vnode.state.selected) + for (let 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, { + predefined: [ + { parameter: "_id" }, + { parameter: "channel" }, + { parameter: "weight" } + ], + filter: vnode.attrs.filter, + onChange: onFilterChanged + }), + renderTable(presets, count.value, selected, showMore, downloadUrl) + ]; + } +}; + +export { init, component }; diff --git a/client/store.js b/client/store.js index ccc259b..5227ba4 100644 --- a/client/store.js +++ b/client/store.js @@ -23,7 +23,7 @@ const queries = { }; const resources = {}; -for (let r of ["devices", "faults", "files"]) +for (let r of ["devices", "faults", "files", "presets"]) resources[r] = { objects: new Map(), count: new Map(), diff --git a/config.json b/config.json index 32dfac5..126c213 100644 --- a/config.json +++ b/config.json @@ -34,6 +34,12 @@ "pass": true } }, + "presets": { + "access": 3, + "validate": { + "pass": true + } + }, "files": { "access": 3 } @@ -47,6 +53,9 @@ "faults": { "access": 2 }, + "presets": { + "access": 2 + }, "files": { "access": 2 } diff --git a/server/db.js b/server/db.js index dafaf92..d8be805 100644 --- a/server/db.js +++ b/server/db.js @@ -83,6 +83,8 @@ function query(resource, filter, options, callback) { docs = docs.map(d => mongodbFunctions.flattenFault(d)); else if (resource === "tasks") docs = docs.map(d => mongodbFunctions.flattenTask(d)); + else if (resource === "presets") + docs = docs.map(d => mongodbFunctions.flattenPreset(d)); return resolve(docs); }); else @@ -94,6 +96,8 @@ function query(resource, filter, options, callback) { doc = mongodbFunctions.flattenFault(doc); else if (resource === "tasks") doc = mongodbFunctions.flattenTask(doc); + else if (resource === "presets") + doc = mongodbFunctions.flattenPreset(doc); callback(doc); }, err => { diff --git a/server/mongodb-functions.js b/server/mongodb-functions.js index 8be074e..a319dc1 100644 --- a/server/mongodb-functions.js +++ b/server/mongodb-functions.js @@ -356,6 +356,98 @@ function flattenTask(task) { return t; } +function mongoQueryToFilter(query) { + let expressions = []; + function recursive(_query) { + for (let [k, v] of Object.entries(_query)) + if (k[0] == "$") { + if (k == "$and") for (let vv of Object.values(v)) recursive(vv); + else throw new Error(`Operator ${k} not supported`); + } else if (k == "_tags") { + if (typeof v === "object") { + if (Array.isArray(v)) throw new Error(`Invalid type`); + + for (let [kk, vv] of Object.entries(v)) + if (kk == "$ne") + expressions.push(["IS NULL", ["PARAM", `Tags.${vv}`]]); + else throw new Error(`Operator ${kk} not supported`); + } else { + expressions.push(["IS NOT NULL", ["PARAM", `Tags.${v}`]]); + } + } else if (typeof v === "object") { + if (Array.isArray(v)) throw new Error(`Invalid type`); + + for (let [kk, vv] of Object.entries(v)) { + let op; + switch (kk) { + case "$eq": + op = "="; + break; + case "$ne": + op = "<>"; + break; + case "$lt": + op = "<"; + break; + case "$lte": + op = "<="; + break; + case "$gt": + op = ">"; + break; + case "$gte": + op = ">="; + break; + default: + throw new Error(`Operator ${kk} not supported`); + } + expressions.push([op, ["PARAM", k], vv]); + } + } else { + expressions.push(["=", ["PARAM", k], v]); + } + } + recursive(query); + + if (expressions.length > 1) return ["AND"].concat(expressions); + + if (expressions.length == 1) return expressions[0]; + + return expressions; +} + +function flattenPreset(preset) { + let p = Object.assign({}, preset); + if (p.precondition) { + p.precondition = mongoQueryToFilter(JSON.parse(p.precondition)); + p.precondition = p.precondition.length + ? expression.stringify(p.precondition) + : ""; + } + + if (p.events) { + let e = []; + for (let [k, v] of Object.entries(p.events)) e.push(v ? k : `-${k}`); + p.events = e.join(", "); + } + + let provision = p.configurations[0]; + if ( + p.configurations.length != 1 || + provision.type != "provision" || + !provision.name || + !provision.name.length + ) + throw new Error("Invalid preset provision"); + p.provision = provision.name; + p.provision_args = + provision.args && provision.args.length + ? `"${provision.args.join('" "')}"` + : ""; + delete p.configurations; + return p; +} + exports.processDeviceFilter = processDeviceFilter; exports.processTasksFilter = processTasksFilter; exports.processFaultsFilter = processFaultsFilter; @@ -365,3 +457,4 @@ exports.processDeviceSort = processDeviceSort; exports.flattenDevice = flattenDevice; exports.flattenFault = flattenFault; exports.flattenTask = flattenTask; +exports.flattenPreset = flattenPreset; diff --git a/server/server.js b/server/server.js index 73a84ab..dfda269 100644 --- a/server/server.js +++ b/server/server.js @@ -26,7 +26,14 @@ function getPermissionSets(roles) { p = Object.assign({}, p); for (let [k, v] of Object.entries(p)) { p[k] = v = Object.assign({}, v); - if (["devices", "faults", "files"].includes(k)) + if ( + [ + "devices", + "faults", + "presets", + "files" + ].includes(k) + ) if (v.filter == null || v.filter === "") v.filter = true; else { v.filter = expression.parse(v.filter);