/**
* Copyright 2013-2019 GenieACS Inc.
*
* This file is part of GenieACS.
*
* GenieACS is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* GenieACS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with GenieACS. If not, see .
*/
import { m } from "./components";
import * as store from "./store";
import * as notifications from "./notifications";
import putFormComponent from "./put-form-component";
import uiConfigComponent from "./ui-config-component";
import * as overlay from "./overlay";
import { parse, stringify } from "../lib/common/expression/parser";
import { loadCodeMirror, loadYaml } from "./dynamic-loader";
import { ClosureComponent, Component, Children } from "mithril";
import { getIcon } from "./icons";
const attributes = [
{ id: "_id", label: "Key" },
{ id: "value", label: "Value", type: "textarea" },
];
interface ValidationErrors {
[prop: string]: string;
}
function putActionHandler(action, _object, isNew?): Promise {
return new Promise((resolve, reject) => {
const object = Object.assign({}, _object);
if (action === "save") {
let id = object["_id"] || "";
delete object["_id"];
const regex = /^[0-9a-zA-Z_.-]+$/;
id = id.trim();
if (!id.match(regex)) return void resolve({ _id: "Invalid ID" });
try {
object.value = stringify(parse(object.value || ""));
} catch (err) {
return void resolve({
value: "Config value must be valid expression",
});
}
store
.resourceExists("config", id)
.then((exists) => {
if (exists && isNew) {
store.setTimestamp(Date.now());
return void resolve({ _id: "Config already exists" });
}
if (!exists && !isNew) {
store.setTimestamp(Date.now());
return void resolve({ _id: "Config does not exist" });
}
store
.putResource("config", id, object)
.then(() => {
notifications.push(
"success",
`Config ${exists ? "updated" : "created"}`
);
store.setTimestamp(Date.now());
resolve(null);
})
.catch(reject);
})
.catch(reject);
} else if (action === "delete") {
store
.deleteResource("config", object["_id"])
.then(() => {
notifications.push("success", "Config deleted");
store.setTimestamp(Date.now());
resolve(null);
})
.catch((err) => {
store.setTimestamp(Date.now());
reject(err);
});
} else {
reject(new Error("Undefined action"));
}
});
}
const formData = {
resource: "config",
attributes: attributes,
};
function escapeRegExp(str): string {
return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&");
}
export function init(): Promise> {
if (!window.authorizer.hasAccess("config", 2)) {
return Promise.reject(
new Error("You are not authorized to view this page")
);
}
return new Promise((resolve, reject) => {
Promise.all([loadCodeMirror(), loadYaml()])
.then(() => {
resolve({});
})
.catch(reject);
});
}
function renderTable(confsResponse, searchString): Children {
const confs = confsResponse.value.sort((a, b) => {
return a._id < b._id ? -1 : 1;
});
let regex;
if (searchString) {
const keywords = searchString.split(" ").filter((s) => s);
if (keywords.length)
regex = new RegExp(keywords.map((s) => escapeRegExp(s)).join(".*"), "i");
}
const rows = [];
for (const conf of confs) {
const attrs = {};
if (regex && !regex.test(conf._id) && !regex.test(conf.value))
attrs["style"] = "display: none;";
const edit = m(
"button",
{
title: "Edit config value",
onclick: () => {
let cb: () => Children = null;
const comp = m(
putFormComponent,
Object.assign(
{
base: conf,
actionHandler: (action, object) => {
return new Promise((resolve) => {
putActionHandler(action, object, false)
.then((errors) => {
const ErrorList = errors ? Object.values(errors) : [];
if (ErrorList.length) {
for (const err of ErrorList)
notifications.push("error", err);
} else {
overlay.close(cb);
}
resolve();
})
.catch((err) => {
notifications.push("error", err.message);
resolve();
});
});
},
},
formData
)
);
cb = () => comp;
overlay.open(
cb,
() =>
!comp.state["current"]["modified"] ||
confirm("You have unsaved changes. Close anyway?")
);
},
},
getIcon("edit")
);
const del = m(
"button",
{
title: "Delete config",
onclick: () => {
if (!confirm(`Deleting ${conf._id} config. Are you sure?`)) return;
putActionHandler("delete", conf).catch((err) => {
throw err;
});
},
},
getIcon("remove")
);
rows.push(
m(
"tr",
attrs,
m("td.left", m("long-text", { text: conf._id })),
m(
"td.right",
m("span", [m("long-text", { text: `${conf.value}` }), edit, del])
)
)
);
}
if (!rows.length)
rows.push(m("tr.empty", m("td", { colspan: 2 }, "No config")));
return m("table", m("tbody", rows));
}
export const component: ClosureComponent = (): 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 confs = store.fetch("config", true);
let newConfig;
const subs = [];
if (window.authorizer.hasAccess("config", 3)) {
newConfig = m(
"button.primary",
{
title: "Create new config",
onclick: () => {
let cb: () => Children = null;
const comp = m(
putFormComponent,
Object.assign(
{
actionHandler: (action, object) => {
return new Promise((resolve) => {
putActionHandler(action, object, true)
.then((errors) => {
const errorList = errors
? Object.values(errors)
: [];
if (errorList.length) {
for (const err of errorList)
notifications.push("error", err);
} else {
overlay.close(cb);
}
resolve(null);
})
.catch((err) => {
notifications.push("error", err.message);
resolve();
});
});
},
},
formData
)
);
cb = () => comp;
overlay.open(
cb,
() =>
!comp.state["current"]["modified"] ||
confirm("You have unsaved changes. Close anyway?")
);
},
},
"New config"
);
const subsData = [
{ name: "overview", prefix: "ui.overview.groups.", data: [] },
{ name: "charts", prefix: "ui.overview.charts.", data: [] },
{ name: "filters", prefix: "ui.filters.", data: [] },
{ name: "index page", prefix: "ui.index.", data: [] },
{ name: "device page", prefix: "ui.device.", data: [] },
];
if (confs.fulfilled) {
for (const conf of confs.value) {
for (const sub of subsData) {
if (conf["_id"].startsWith(sub["prefix"])) {
sub["data"].push(conf);
break;
}
}
}
}
for (const sub of subsData) {
const attrs = { prefix: sub.prefix, name: sub.name, data: sub.data };
subs.push(
m(
"button",
{
onclick: () => {
let cb: () => Children = null;
const comp = m(
uiConfigComponent,
Object.assign(
{
onUpdate: (errs: Record) => {
const errors = errs ? Object.values(errs) : [];
if (errors.length) {
for (const err of errors)
notifications.push("error", err);
} else {
notifications.push(
"success",
`${sub.name.replace(
/^[a-z]/,
sub.name[0].toUpperCase()
)} config updated`
);
overlay.close(cb);
}
store.setTimestamp(Date.now());
},
onError: (err) => {
notifications.push("error", err.message);
store.setTimestamp(Date.now());
overlay.close(cb);
},
},
attrs
)
);
cb = () => comp;
overlay.open(
cb,
() =>
!comp.state["modified"] ||
confirm("You have unsaved changes. Close anyway?")
);
},
},
`Edit ${sub.name}`
)
);
}
}
return [
m("h1", "Listing config"),
m(
"loading",
{ queries: [confs] },
m(
".all-parameters",
search,
m(
".parameter-list",
{ style: "height: 400px" },
renderTable(confs, vnode.state["searchString"])
),
m(".actions-bar", [newConfig].concat(subs))
)
),
];
},
};
};