//////////////////////////////////////////////////////////////////////////////// class Component { constructor(element) { if (element instanceof Component) element = element._element; this._element = element; } inner() { return this._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; } on_click(func) { this.classify("zen_action"); return this.on("click", func); } } //////////////////////////////////////////////////////////////////////////////// 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"); const bg = this._root.tag().classify("zen_modal_bg"); bg.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 Toolbar extends Component { static Side = class extends Component { add(name, tag="div") { return this.tag(tag).text(name); } sep() { return this.tag().text("|").classify("zen_toolbar_sep"); } } constructor(parent, inline=false) { var root = parent.tag().classify("zen_toolbar"); super(root); if (inline) root.classify("zen_toolbar_inline"); this._left = new Toolbar.Side(root.tag()) this._right = new Toolbar.Side(root.tag()) } left() { return this._left; } right() { return this._right; } } //////////////////////////////////////////////////////////////////////////////// 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_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; this._sectormatron = new Sectormatron(parent); } set_title(name) { document.title = "zen - " + name; } get_param(name, fallback=undefined) { var ret = this._params.get(name); return (ret != undefined) ? ret : fallback; } add_section(name) { return this._sectormatron.add_section(name); } } //////////////////////////////////////////////////////////////////////////////// class Entry extends Page { async main() { this.set_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() { this.set_title("oplog"); const project = this.get_param("project"); const oplog = this.get_param("oplog"); var section = this.add_section(project + " - " + oplog); this._build_nav(section) this._entry_table = new Table(section, ["key"]); await this._build_table(); } _build_nav(section) { var nav = new Toolbar(section); nav.left().add("prev").on_click(() => this._on_next_prev(-1)); nav.left().add("next").on_click(() => this._on_next_prev( 1)); nav.left().sep(); for (var count of [10, 25, 50, 100]) { var handler = (e) => this._on_change_count(e.innerHTML); nav.left().add(count).on_click(handler); } nav.right().add("search:", "span"); nav.right().add("", "input").attr("disabled", ""); } 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); 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"], }); } } _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() { this.set_title("project"); // info var section = this.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 = this.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, ]); var cell = cells[0]; cell.link("", { "page" : "oplog", "project" : project, "oplog" : name, }); var action_tb = new Toolbar(cells.at(-1), true); action_tb.left().add("drop").attr("zen_param", name).on_click((e) => this._on_drop(e)); } // files /* section = this.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 Test extends Page { main() { var gen_word = (function() { var s = 0x314251; var r = function(a, b) { s = (s * 0x493) & 0x7fffffff; return ((s >> 3) % (b - a)) + a; }; return function(a=5, b=10) { const co = "aeioubcdfghjklmnpqrstvwxyz"; var ret = "" for (var i = 0, n = r(a,b); i < n; ++i) ret += co[r(0, co.length)]; return ret }; })(); var gen_para = function(a=5, b=10, s=" ") { var ret = gen_word(2, 9); for (var i = 0; i < ((ret.length * 0x493) % (b - a)) + b; ++i) ret += s + gen_word(2, 9); return ret; } this.set_title("test"); // section var section0 = this.add_section("section"); var section1 = section0.add_section("sub-section"); var section2 = section1.add_section("sub-sub-section"); // table const cols = [gen_word(), gen_word(), gen_word(), gen_word()]; var tables = [ new Table(section0, cols), new Table(section1, cols, 5), new Table(section2, cols, -1), ]; for (const table of tables) { table.add_row([gen_word()]); table.add_row([gen_word(), gen_word(), gen_word(), gen_word()]); table.add_row([gen_word(), gen_word(), gen_para(15, 25), gen_word(), gen_word(), gen_word(), gen_word(), gen_word()]); } // prop-table var pt_section = section0.add_section("prop-table") var prop_table = new PropTable(pt_section); for (var i = 0; i < 7; ++i) prop_table.add_property(gen_word(), gen_para(1, 14, "/")); // misc const misc_section = section0.add_section("misc").add_section("misc"); misc_section.tag().text("just text"); misc_section.tag().text("this is a link").link(); misc_section.tag().text("MODAL DIALOG").on_click((e) => { new Modal() .title("modal") .message("here is a message what I wrote") .option("press me!", () => { alert("hi"); }) .option("cancel", () => void(0)); }); // toolbar pt_section.add_section("toolbar"); var toolbar = new Toolbar(pt_section); for (const side of [toolbar.left(), toolbar.right()]) { side.add("tb_item0"); side.add("tb_item1"); side.sep(); side.add("tb_item2"); } var cell = prop_table.add_property("toolbar", ""); toolbar = new Toolbar(cell[1], true); toolbar.left().add("tbitem0"); toolbar.left().add("tbitem1"); toolbar.right().add("tbitem2"); toolbar.right().add("tbitem3"); // error throw Error("deliberate error"); } } //////////////////////////////////////////////////////////////////////////////// class Start extends Page { async main() { this.set_title("main"); var section = this.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, ]); cells[0].link("", {"page" : "project", "project" : project.Id}); var action_tb = new Toolbar(cells.at(-1), true); action_tb.left().add("view").on_click(() => void(0)); action_tb.left().add("drop").on_click(() => this.on_drop()); } // stats section = this.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]); } } on_drop() { new Modal().title("Confirmation").message("TODO").option("Okay"); } } //////////////////////////////////////////////////////////////////////////////// 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").tag(); 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 == "test") impl = new Test(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; } } /* _________ _______ __ \____ /___ ___ / ___// |__ ___ ______ ____ / __/ __ \ / \ \___ \\_ __// \\_ \/ __ \ / \ __// | \/ \| | ( - )| |\/\ __/ /______/\___/\__|__/\______/|__| \___/ |__| \___| */