diff options
| author | Martin Ridgers <[email protected]> | 2024-09-18 15:07:12 +0200 |
|---|---|---|
| committer | Martin Ridgers <[email protected]> | 2024-09-24 10:57:34 +0200 |
| commit | a8d228b8af5135b01cc964e604d50111a8697471 (patch) | |
| tree | 151d5a4300b4309c1c4f99b7a0db462837bfa4d5 /src/zenserver/frontend/html/zen.js | |
| parent | Updated stale comment (diff) | |
| download | zen-a8d228b8af5135b01cc964e604d50111a8697471.tar.xz zen-a8d228b8af5135b01cc964e604d50111a8697471.zip | |
Initial version of in-proc HTML dashboard
Diffstat (limited to 'src/zenserver/frontend/html/zen.js')
| -rw-r--r-- | src/zenserver/frontend/html/zen.js | 595 |
1 files changed, 595 insertions, 0 deletions
diff --git a/src/zenserver/frontend/html/zen.js b/src/zenserver/frontend/html/zen.js new file mode 100644 index 000000000..a338739be --- /dev/null +++ b/src/zenserver/frontend/html/zen.js @@ -0,0 +1,595 @@ +//////////////////////////////////////////////////////////////////////////////// +class Component +{ + constructor(element) + { + if (element instanceof Component) + element = element._element; + this._element = element; + } + + destroy() + { + this._element.parentNode.removeChild(this._element); + } + + tag(tag="div") + { + var element = document.createElement(tag); + this._element.appendChild(element); + return new Component(element); + } + + text(value) + { + value = String(value); + this._element.innerHTML = (value != "") ? value : " "; + return this; + } + + id(value) + { + this._element.id = value; + return this; + } + + classify(value) + { + var cur = this._element.className; + cur += cur ? " " : ""; + cur += value; + this._element.className = cur; + return this; + } + + css_var(key, value) + { + this._element.style.setProperty("--" + key, value); + return this; + } + + attr(key, value) + { + this._element.setAttribute(key, value); + return this; + } + + link(resource=undefined, query_params={}) + { + if (resource != undefined) + { + var href = resource; + var sep = "?"; + for (const key in query_params) + { + href += sep + key + "=" + query_params[key]; + sep = "&"; + } + } + else + href = "javascript:void(0);"; + + var text = this._element.innerHTML; + this._element.innerHTML = ""; + this.tag("a").text(text).attr("href", href); + return this; + } + + on(what, func) + { + const thunk = (src) => { + if (src.target != this._element) + return; + + func(src.target); + src.stopPropagation(); + }; + this._element.addEventListener(what, thunk); + return this; + } +} + + + +//////////////////////////////////////////////////////////////////////////////// +class Cell extends Component +{ +} + +//////////////////////////////////////////////////////////////////////////////// +class Table extends Component +{ + constructor(parent, column_names, index_base=0) + { + var root = parent.tag().classify("zen_table"); + super(root); + + this._index = index_base; + if (index_base >= 0) + column_names = ["#", ...column_names]; + + this._num_columns = column_names.length; + root.css_var("zen_columns", this._num_columns); + + this._num_columns = column_names.length; + this.add_row(column_names, false); + } + + add_row(cells, indexed=true) + { + if (indexed && this._index >= 0) + cells = [this._index++, ...cells]; + + cells = cells.slice(0, this._num_columns); + while (cells.length < this._num_columns) + cells.push(""); + + var ret = []; + var row = this.tag().classify("zen_row"); + for (const cell of cells) + { + var leaf = row.tag().classify("zen_cell").text(cell); + ret.push(new Cell(leaf)); + } + + var bias = (this._index >= 0) ? 1 : 0; + return ret.slice(bias); + } + + clear(index=0) + { + const elem = this._element; + elem.replaceChildren(elem.firstElementChild); + this._index = index; + } +} + + + +//////////////////////////////////////////////////////////////////////////////// +class PropTable extends Table +{ + constructor(parent) + { + super(parent, ["prop", "value"], -1); + } + + add_property(key, value) + { + var ret = this.add_row([key, value]); + ret[0].classify("zen_prop_key"); + ret[1].classify("zen_prop_value"); + return ret; + } +} + + + +//////////////////////////////////////////////////////////////////////////////// +class Modal +{ + constructor() + { + const body = new Component(document.body); + this._root = body.tag().classify("zen_modal"); + this._root.on("click", () => this._root.destroy()); + + const rect = this._root.tag(); + this._title = rect.tag().classify("zen_modal_title") + this._content = rect.tag().classify("zen_modal_message") + this._buttons = rect.tag().classify("zen_modal_buttons"); + } + + title(value) + { + this._title.text(value); + return this; + } + + message(value) + { + this._content.text(value); + return this; + } + + option(name, func) + { + const thunk = (src) => { + this._root.destroy(); + func(src); + }; + this._buttons.tag().text(name).on("click", thunk); + return this; + } +} + + + +//////////////////////////////////////////////////////////////////////////////// +class Sectormatron extends Component +{ + constructor(parent, depth=1) + { + super(parent); + this._depth = depth; + } + + add_section(name) + { + var node = this.tag(); + if (this._depth == 1) + node.classify("zen_sector"); + + node.tag("h" + this._depth).text(name); + return new Sectormatron(node, this._depth + 1); + } +} + + + +//////////////////////////////////////////////////////////////////////////////// +async function zen_fetch(resource, method="GET") +{ + var response = await fetch(resource, { + "method" : method, + "headers" : { "Accept": "application/json" }, + }); + + return await response.json(); +} + +//////////////////////////////////////////////////////////////////////////////// +function zen_title(name) +{ + document.title = "zen - " + name; +} + +//////////////////////////////////////////////////////////////////////////////// +function zen_flatten(object, ret=null, prefix="") +{ + if (ret == null) + ret = new Object(); + + for (const key in object) + { + const value = object[key]; + if (value instanceof Object) + { + zen_flatten(value, ret, key + "."); + continue; + } + + ret[prefix + key] = value; + } + + return ret; +} + + + +//////////////////////////////////////////////////////////////////////////////// +class Page +{ + constructor(parent, params) + { + this._parent = parent; + this._params = params; + } + + get_param(name, fallback=undefined) + { + var ret = this._params.get(name); + return (ret != undefined) ? ret : fallback; + } +} + + + +//////////////////////////////////////////////////////////////////////////////// +class Entry extends Page +{ + async main() + { + zen_title("oplog entry"); + + const project = this.get_param("project"); + const oplog = this.get_param("oplog"); + const key = this.get_param("key"); + const uri = `/prj/${project}/oplog/${oplog}/entries?opkey=${key}`; + + var entry = await zen_fetch(uri); + var text = JSON.stringify(entry, null, 2); + this._parent.tag("pre").text(text); + } +} + + + +//////////////////////////////////////////////////////////////////////////////// +class Oplog extends Page +{ + constructor(...args) + { + super(...args); + + this._index_start = this.get_param("start", 0); + this._index_count = this.get_param("start", 50); + this._entry_table = undefined; + } + + async main() + { + zen_title("oplog"); + + const project = this.get_param("project"); + const oplog = this.get_param("oplog"); + + var builder = new Sectormatron(this._parent); + var section = builder.add_section(project + " - " + oplog); + + this._entry_table = new Table(section, ["key"]); + await this._build_table(); + } + + async _build_table() + { + const project = this.get_param("project"); + const oplog = this.get_param("oplog"); + + var uri = `/prj/${project}/oplog/${oplog}/entries`; + uri += "?start=" + this._index_start; + uri += "&count=" + this._index_count; + + var entries = zen_fetch(uri); + + this._entry_table.clear(this._index_start); + + var count = 12; + for (const entry of (await entries)["entries"]) + { + var cells = this._entry_table.add_row([ + entry["key"] + ]); + + cells[0].link("", { + "page" : "entry", + "project" : project, + "oplog" : oplog, + "key" : entry["key"], + }); + + --count; + if (count == 0) + break; + } + } + + _on_change_count(value) + { + this._index_count = parseInt(value); + this._build_table(); + } + + _on_next_prev(direction) + { + const index = this._index_start + (this._index_count * direction); + this._index_start = Math.max(0, index); + this._build_table(); + } +} + + + +//////////////////////////////////////////////////////////////////////////////// +class Project extends Page +{ + async main() + { + zen_title("project"); + + var builder = new Sectormatron(this._parent); + + // info + var section = builder.add_section("info"); + + const project = this.get_param("project"); + const prefix = "/prj/" + project; + + var info = await zen_fetch(prefix); + var prop_table = new PropTable(section); + for (const key in info) + { + if (key == "oplogs") + continue; + + prop_table.add_property(key, info[key]); + } + + // oplog + section = builder.add_section("oplogs"); + + var oplog_table = new Table(section, ["name", "actions"]) + + var count = 0; + for (const oplog of info["oplogs"]) + { + const name = oplog["id"]; + + var cells = oplog_table.add_row([ + name, + "drop", + ]); + + var cell = cells[0]; + cell.link("", { + "page" : "oplog", + "project" : project, + "oplog" : name, + }); + + cell = cells.at(-1); + cell.attr("zen_param", name).on("click", (e) => this._on_drop(e)); + } + + // files + /* + section = builder.add_section("files"); + for (const oplog of info["oplogs"]) + { + const name = oplog["id"]; + + var files = await zen_fetch(prefix + "/oplog/" + name + "/files"); + if (files["files"].length == 0) + continue; + + var sub_section = section.add_section(name); + var table = new Table(sub_section, [ + "id", + "clientpath", + "serverpath", + ]); + var count = 0; + for (const file of files["files"]) + { + table.add_row([ + file["id"], + file["clientpath"], + file["serverpath"], + ]); + + if (++count > 10) + break; + } + } + */ + } + + drop() + { + alert("\\o/"); + } + + _on_drop(e) + { + new Modal() + .title("Confirmation") + .message(`Drop oplog '${e.getAttribute("zen_param")}'?`) + .option("Yes", () => this.drop()) + .option("No", () => void(0)) + } +} + + + +//////////////////////////////////////////////////////////////////////////////// +class Start extends Page +{ + async main() + { + zen_title("main"); + + var builder = new Sectormatron(this._parent); + var section = builder.add_section("projects"); + + // project list + var columns = [ + "name", + "project_dir", + "engine_dir", + "actions", + ]; + var table = new Table(section, columns); + + for (const project of await zen_fetch("/prj/list")) + { + var cells = table.add_row([ + project.Id, + project.ProjectRootDir, + project.EngineRootDir, + "drop", + ]); + + cells[0].link("", {"page" : "project", "project" : project.Id}); + } + + // stats + section = builder.add_section("stats"); + var providers = zen_fetch("/stats"); + for (var provider of (await providers)["providers"]) + { + var stats = zen_fetch("/stats/" + provider); + + var section_provider = section.add_section(provider); + var table = new PropTable(section_provider); + + stats = zen_flatten(await stats); + for (const key in stats) + table.add_property(key, stats[key]); + } + } +} + +//////////////////////////////////////////////////////////////////////////////// +function add_branding(parent) +{ + var root = parent.tag().id("branding"); + + root.tag("pre").id("logo").text( + "_________ _______ __\n" + + "\\____ /___ ___ / ___// |__ ___ ______ ____\n" + + " / __/ __ \\ / \\ \\___ \\\\_ __// \\\\_ \\/ __ \\\n" + + " / \\ __// | \\/ \\| | ( - )| |\\/\\ __/\n" + + "/______/\\___/\\__|__/\\______/|__| \\___/ |__| \\___|" + ); + + root.tag("img").attr("src", "favicon.ico").id("ue_logo"); +} + +//////////////////////////////////////////////////////////////////////////////// +async function main_guarded() +{ + const root = new Component(document.body).tag().id("container"); + + add_branding(root); + + const params = new URLSearchParams(window.location.search); + const page = params.get("page"); + var impl = undefined; + + if (page == "project") impl = new Project(root, params); + if (page == "oplog") impl = new Oplog(root, params); + if (page == "entry") impl = new Entry(root, params); + if (page == undefined) impl = new Start(root, params); + + if (impl == undefined) + { + root.tag().text("unknown page"); + return; + } + + return impl.main(); +} + +//////////////////////////////////////////////////////////////////////////////// +async function main() +{ + try + { + return await main_guarded(); + } + catch (e) + { + var pane = new Component(document.body).tag().id("error"); + pane.tag("pre").text(e); + pane.tag("pre").text(e.stack); + throw e; + } +} + +/* +_________ _______ __ +\____ /___ ___ / ___// |__ ___ ______ ____ + / __/ __ \ / \ \___ \\_ __// \\_ \/ __ \ + / \ __// | \/ \| | ( - )| |\/\ __/ +/______/\___/\__|__/\______/|__| \___/ |__| \___| +*/ |