Add sub configs edit component

This commit is contained in:
Shweelan Samson authored and Zaid Abdulla committed 2019-03-05 18:48:53 -08:00
1 parent d70263e798
commit 152d8f5283
10 files changed
+386 -44

No files matched your search

+2 -1
View File
@@ -52,7 +52,8 @@ const externals = [
"mithril", "mithril",
"parsimmon", "parsimmon",
"codemirror", "codemirror",
"codemirror/mode/javascript/javascript" "codemirror/mode/javascript/javascript",
"codemirror/mode/yaml/yaml"
]; ];
function rmDirSync(dirPath): void { function rmDirSync(dirPath): void {
+26 -9
View File
@@ -363,26 +363,42 @@ function refresh(callback): void {
conf.sort((a, b) => (a.id > b.id ? 1 : a.id < b.id ? -1 : 0)); conf.sort((a, b) => (a.id > b.id ? 1 : a.id < b.id ? -1 : 0));
const ui = {
filters: {},
device: {},
index: {},
overview: { charts: {}, groups: {} }
};
const _config = {}; const _config = {};
for (const c of conf) { for (const c of conf) {
// Evaluate expressions to simplify them
const val = expression.evaluate(c.value);
_config[c.id] = val;
if (c.id.startsWith("ui.")) { if (c.id.startsWith("ui.")) {
const keys = c.id.split("."); const keys = c.id.split(".");
let ref = _config; // remove the first key(ui)
keys.shift();
let ref = ui;
while (keys.length > 1) { while (keys.length > 1) {
const k = keys.shift(); const k = keys.shift();
if (typeof ref[k] !== "object") ref[k] = {}; if (typeof ref[k] !== "object") ref[k] = {};
ref = ref[k]; ref = ref[k];
} }
// Evaluate expressions to simplify them ref[keys[0]] = val;
ref[keys[0]] = expression.evaluate(c.value);
} else {
// Evaluate expressions to simplify them
_config[c.id] = expression.evaluate(c.value);
} }
} }
resolve(_config); if (!Object.keys(ui["index"]).length) {
ui["index"] = {
"0": {
label: "ID",
parameter: ["PARAM", "DeviceID.ID"]
}
};
}
resolve([_config, ui]);
}); });
}) })
); );
@@ -396,7 +412,8 @@ function refresh(callback): void {
files: res[3], files: res[3],
permissions: res[4], permissions: res[4],
users: res[5], users: res[5],
config: res[6] config: res[6][0],
ui: res[6][1]
}; };
if (currentSnapshot) { if (currentSnapshot) {
@@ -536,5 +553,5 @@ export function getUiConfig(snapshotKey): {} {
const snapshot = snapshots.get(snapshotKey); const snapshot = snapshots.get(snapshotKey);
if (!snapshot) throw new Error("Cache snapshot does not exist"); if (!snapshot) throw new Error("Cache snapshot does not exist");
return snapshot.config.ui; return snapshot.ui;
} }
+42
View File
@@ -0,0 +1,42 @@
import { ClosureComponent, Component } from "mithril";
import { m } from "./components";
import { codeMirror } from "./dynamic-loader";
const component: ClosureComponent = (): Component => {
return {
view: vnode => {
return m("textarea", {
name: vnode.attrs["id"],
value: vnode.attrs["value"],
oncreate: _vnode => {
const editor = codeMirror.fromTextArea(_vnode.dom, {
mode: vnode.attrs["mode"],
lineNumbers: true,
readOnly: vnode.attrs["readOnly"],
extraKeys: {
"Ctrl-Enter": () => {
if (vnode.attrs["onSubmit"])
vnode.attrs["onSubmit"](_vnode.dom);
},
"Cmd-Enter": () => {
if (vnode.attrs["onSubmit"])
vnode.attrs["onSubmit"](_vnode.dom);
}
}
});
if (vnode.attrs["onChange"]) {
editor.on("change", e => {
vnode.attrs["onChange"](e.getValue());
});
}
if (vnode.attrs["focus"]) editor.focus();
if (vnode.attrs["onReady"]) vnode.attrs["onReady"](editor);
}
});
}
};
};
export default component;
+1
View File
@@ -10,6 +10,7 @@ const component: ClosureComponent = (): Component => {
view: vnode => { view: vnode => {
const device = vnode.attrs["device"]; const device = vnode.attrs["device"];
const chart = CHARTS[vnode.attrs["chart"]]; const chart = CHARTS[vnode.attrs["chart"]];
if (!chart) return null;
for (const slice of Object.values(chart.slices)) { for (const slice of Object.values(chart.slices)) {
const filter = slice["filter"]; const filter = slice["filter"];
if (store.evaluateExpression(filter, device)) { if (store.evaluateExpression(filter, device)) {
+92
View File
@@ -0,0 +1,92 @@
interface Config {
_id: string;
value: string;
}
interface Diff {
add: Config[];
remove: string[];
}
export function flattenConfig(config): any {
const flatten = {};
const recuresive = (obj: any, root: string): void => {
for (const [k, v] of Object.entries(obj)) {
const key = root ? `${root}.${k}` : k;
if (v === undefined) continue;
if (v === null || typeof v !== "object") flatten[key] = v;
else recuresive(v, key);
}
};
if (config !== null && typeof config === "object") recuresive(config, "");
return flatten;
}
export function structureConfig(config: Config[]): any {
config.sort((a, b) => (a._id > b._id ? 1 : a._id < b._id ? -1 : 0));
const _config = {};
for (const c of config) {
const keys = c._id.split(".");
let ref = _config;
while (keys.length > 1) {
const k = keys.shift();
if (typeof ref[k] !== "object") ref[k] = {};
ref = ref[k];
}
ref[keys[0]] = c.value;
}
const toArray = function(object): any {
const MAX_BITS = 30;
const MAX_ARRAY_SIZE = MAX_BITS * 10;
if (object == null || typeof object !== "object") return object;
if (Object.keys(object).length <= MAX_ARRAY_SIZE) {
let indexes = [];
for (const key of Object.keys(object)) {
const idx = Math.floor(+key);
if (idx >= 0 && idx < MAX_ARRAY_SIZE && String(idx) === key) {
const pos = Math.floor(idx / MAX_BITS);
if (!indexes[pos]) indexes[pos] = 0;
indexes[pos] |= 1 << idx % MAX_BITS;
} else {
indexes = [];
break;
}
}
let index = 0;
while (indexes.length && (index = indexes.shift()) === 1073741823);
if (index && (~index & (index + 1)) === index + 1) {
// its an array
const array = [];
for (let i = 0; i < Object.keys(object).length; i++)
array[i] = object[i];
object = array;
}
}
for (const [k, v] of Object.entries(object)) object[k] = toArray(v);
return object;
};
return toArray(_config);
}
export function diffConfig(current, target): Diff {
const diff = {
add: [],
remove: []
};
for (const [k, v] of Object.entries(target))
if (!current[k] || current[k] !== v) diff.add.push({ _id: k, value: v });
for (const k of Object.keys(current)) if (!target[k]) diff.remove.push(k);
return diff;
}
+75 -6
View File
@@ -2,9 +2,10 @@ import { m } from "./components";
import * as store from "./store"; import * as store from "./store";
import * as notifications from "./notifications"; import * as notifications from "./notifications";
import putFormComponent from "./put-form-component"; import putFormComponent from "./put-form-component";
import uiConfigComponent from "./ui-config-component";
import * as overlay from "./overlay"; import * as overlay from "./overlay";
import * as expressionParser from "../lib/common/expression-parser"; import * as expressionParser from "../lib/common/expression-parser";
import { loadCodeMirror } from "./dynamic-loader"; import { loadCodeMirror, loadYaml } from "./dynamic-loader";
import { ClosureComponent, Component, Children } from "mithril"; import { ClosureComponent, Component, Children } from "mithril";
const attributes = [ const attributes = [
@@ -92,7 +93,7 @@ export function init(): Promise<{}> {
); );
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
loadCodeMirror() Promise.all([loadCodeMirror(), loadYaml()])
.then(() => { .then(() => {
resolve({}); resolve({});
}) })
@@ -209,7 +210,11 @@ export const component: ClosureComponent = (): Component => {
} }
}); });
const confs = store.fetch("config", true);
let newConfig; let newConfig;
const subs = [];
if (window.authorizer.hasAccess("config", 3)) { if (window.authorizer.hasAccess("config", 3)) {
newConfig = m( newConfig = m(
"button.primary", "button.primary",
@@ -250,11 +255,75 @@ export const component: ClosureComponent = (): Component => {
overlay.open(cb); overlay.open(cb);
} }
}, },
"New" "New config"
); );
}
const confs = store.fetch("config", true); 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: () => {
const cb = (): Children => {
return m(
uiConfigComponent,
Object.assign(
{
onUpdate: errs => {
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.fulfill(0, Date.now());
},
onError: err => {
notifications.push("error", err.message);
store.fulfill(0, Date.now());
overlay.close(cb);
}
},
attrs
)
);
};
overlay.open(cb);
}
},
`Edit ${sub.name}`
)
);
}
}
return [ return [
m("h1", "Listing config"), m("h1", "Listing config"),
@@ -266,7 +335,7 @@ export const component: ClosureComponent = (): Component => {
{ style: "height: 400px" }, { style: "height: 400px" },
renderTable(confs, vnode.state["searchString"]) renderTable(confs, vnode.state["searchString"])
), ),
m(".actions-bar", newConfig) m(".actions-bar", [newConfig].concat(subs))
) )
]; ];
} }
+2 -1
View File
@@ -7,7 +7,8 @@ export function loadCodeMirror(): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const promises = [ const promises = [
import(/* webpackChunkName: "codemirror" */ "codemirror"), import(/* webpackChunkName: "codemirror" */ "codemirror"),
import(/* webpackChunkName: "codemirror" */ "codemirror/mode/javascript/javascript") import(/* webpackChunkName: "codemirror" */ "codemirror/mode/javascript/javascript"),
import(/* webpackChunkName: "codemirror" */ "codemirror/mode/yaml/yaml")
]; ];
Promise.all(promises) Promise.all(promises)
.then(modules => { .then(modules => {
+11 -27
View File
@@ -1,6 +1,6 @@
import { VnodeDOM, ClosureComponent, Component, Children } from "mithril"; import { VnodeDOM, ClosureComponent, Component, Children } from "mithril";
import { m } from "./components"; import { m } from "./components";
import { codeMirror } from "./dynamic-loader"; import codeEditorComponent from "./code-editor-component";
const singular = { const singular = {
presets: "preset", presets: "preset",
@@ -59,34 +59,18 @@ function createField(current, attr, focus): Children {
return m("table", options); return m("table", options);
} else if (attr.type === "code") { } else if (attr.type === "code") {
return m("textarea", { const attrs = {
name: attr.id, id: attr.id,
value: current.object[attr.id], value: current.object[attr.id],
oncreate: _vnode => { mode: "javascript",
const editor = codeMirror.fromTextArea(_vnode.dom, { onSubmit: dom => {
mode: "javascript", dom.form.querySelector("button[type=submit]").click();
lineNumbers: true, },
extraKeys: { onChange: value => {
"Ctrl-Enter": () => { current.object[attr.id] = value;
((_vnode.dom as HTMLTextAreaElement).form.querySelector(
"button[type=submit]"
) as HTMLButtonElement).click();
},
"Cmd-Enter": () => {
((_vnode.dom as HTMLTextAreaElement).form.querySelector(
"button[type=submit]"
) as HTMLButtonElement).click();
}
}
});
editor.on("change", e => {
current.object[attr.id] = e.getValue();
});
if (focus) editor.focus();
} }
}); };
return m(codeEditorComponent, attrs);
} else if (attr.type === "file") { } else if (attr.type === "file") {
return m("input", { return m("input", {
type: "file", type: "file",
+9
View File
@@ -520,6 +520,15 @@ export function putResource(resourceType, id, object): Promise<void> {
}); });
} }
export function queryConfig(pattern = "%"): Promise<any[]> {
const filter = stringify(["LIKE", ["PARAM", "_id"], pattern]);
return m.request({
method: "GET",
url: `api/config/?${m.buildQueryString({ filter: filter })}`,
background: true
});
}
export function resourceExists(resource, id): Promise<number> { export function resourceExists(resource, id): Promise<number> {
const param = resource === "devices" ? "DeviceID.ID" : "_id"; const param = resource === "devices" ? "DeviceID.ID" : "_id";
const filter = ["=", ["PARAM", param], id]; const filter = ["=", ["PARAM", param], id];
+126
View File
@@ -0,0 +1,126 @@
import { ClosureComponent, Component } from "mithril";
import { m } from "./components";
import * as store from "./store";
import { yaml } from "./dynamic-loader";
import * as configFunctions from "./config-functions";
import codeEditorComponent from "./code-editor-component";
import { parse } from "../lib/common/expression-parser";
function putActionHandler(prefix: string[], dataYaml: string): Promise<any> {
return new Promise((resolve, reject) => {
try {
let updated = yaml.parse(dataYaml, { schema: "failsafe" });
if (updated) {
const config = {};
let ref = config;
prefix.forEach((seg, index) => {
if (index < prefix.length - 1) {
ref[seg] = {};
ref = ref[seg];
} else {
ref[seg] = updated;
}
});
updated = configFunctions.flattenConfig(config);
} else {
updated = {};
}
// Try parse to ensure valid expressions
for (const v of Object.values(updated)) parse(v);
store
.queryConfig(`${prefix.join(".")}.%`)
.then(res => {
const current = {};
for (const f of res) current[f._id] = f.value;
const diff = configFunctions.diffConfig(current, updated);
if (!diff.add.length && !diff.remove.length) return void resolve();
const promises = [];
for (const obj of diff.add)
promises.push(store.putResource("config", obj._id, obj));
for (const id of diff.remove)
promises.push(store.deleteResource("config", id));
Promise.all(promises)
.then(() => {
resolve();
})
.catch(reject);
})
.catch(reject);
} catch (error) {
resolve({ config: error.message });
}
});
}
const component: ClosureComponent = (): Component => {
return {
view: vnode => {
const prefix = vnode.attrs["prefix"].split(".");
const name = vnode.attrs["name"];
const data = vnode.attrs["data"];
if (prefix[prefix.length - 1] === "") prefix.pop();
let config;
if (data.length) {
config = configFunctions.structureConfig(data);
for (const seg of prefix) config = config[seg];
}
const yamlString =
config && Object.values(config).length
? yaml.stringify(config, {
schema: "failsafe",
tags: args => {
return args;
}
})
: "";
const attrs = {
id: `${name}-ui-config`,
value: yamlString,
mode: "yaml",
focus: true,
onSubmit: dom => {
dom.form.querySelector("button[type=submit]").click();
},
onChange: value => {
vnode.state["updatedYaml"] = value;
}
};
const code = m(codeEditorComponent, attrs);
const submit = m("button.primary", { type: "submit" }, "Save");
return m("div.put-form", [
m("h1", `Editing ${name}`),
m(
"form",
{
onsubmit: e => {
e.redraw = false;
e.preventDefault();
if (vnode.state["updatedYaml"] == null)
vnode.state["updatedYaml"] = yamlString;
putActionHandler(prefix, vnode.state["updatedYaml"])
.then(vnode.attrs["onUpdate"])
.catch(vnode.attrs["onError"]);
}
},
[code, m(".actions-bar", [submit])]
)
]);
}
};
};
export default component;