"use strict"; //////////////////////////////////////////////////////////////////////////////// class Friendly { static sep(value, prec=0) { return value.toLocaleString("en", { style: "decimal", minimumFractionDigits : prec, maximumFractionDigits : prec, }); } static k(x) { return Friendly.sep((x + 999) / Math.pow(10, 3), 0) + "K"; } static m(x) { return Friendly.sep( x / Math.pow(10, 6), 1) + "M"; } static g(x) { return Friendly.sep( x / Math.pow(10, 6), 2) + "G"; } static kib(x) { return Friendly.sep((x + 1023) / (1 << 10), 0) + " KiB"; } static mib(x) { return Friendly.sep( x / (1 << 20), 1) + " MiB"; } static gib(x) { return Friendly.sep( x / (1 << 30), 2) + " GiB"; } } //////////////////////////////////////////////////////////////////////////////// class ComponentBase { constructor(element) { if (element instanceof ComponentBase) element = element._element; this._element = element; } inner() { return this._element; } destroy() { this._element.parentNode.removeChild(this._element); } } //////////////////////////////////////////////////////////////////////////////// class ComponentDom extends ComponentBase { tag(tag="div") { var element = document.createElement(tag); this._element.appendChild(element); return this.new_component(element); } retag(new_tag) { if (this._element.tagName == new_tag.toUpperCase()) return this; var element = document.createElement(new_tag); element.innerHTML = this._element.innerHTML; this._element.parentNode.replaceChild(element, this._element); this._element = element; return this; } text(value) { value = value.toString(); this._element.innerHTML = (value != "") ? value : ""; return this; } id(value) { this._element.id = value; return this; } classify(value) { this._element.classList.add(value); return this; } css_var(key, value) { this._element.style.setProperty("--" + key, value); return this; } attr(key, value) { this._element.setAttribute(key, value); return this; } } //////////////////////////////////////////////////////////////////////////////// class ComponentInteract extends ComponentDom { 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, ...args) { const thunk = (src) => { if (src.target != this._element) return; func(...args); src.stopPropagation(); }; this._element.addEventListener(what, thunk); return this; } on_click(func, ...args) { this.classify("zen_action"); return this.on("click", func, ...args); } } //////////////////////////////////////////////////////////////////////////////// class Component extends ComponentInteract { new_component(...args) { return new Component(...args); } } //////////////////////////////////////////////////////////////////////////////// class TableCell extends Component { constructor(element, row) { super(element); this._row = row } get_table() { return this.get_row().get_table(); } get_row() { return this._row; } } //////////////////////////////////////////////////////////////////////////////// class TableRow extends Component { constructor(element, table, index, cells) { super(element); this._table = table; this._index = index; this._cells = cells; } *[Symbol.iterator]() { for (var cell of this._cells) yield cell; } get_table() { return this._table; } get_index() { return this._index; } get_cell(index) { return this._cells.at(index); } } //////////////////////////////////////////////////////////////////////////////// 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); this._rows = []; } *[Symbol.iterator]() { for (var row of this._rows) yield row; } get_row(index) { return this._rows.at(index); } _add_row(cells, indexed=true) { var index = -1; if (indexed && this._index >= 0) { index = this._index++; cells = [index, ...cells]; } cells = cells.slice(0, this._num_columns); while (cells.length < this._num_columns) cells.push(""); var ret = []; var row = this.tag() row = new TableRow(row, this, index, ret); for (const cell of cells) { var leaf = row.tag().text(cell); ret.push(new TableCell(leaf, row)); } if (this._index >= 0) ret.shift(); return row } add_row(...args) { var row = this._add_row(...args); this._rows.push(row); return row; } clear(index=0) { const elem = this._element; elem.replaceChildren(elem.firstElementChild); this._index = (this._index >= 0) ? index : -1; this._rows = []; } } //////////////////////////////////////////////////////////////////////////////// class PropTable extends Table { constructor(parent) { super(parent, ["prop", "value"], -1); this.classify("zen_proptable"); } add_property(key, value) { return this.add_row([key, value]); } add_object(object, friendly=false, prec=2) { const impl = (node, prefix="") => { for (const key in node) { var value = node[key]; if (value instanceof Object && (value.constructor.name == "Object" || value.constructor.name == "Array")) { impl(value, prefix + key + "."); continue; } if (friendly && typeof value == "number") { if (key.indexOf("memory") >= 0) value = Friendly.kib(value); else if (key.indexOf("disk") >= 0) value = Friendly.kib(value); else if (value > 100000) value = Friendly.k(value); else if (value % 1) value = Friendly.sep(value, 3); else value = Friendly.sep(value, 0); } this.add_property(prefix + key, value); } }; return impl(object); } filter(...needles) { for (var row of this) row.retag("div") if (needles.length == 0) return; for (var row of this) { var hide = false; var cell = row.get_cell(0); for (var needle of needles) hide = hide || (cell.inner().innerHTML.indexOf(needle) < 0); if (hide) row.retag("hidden"); } } } //////////////////////////////////////////////////////////////////////////////// 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, ...args) { const thunk = () => { this._root.destroy(); func(...args); }; 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); } } //////////////////////////////////////////////////////////////////////////////// class Fetcher { constructor() { this._resource = ""; this._query = {}; } resource(...parts) { var value = parts.join("/"); if (!value.startsWith("/")) value= "/" + value; this._resource = value; return this; } param(name, value) { this._query[name] = value; return this; } async json() { const response = await this._get("application/json"); return response ? (await response.json()) : {}; } async cbo() { const response = await this._get("application/x-ue-cb"); if (!response) return null; const buffer = await response.arrayBuffer(); const data = new Uint8Array(buffer); return new CbObject(data); } async delete() { const resource = this._build_uri(); const response = await fetch(resource, { "method" : "DELETE" }); } _build_uri() { var suffix = ""; for (var key in this._query) { suffix += suffix ? "&" : "?"; suffix += key + "=" + this._query[key]; } return this._resource + suffix; } async _get(accept="*") { const resource = this._build_uri(); const response = await fetch(resource, { "method" : "GET", "headers" : { "Accept": accept }, }); if (response.status >= 200 || response.status <= 299) return response; } } //////////////////////////////////////////////////////////////////////////////// class Page { constructor(parent, params) { this._parent = parent; this._params = params; this._sectormatron = new Sectormatron(parent); } set_title(name) { var value = document.title; if (name.length && value.length) name = value + " - " + name; document.title = name; } get_param(name, fallback=undefined) { var ret = this._params.get(name); if (ret != undefined) return ret; if (fallback != undefined) this.set_param(name, fallback); return fallback; } set_param(name, value, update=true) { this._params.set(name, value); if (!update) return value; const url = new URL(window.location); for (var [key, xfer] of this._params) url.searchParams.set(key, xfer) history.replaceState(null, "", url); return value; } add_section(name) { return this._sectormatron.add_section(name); } reload() { window.location.reload(); } } //////////////////////////////////////////////////////////////////////////////// class ZenPage extends Page { constructor(parent, ...args) { super(parent, ...args); super.set_title("zen"); this.add_branding(parent); this.generate_crumbs(); } add_branding(parent) { var root = parent.tag().id("branding"); const zen_store = root.tag("pre").id("logo").text( "_________ _______ __\n" + "\\____ /___ ___ / ___// |__ ___ ______ ____\n" + " / __/ __ \\ / \\ \\___ \\\\_ __// \\\\_ \\/ __ \\\n" + " / \\ __// | \\/ \\| | ( - )| |\\/\\ __/\n" + "/______/\\___/\\__|__/\\______/|__| \\___/ |__| \\___|" ); zen_store.tag().id("go_home").on_click(() => window.location.search = ""); root.tag("img").attr("src", "favicon.ico").id("ue_logo"); } set_title(...args) { super.set_title(...args); } generate_crumbs() { const auto_name = this.constructor.name; if (auto_name == "Start") return; const crumbs = this._parent.tag().id("crumbs"); const new_crumb = function(name, search=undefined) { crumbs.tag(); var crumb = crumbs.tag().text(name); if (search != undefined) crumb.on_click((x) => window.location.search = x, search); }; new_crumb("home", ""); var project = this.get_param("project"); if (project != undefined) { var oplog = this.get_param("oplog"); if (oplog != undefined) { new_crumb("project", `?page=project&project=${project}`); if (this.get_param("opkey")) new_crumb("oplog", `?page=oplog&project=${project}&oplog=${oplog}`); } } new_crumb(auto_name.toLowerCase()); } } //////////////////////////////////////////////////////////////////////////////// class Entry extends ZenPage { async main() { this.set_title("oplog entry"); const project = this.get_param("project"); const oplog = this.get_param("oplog"); const opkey = this.get_param("opkey"); var entry = new Fetcher() .resource("prj", project, "oplog", oplog, "entries") .param("opkey", opkey) .cbo() entry = await entry; entry = entry.as_object().find("entry").as_object(); const name = entry.find("key").as_value(); var section = this.add_section(name); // tree { var tree = entry.find("$tree"); if (tree == undefined) tree = this._convert_legacy_to_tree(entry); delete tree["$id"]; const sub_section = section.add_section("deps"); for (const dep_name in tree) { const dep_section = sub_section.add_section(dep_name); const table = new Table(dep_section, ["id", "name"]); for (const dep_id of tree[dep_name]) { const cell_values = [dep_id.toString(16)]; const row = table.add_row(cell_values); row.get_cell(0).on_click(() => void(0)); } } } // props { const object = entry.to_js_object(); var sub_section = section.add_section("props"); new PropTable(sub_section).add_object(object); } } _convert_legacy_to_tree(entry) { const tree = {}; var id = 0n; for (var item of entry.find("packagedata").as_array()) { var pkg_id = item.as_object().find("id"); if (pkg_id == undefined) continue; for (var x of pkg_id.as_value().subarray(0, 8)) { id <<= 8n; id |= BigInt(x); } break; } tree["$id"] = id; const pkgst_entry = entry.find("packagestoreentry").as_object(); const imported = pkgst_entry.find("importedpackageids"); if (imported == undefined) return tree; var out = tree["imported"] = []; for (var item of imported.as_array()) out.push(item.as_value(BigInt)); return tree; } } //////////////////////////////////////////////////////////////////////////////// class Oplog extends ZenPage { constructor(...args) { super(...args); this._index_start = Number(this.get_param("start", 0)); this._index_count = Number(this.get_param("count", 50)); this._entry_table = undefined; } async main() { const project = this.get_param("project"); const oplog = this.get_param("oplog"); this.set_title("oplog - " + 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("<<").on_click(() => this._on_next_prev(-10)); nav.left().add("prev") .on_click(() => this._on_next_prev( -1)); nav.left().add("next") .on_click(() => this._on_next_prev( 1)); nav.left().add(">>").on_click(() => this._on_next_prev( 10)); nav.left().sep(); for (var count of [10, 25, 50, 100]) { var handler = (n) => this._on_change_count(n); nav.left().add(count).on_click(handler, count); } nav.right().add("search:", "label"); nav.right().add("", "input").attr("disabled", ""); } async _build_table() { const project = this.get_param("project"); const oplog = this.get_param("oplog"); var entries = new Fetcher() .resource("prj", project, "oplog", oplog, "entries") .param("start", this.set_param("start", this._index_start)) .param("count", this.set_param("count", this._index_count)) .json(); this._entry_table.clear(this._index_start); entries = (await entries)["entries"]; if (entries == undefined) return for (const entry of entries) { var row = this._entry_table.add_row([ entry["key"] ]); row.get_cell(0).link("", { "page" : "entry", "project" : project, "oplog" : oplog, "opkey" : 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 ZenPage { async main() { // info var section = this.add_section("info"); const project = this.get_param("project"); this.set_title("project - " + project); var info = await new Fetcher().resource("prj", project).json(); 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 row = oplog_table.add_row([ name, ]); var cell = row.get_cell(0); cell.link("", { "page" : "oplog", "project" : project, "oplog" : name, }); cell = row.get_cell(-1); var action_tb = new Toolbar(cell, true); action_tb.left().add("drop").on_click((x) => this.drop_oplog(x), name); } } drop_oplog(oplog_id) { const drop = async () => { await new Fetcher() .resource("prj", this.get_param("project"), "oplog", oplog_id) .delete(); this.reload(); }; new Modal() .title("Confirmation") .message(`Drop oplog '${oplog_id}'?`) .option("Yes", () => drop()) .option("No") } } //////////////////////////////////////////////////////////////////////////////// class Test extends ZenPage { 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 tb_item_clicked = function(arg0, arg1) { alert(arg0 + " != " + arg1); }; var row = prop_table.add_property("toolbar", ""); toolbar = new Toolbar(row.get_cell(-1), true); toolbar.left() .add("tbitem0").on_click(tb_item_clicked, 11, -22); toolbar.left() .add("tbitem1").on_click(tb_item_clicked, 22, -33); toolbar.right().add("tbitem2").on_click(tb_item_clicked, 33, -55); toolbar.right().add("tbitem3").on_click(tb_item_clicked, 44, -88); this._cbo_test(); // error throw Error("deliberate error"); } async _cbo_test() { var data = new Uint8Array(await (await fetch("/prj/list")).arrayBuffer()); for (var item of new CbObject(data).as_array()) { for (var subitem of item.as_object()) { console.log(subitem.get_name(), subitem.as_value()); } } data = new Uint8Array(await (await fetch("/stats")).arrayBuffer()); { var item = new CbObject(data).as_object().find("providers").as_array(); console.log(item.num()); for (var subitem of item) { console.log(subitem.as_value()); data = new Uint8Array(await (await fetch("/stats/" + subitem.as_value())).arrayBuffer()); for (var ssitem of new CbObject(data).as_object()) { console.log(ssitem.get_name(), ssitem.as_value()); } } } } } //////////////////////////////////////////////////////////////////////////////// class Start extends ZenPage { async 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 new Fetcher().resource("/prj/list").json()) { var row = table.add_row([ "", project.ProjectRootDir, project.EngineRootDir, ]); var cell = row.get_cell(0); cell.tag().text(project.Id).on_click((x) => this.view_project(x), project.Id); var cell = row.get_cell(-1); var action_tb = new Toolbar(cell, true); action_tb.left().add("view").on_click((x) => this.view_project(x), project.Id); action_tb.left().add("drop").on_click((x) => this.drop_project(x), project.Id); } // stats section = this.add_section("stats"); columns = [ "name", "req count", "size disk", "size mem", "cid total", ]; const stats_table = new Table(section, columns); var providers = new Fetcher().resource("stats").json(); for (var provider of (await providers)["providers"]) { var stats = await new Fetcher().resource("stats", provider).json(); var values = [""]; try { values.push(stats.requests.count); const size_stat = (stats.store || stats.cache).size; values.push(Friendly.kib(size_stat.disk)); values.push(Friendly.kib(size_stat.memory)); values.push(stats.cid.size.total); } catch {} row = stats_table.add_row(values); row.get_cell(0).tag().text(provider).on_click((x) => this.view_stat(x), provider); } } view_stat(provider) { window.location = "?page=stat&provider=" + provider; } view_project(project_id) { window.location = "?page=project&project=" + project_id; } drop_project(project_id) { const drop = async () => { await new Fetcher().resource("prj", project_id).delete(); this.reload(); }; new Modal() .title("Confirmation") .message(`Drop project '${project_id}'?`) .option("Yes", () => drop()) .option("No"); } } //////////////////////////////////////////////////////////////////////////////// class Stat extends ZenPage { static TemporalStat = class { constructor(data, as_bytes) { this._data = data; this._as_bytes = as_bytes; } toString() { const columns = [ /* count */ {}, /* rate */ {}, /* t */ {}, {}, ]; const data = this._data; for (var key in data) { var out = columns[0]; if (key.startsWith("rate_")) out = columns[1]; else if (key.startsWith("t_p")) out = columns[3]; else if (key.startsWith("t_")) out = columns[2]; out[key] = data[key]; } var friendly = this._as_bytes ? Friendly.kib : Friendly.sep; var content = ""; for (var i = 0; i < columns.length; ++i) { content += "
";
				const column = columns[i];
				for (var key in column)
				{
					var value = column[key];
					if (i)
					{
						value = Friendly.sep(value, 2);
						key = key.padStart(9);
						content += key + ": " + value;
					}
					else
						content += friendly(value);
					content += "\n";
				}
				content += "
