Add sub configs edit component
This commit is contained in:
1 parent
d70263e798
commit
152d8f5283
10 files changed
+383
-41
No files matched your search
+2
-1
@@ -52,7 +52,8 @@ const externals = [
|
||||
"mithril",
|
||||
"parsimmon",
|
||||
"codemirror",
|
||||
"codemirror/mode/javascript/javascript"
|
||||
"codemirror/mode/javascript/javascript",
|
||||
"codemirror/mode/yaml/yaml"
|
||||
];
|
||||
|
||||
function rmDirSync(dirPath): void {
|
||||
|
||||
+26
-9
@@ -363,26 +363,42 @@ function refresh(callback): void {
|
||||
|
||||
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 = {};
|
||||
|
||||
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.")) {
|
||||
const keys = c.id.split(".");
|
||||
let ref = _config;
|
||||
// remove the first key(ui)
|
||||
keys.shift();
|
||||
let ref = ui;
|
||||
while (keys.length > 1) {
|
||||
const k = keys.shift();
|
||||
if (typeof ref[k] !== "object") ref[k] = {};
|
||||
ref = ref[k];
|
||||
}
|
||||
// Evaluate expressions to simplify them
|
||||
ref[keys[0]] = expression.evaluate(c.value);
|
||||
} else {
|
||||
// Evaluate expressions to simplify them
|
||||
_config[c.id] = expression.evaluate(c.value);
|
||||
ref[keys[0]] = val;
|
||||
}
|
||||
}
|
||||
|
||||
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],
|
||||
permissions: res[4],
|
||||
users: res[5],
|
||||
config: res[6]
|
||||
config: res[6][0],
|
||||
ui: res[6][1]
|
||||
};
|
||||
|
||||
if (currentSnapshot) {
|
||||
@@ -536,5 +553,5 @@ export function getUiConfig(snapshotKey): {} {
|
||||
const snapshot = snapshots.get(snapshotKey);
|
||||
if (!snapshot) throw new Error("Cache snapshot does not exist");
|
||||
|
||||
return snapshot.config.ui;
|
||||
return snapshot.ui;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -10,6 +10,7 @@ const component: ClosureComponent = (): Component => {
|
||||
view: vnode => {
|
||||
const device = vnode.attrs["device"];
|
||||
const chart = CHARTS[vnode.attrs["chart"]];
|
||||
if (!chart) return null;
|
||||
for (const slice of Object.values(chart.slices)) {
|
||||
const filter = slice["filter"];
|
||||
if (store.evaluateExpression(filter, device)) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
+74
-5
@@ -2,9 +2,10 @@ 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 * as expressionParser from "../lib/common/expression-parser";
|
||||
import { loadCodeMirror } from "./dynamic-loader";
|
||||
import { loadCodeMirror, loadYaml } from "./dynamic-loader";
|
||||
import { ClosureComponent, Component, Children } from "mithril";
|
||||
|
||||
const attributes = [
|
||||
@@ -92,7 +93,7 @@ export function init(): Promise<{}> {
|
||||
);
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
loadCodeMirror()
|
||||
Promise.all([loadCodeMirror(), loadYaml()])
|
||||
.then(() => {
|
||||
resolve({});
|
||||
})
|
||||
@@ -209,7 +210,11 @@ export const component: ClosureComponent = (): Component => {
|
||||
}
|
||||
});
|
||||
|
||||
const confs = store.fetch("config", true);
|
||||
|
||||
let newConfig;
|
||||
const subs = [];
|
||||
|
||||
if (window.authorizer.hasAccess("config", 3)) {
|
||||
newConfig = m(
|
||||
"button.primary",
|
||||
@@ -250,11 +255,75 @@ export const component: ClosureComponent = (): Component => {
|
||||
overlay.open(cb);
|
||||
}
|
||||
},
|
||||
"New"
|
||||
"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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const confs = store.fetch("config", true);
|
||||
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 [
|
||||
m("h1", "Listing config"),
|
||||
@@ -266,7 +335,7 @@ export const component: ClosureComponent = (): Component => {
|
||||
{ style: "height: 400px" },
|
||||
renderTable(confs, vnode.state["searchString"])
|
||||
),
|
||||
m(".actions-bar", newConfig)
|
||||
m(".actions-bar", [newConfig].concat(subs))
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ export function loadCodeMirror(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const promises = [
|
||||
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)
|
||||
.then(modules => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { VnodeDOM, ClosureComponent, Component, Children } from "mithril";
|
||||
import { m } from "./components";
|
||||
import { codeMirror } from "./dynamic-loader";
|
||||
import codeEditorComponent from "./code-editor-component";
|
||||
|
||||
const singular = {
|
||||
presets: "preset",
|
||||
@@ -59,34 +59,18 @@ function createField(current, attr, focus): Children {
|
||||
|
||||
return m("table", options);
|
||||
} else if (attr.type === "code") {
|
||||
return m("textarea", {
|
||||
name: attr.id,
|
||||
const attrs = {
|
||||
id: attr.id,
|
||||
value: current.object[attr.id],
|
||||
oncreate: _vnode => {
|
||||
const editor = codeMirror.fromTextArea(_vnode.dom, {
|
||||
mode: "javascript",
|
||||
lineNumbers: true,
|
||||
extraKeys: {
|
||||
"Ctrl-Enter": () => {
|
||||
((_vnode.dom as HTMLTextAreaElement).form.querySelector(
|
||||
"button[type=submit]"
|
||||
) as HTMLButtonElement).click();
|
||||
onSubmit: dom => {
|
||||
dom.form.querySelector("button[type=submit]").click();
|
||||
},
|
||||
"Cmd-Enter": () => {
|
||||
((_vnode.dom as HTMLTextAreaElement).form.querySelector(
|
||||
"button[type=submit]"
|
||||
) as HTMLButtonElement).click();
|
||||
onChange: value => {
|
||||
current.object[attr.id] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
editor.on("change", e => {
|
||||
current.object[attr.id] = e.getValue();
|
||||
});
|
||||
|
||||
if (focus) editor.focus();
|
||||
}
|
||||
});
|
||||
};
|
||||
return m(codeEditorComponent, attrs);
|
||||
} else if (attr.type === "file") {
|
||||
return m("input", {
|
||||
type: "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> {
|
||||
const param = resource === "devices" ? "DeviceID.ID" : "_id";
|
||||
const filter = ["=", ["PARAM", param], id];
|
||||
|
||||
@@ -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;
|
||||
Reference in new issue
Block a user