diff --git a/client/app.js b/client/app.js index c88bad5..629dccc 100644 --- a/client/app.js +++ b/client/app.js @@ -12,6 +12,7 @@ import * as errorPage from "./error-page"; import * as faultsPage from "./faults-page"; import * as presetsPage from "./presets-page"; import * as provisionsPage from "./provisions-page"; +import * as virtualParametersPage from "./virtual-parameters-page"; import Authorizer from "../common/authorizer"; import * as notifications from "./notifications"; @@ -82,5 +83,6 @@ m.route(document.body, "/overview", { "/devices/:id": pagify("devices", devicePage), "/faults": pagify("faults", faultsPage), "/presets": pagify("presets", presetsPage), - "/provisions": pagify("provisions", provisionsPage) + "/provisions": pagify("provisions", provisionsPage), + "/virtualParameters": pagify("virtualParameters", virtualParametersPage) }); diff --git a/client/components/put-form.js b/client/components/put-form.js index 37de10e..5d70b9f 100644 --- a/client/components/put-form.js +++ b/client/components/put-form.js @@ -6,7 +6,8 @@ import "codemirror/mode/javascript/javascript"; const singular = { presets: "preset", - provisions: "provision" + provisions: "provision", + virtualParameters: "virtual parameter" }; function createField(current, attr, focus) { diff --git a/client/menu.js b/client/menu.js index fa15deb..c77b414 100644 --- a/client/menu.js +++ b/client/menu.js @@ -52,6 +52,15 @@ const menu = { ) ); + if (window.authorizer.hasAccess("virtualParameters", 2)) + tabs.push( + m( + "li", + { class: active["virtualParameters"] }, + m("a", { href: "#!/virtualParameters" }, "Virtual Parameters") + ) + ); + return m("nav", m("ul", tabs)); } }; diff --git a/client/smart-query.js b/client/smart-query.js index 771e32d..ebabea6 100644 --- a/client/smart-query.js +++ b/client/smart-query.js @@ -19,6 +19,9 @@ let resources = { }, provisions: { ID: { parameter: ["PARAM", "_id"], type: "string" } + }, + virtualParameters: { + ID: { parameter: ["PARAM", "_id"], type: "string" } } }; diff --git a/client/store.js b/client/store.js index f6c7ba5..052a0db 100644 --- a/client/store.js +++ b/client/store.js @@ -28,7 +28,8 @@ for (let r of [ "faults", "files", "presets", - "provisions" + "provisions", + "virtualParameters" ]) resources[r] = { objects: new Map(), diff --git a/client/virtual-parameters-page.js b/client/virtual-parameters-page.js new file mode 100644 index 0000000..f6e23a6 --- /dev/null +++ b/client/virtual-parameters-page.js @@ -0,0 +1,401 @@ +"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"; +import putForm from "./components/put-form"; +import * as overlay from "./overlay"; +import * as smartQuery from "./smart-query"; +import * as expressionParser from "../common/expression-parser"; + +const PAGE_SIZE = config.ui.pageSize || 10; + +const memoizedParse = memoize(expression.parse); +const memoizedJsonParse = memoize(JSON.parse); + +const attributes = [ + { id: "_id", label: "Name" }, + { id: "script", label: "Script", type: "code" } +]; + +const unpackSmartQuery = memoize(query => { + return expressionParser.map(query, e => { + if (Array.isArray(e) && e[0] === "FUNC" && e[1] === "Q") + return smartQuery.unpack("virtualParameters", e[2], e[3]); + return e; + }); +}); + +function putActionHandler(action, object, isNew) { + if (action === "save") { + let id = object["_id"]; + delete object["_id"]; + + if (!id) return notifications.push("error", "ID can not be empty"); + + store + .resourceExists("virtualParameters", id) + .then(exists => { + if (exists && isNew) { + notifications.push("error", "Virtual parameter already exists"); + store.fulfill(0, Date.now()); + return; + } + + if (!exists && !isNew) { + notifications.push("error", "Virtual parameter already deleted"); + store.fulfill(0, Date.now()); + return; + } + + store + .putResource("virtualParameters", id, object) + .then(() => { + notifications.push( + "success", + `Virtual parameter ${exists ? "updated" : "created"}` + ); + store.fulfill(0, Date.now()); + }) + .catch(err => { + notifications.push("error", err.message); + }); + }) + .catch(err => { + notifications.push("error", err.message); + }); + } else if (action === "delete") { + store + .deleteResource("virtualParameters", object["_id"]) + .then(() => { + notifications.push("success", "Virtual parameter deleted"); + store.fulfill(0, Date.now()); + }) + .catch(err => { + notifications.push("error", err.message); + }); + } else { + throw new Error("Undefined action"); + } +} + +let formData = { + resource: "virtualParameters", + attributes: attributes +}; + +const getDownloadUrl = memoize(filter => { + let cols = {}; + for (let attr of attributes) cols[attr.label] = attr.id; + return `/api/virtualParameters.csv?${m.buildQueryString({ + filter: filter, + columns: JSON.stringify(cols) + })}`; +}); + +function init(args) { + if (!window.authorizer.hasAccess("virtualParameters", 2)) + return Promise.reject( + new Error("You are not authorized to view this page") + ); + + const sort = args.sort; + const filter = args.filter; + return Promise.resolve({ filter, sort }); +} + +function renderTable( + virtualParametersResponse, + total, + selected, + showMoreCallback, + downloadUrl, + sort, + onSortChange +) { + const virtualParameters = virtualParametersResponse.value; + const selectAll = m("input", { + type: "checkbox", + checked: + virtualParameters.length && selected.size === virtualParameters.length, + onchange: e => { + for (let virtualParameter of virtualParameters) + if (e.target.checked) selected.add(virtualParameter["_id"]); + else selected.delete(virtualParameter["_id"]); + }, + disabled: !total + }); + + const labels = [m("th", selectAll)]; + for (let attr of attributes) { + let label = attr.label; + + let direction = 1; + + let symbol = "\u21f3"; + if (sort[attr.id] > 0) symbol = "\u2b07"; + else if (sort[attr.id] < 0) symbol = "\u2b06"; + + let sortable = m( + "button", + { + onclick: () => { + if (sort[attr.id] > 0) direction *= -1; + return onSortChange(JSON.stringify({ [attr.id]: direction })); + } + }, + symbol + ); + + labels.push(m("th", [label, sortable])); + } + + let rows = []; + for (let virtualParameter of virtualParameters) { + let checkbox = m("input", { + type: "checkbox", + checked: selected.has(virtualParameter["_id"]), + onchange: e => { + if (e.target.checked) selected.add(virtualParameter["_id"]); + else selected.delete(virtualParameter["_id"]); + }, + onclick: e => { + e.stopPropagation(); + e.redraw = false; + } + }); + + let tds = [m("td", checkbox)]; + for (let attr of attributes) + if (attr.id == "script") + tds.push( + m( + "td", + { title: virtualParameter[attr.id] }, + virtualParameter[attr.id] + ) + ); + else tds.push(m("td", virtualParameter[attr.id])); + + tds.push( + m( + "td.table-row-links", + m( + "a", + { + onclick: () => { + let cb = () => { + return m( + putForm, + Object.assign( + { + base: virtualParameter, + actionHandler: (action, object) => { + overlay.close(cb); + putActionHandler(action, object, false); + } + }, + formData + ) + ); + }; + overlay.open(cb); + } + }, + "Show" + ) + ) + ); + + rows.push( + m( + "tr", + { + onclick: e => { + if (["INPUT", "BUTTON", "A"].includes(e.target.nodeName)) { + e.redraw = false; + return; + } + + if (!selected.delete(virtualParameter["_id"])) + selected.add(virtualParameter["_id"]); + } + }, + tds + ) + ); + } + + if (!rows.length) + rows.push( + m( + "tr.empty", + m("td", { colspan: labels.length }, "No virtual parameters") + ) + ); + + let footerElements = []; + if (total != null) + footerElements.push(`${virtualParameters.length}/${total}`); + else footerElements.push(`${virtualParameters.length}`); + + footerElements.push( + m( + "button", + { + title: "Show more virtual parameters", + onclick: showMoreCallback, + disabled: + virtualParameters.length >= total || + !virtualParametersResponse.fulfilled + }, + "More" + ) + ); + + if (downloadUrl) + footerElements.push( + m("a.download-csv", { href: downloadUrl, download: "" }, "Download") + ); + + let tfoot = m( + "tfoot", + m("tr", m("td", { colspan: labels.length }, footerElements)) + ); + + const buttons = [ + m( + "button.primary", + { + title: "Delete selected virtual parameters", + disabled: !selected.size, + onclick: e => { + e.redraw = false; + e.target.disabled = true; + Promise.all( + Array.from(selected).map(id => + store.deleteResource("virtualParameters", id) + ) + ) + .then(res => { + notifications.push( + "success", + `${res.length} virtual parameters deleted` + ); + store.fulfill(0, Date.now()); + }) + .catch(err => { + notifications.push("error", err.message); + }); + } + }, + "Delete" + ) + ]; + + if (window.authorizer.hasAccess("virtualParameters", 3)) + buttons.push( + m( + "button.primary", + { + title: "Create new virtual parameter", + onclick: () => { + let cb = () => { + return m( + putForm, + Object.assign( + { + actionHandler: (action, object) => { + putActionHandler(action, object, true); + overlay.close(cb); + } + }, + formData + ) + ); + }; + overlay.open(cb); + } + }, + "New" + ) + ); + + return [ + m( + "table.table.highlight", + m("thead", m("tr", labels)), + m("tbody", rows), + tfoot + ), + m("div.actions-bar", buttons) + ]; +} + +const component = { + view: vnode => { + document.title = "Virtual Parameters - GenieACS"; + + function showMore() { + vnode.state.showCount = (vnode.state.showCount || PAGE_SIZE) + PAGE_SIZE; + m.redraw(); + } + + function onFilterChanged(filter) { + let ops = { filter }; + if (vnode.attrs.sort) ops.sort = vnode.attrs.sort; + m.route.set("/virtualParameters", ops); + } + + function onSortChange(sort) { + let ops = { sort }; + if (vnode.attrs.filter) ops.filter = vnode.attrs.filter; + m.route.set("/virtualParameters", ops); + } + + const sort = vnode.attrs.sort ? memoizedJsonParse(vnode.attrs.sort) : {}; + let filter = vnode.attrs.filter ? memoizedParse(vnode.attrs.filter) : true; + filter = unpackSmartQuery(filter); + + let virtualParameters = store.fetch("virtualParameters", filter, { + limit: vnode.state.showCount || PAGE_SIZE, + sort: sort + }); + + let count = store.count("virtualParameters", filter); + + let selected = new Set(); + if (vnode.state.selected) + for (let 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 + ) + ]; + } +}; + +export { init, component }; diff --git a/config.json b/config.json index 1130e67..079c7be 100644 --- a/config.json +++ b/config.json @@ -48,6 +48,12 @@ "pass": true } }, + "virtualParameters" : { + "access": 3, + "validate": { + "pass": true + } + }, "files": { "access": 3 } @@ -67,6 +73,9 @@ "provisions": { "access": 2 }, + "virtualParameters": { + "access": 2 + }, "files": { "access": 2 } diff --git a/server/api-functions.js b/server/api-functions.js index 737fc2b..b8c859d 100644 --- a/server/api-functions.js +++ b/server/api-functions.js @@ -69,8 +69,10 @@ function filterToMongoQuery(filter, negate = false, res = {}) { function deleteResource(resource, id) { return new Promise((resolve, reject) => { + let resourceUrl = + resource === "virtualParameters" ? "virtual_parameters" : resource; let options = url.parse( - `${config.server.nbi}${resource}/${encodeURIComponent(id)}` + `${config.server.nbi}${resourceUrl}/${encodeURIComponent(id)}` ); options.method = "DELETE"; @@ -286,8 +288,10 @@ function ping(host) { function putResource(resource, id, data) { return new Promise((resolve, reject) => { + let resourceUrl = + resource === "virtualParameters" ? "virtual_parameters" : resource; let options = url.parse( - `${config.server.nbi}${resource}/${encodeURIComponent(id)}` + `${config.server.nbi}${resourceUrl}/${encodeURIComponent(id)}` ); options.method = "PUT"; @@ -299,6 +303,9 @@ function putResource(resource, id, data) { if (resource === "provisions") data = mongodbFunctions.preProcessProvision(data); + if (resource === "virtualParameters") + data = mongodbFunctions.preProcessVirtualParameters(data); + let body = typeof data !== "string" ? JSON.stringify(data) : data; _http .request(options, res => { diff --git a/server/api.js b/server/api.js index 970d608..0d8c205 100644 --- a/server/api.js +++ b/server/api.js @@ -23,7 +23,7 @@ const RESOURCE_IDS = { presets: "_id", provisions: "_id", files: "_id", - virtual_parameters: "_id", + virtualParameters: "_id", faults: "_id", tasks: "_id" }; @@ -33,7 +33,7 @@ const resources = { presets: 0 | RESOURCE_DELETE | RESOURCE_PUT, provisions: 0 | RESOURCE_DELETE | RESOURCE_PUT, files: 0 | RESOURCE_DELETE, - virtual_parameters: 0 | RESOURCE_DELETE, + virtualParameters: 0 | RESOURCE_DELETE | RESOURCE_PUT, faults: 0 | RESOURCE_DELETE, tasks: 0 }; @@ -347,17 +347,6 @@ for (let [resource, flags] of Object.entries(resources)) { logger.accessInfo(log); - db.putAudit({ - username: ctx.state.user.username, - action: "put", - objectType: resource, - objectId: id - }).catch(err => { - setTimeout(() => { - throw err; - }, 0); - }); - ctx.body = ""; }); } diff --git a/server/db.js b/server/db.js index e37bf1c..d3b5b90 100644 --- a/server/db.js +++ b/server/db.js @@ -13,9 +13,12 @@ const RESOURCE_DB = { tasks: "genieacs", presets: "genieacs", provisions: "genieacs", + virtualParameters: "genieacs", files: "genieacs" }; +const RESOURCE_COLLECTION = {}; + function getClient() { if (!_clientPromise) _clientPromise = new Promise((resolve, reject) => { @@ -54,7 +57,7 @@ function query(resource, filter, options, callback) { getClient().then(client => { const collection = client .db(RESOURCE_DB[resource]) - .collection(resource); + .collection(RESOURCE_COLLECTION[resource] || resource); let cursor = collection.find( q, resource === "Devices" @@ -130,7 +133,9 @@ function count(resource, filter) { return new Promise((resolve, reject) => { getClient().then(client => { - const collection = client.db(RESOURCE_DB[resource]).collection(resource); + const collection = client + .db(RESOURCE_DB[resource]) + .collection(RESOURCE_COLLECTION[resource] || resource); collection.find(q).count((err, c) => { if (err) reject(err); else resolve(c); diff --git a/server/mongodb-functions.js b/server/mongodb-functions.js index 630f0d7..c6643a1 100644 --- a/server/mongodb-functions.js +++ b/server/mongodb-functions.js @@ -498,6 +498,10 @@ function preProcessProvision(data) { return data.script; } +function preProcessVirtualParameters(data) { + return data.script; +} + exports.processDeviceFilter = processDeviceFilter; exports.processTasksFilter = processTasksFilter; exports.processFaultsFilter = processFaultsFilter; @@ -510,3 +514,4 @@ exports.flattenTask = flattenTask; exports.flattenPreset = flattenPreset; exports.preProcessPreset = preProcessPreset; exports.preProcessProvision = preProcessProvision; +exports.preProcessVirtualParameters = preProcessVirtualParameters; diff --git a/server/server.js b/server/server.js index adc5239..2e71034 100644 --- a/server/server.js +++ b/server/server.js @@ -39,6 +39,7 @@ function getPermissionSets(roles) { "faults", "presets", "provisions", + "virtualParameters", "files" ].includes(k) )