"; } return content; } } async main() { const provider = this.get_param("provider", "z$"); var stats = new Fetcher() .resource("stats", provider) .param("cidstorestats", "true") .param("cachestorestats", "true") .json(); this.set_title("stat - " + provider); const section = this.add_section(provider); var toolbar = new Toolbar(section); var tb_right = toolbar.right(); tb_right.add("filter:"); tb_right.add("-none-").on_click((x) => this.update_filter("")); for (var preset of ["read.", "write.", ".request", ".bytes"]) tb_right.add(preset).on_click((x) => this.update_filter(x), preset); { var input = tb_right.add("", "input") input.on("change", (x) => this.update_filter(x.inner().value), input); } this._table = new PropTable(section); this._stats = stats = await stats; this._condense(stats); var first = undefined; for (var name in stats) { first = first || name; toolbar.left().add(name).on_click((x) => this.view_category(x), name); } first = this.get_param("view", first); this.view_category(first); var filter = this.get_param("filter"); if (filter) this.update_filter(filter); } view_category(name) { const friendly = (this.get_param("raw") == undefined); this._table.clear(); this._table.add_object(this._stats[name], friendly, 3); this.set_param("view", name); this.set_param("filter", ""); } update_filter(needle) { this.set_param("filter", needle); if (!needle) return this._table.filter(); var needles = needle.split(" "); this._table.filter(...needles); } _condense(stats) { const impl = function(node) { for (var name in node) { const candidate = node[name]; if (!(candidate instanceof Object)) continue; if (candidate["rate_mean"] != undefined) { const as_bytes = (name.indexOf("bytes") >= 0); node[name] = new Stat.TemporalStat(candidate, as_bytes); continue; } impl(candidate); } } for (var name in stats) impl(stats[name]); } } //////////////////////////////////////////////////////////////////////////////// async function main_guarded() { const params = new URLSearchParams(window.location.search); const page = params.get("page"); const body = new Component(document.body).id(page); const root = body.tag().id("container").tag(); var impl = undefined; if (page == "project") impl = new Project(root, params); else if (page == "stat") impl = new Stat(root, params); else if (page == "oplog") impl = new Oplog(root, params); else if (page == "entry") impl = new Entry(root, params); else if (page == "test") impl = new Test(root, params); else 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; } } /* _________ _______ __ \____ /___ ___ / ___// |__ ___ ______ ____ / __/ __ \ / \ \___ \\_ __// \\_ \/ __ \ / \ __// | \/ \| | ( - )| |\/\ __/ /______/\___/\__|__/\______/|__| \___/ |__| \___| */