diff options
| author | Martin Ridgers <[email protected]> | 2024-11-18 08:41:46 +0100 |
|---|---|---|
| committer | GitHub Enterprise <[email protected]> | 2024-11-18 08:41:46 +0100 |
| commit | cca69117b7ffac5cdd8933148ed9c94dd241528d (patch) | |
| tree | ba9dfce342e86d9cbdf6cf54059e1e7d618eecee /src | |
| parent | oplog prep gc fix (#216) (diff) | |
| download | zen-cca69117b7ffac5cdd8933148ed9c94dd241528d.tar.xz zen-cca69117b7ffac5cdd8933148ed9c94dd241528d.zip | |
Dashboard: oplog tree view (#217)
* Turned tables and progress bars and friends into "widgets!"
* A step to abstracting away a page's the internal DOM structure
* Folded sector creation into Page and pivoted it to a widget host
* Try and keep start/count as numbers regardless of input
* No need for the entry table to be defined up front now
* Add op count and log sixe to oplog list page
* Cache left side toolbar object
* Bounds count page start when building list of oplog entrie
* Start/end navigation tools
* Build rest of entry page while waiting for indexer to load
* Consistent naming with other pages
* Spacially consolidate fetching code
* Hide fetch latency to speed up index generation workers
* Extract dashboard structure from zen.js monolith
* Fix breadcrumbs after restructuring
* Add view link to actions cell of oplogs list
* Generator to enumerate names of entries in indexer
* Methods for simple traversal of component relations
* is() to check if a component is of a certain type
* Extend attr() to get and unset a component's attributes
* Unsetting all styles of anchor tags was underisrable
* Restore page name as id of container element
* A tree view of an oplog
* Move helper class out to private module scope
* Small tweak to use left var that already exists
* Changelog update
* Updated frontend .zip archive
Diffstat (limited to 'src')
| -rw-r--r-- | src/zenserver/frontend/html.zip | bin | 136697 -> 146336 bytes | |||
| -rw-r--r-- | src/zenserver/frontend/html/indexer/indexer.js | 7 | ||||
| -rw-r--r-- | src/zenserver/frontend/html/indexer/worker.js | 27 | ||||
| -rw-r--r-- | src/zenserver/frontend/html/pages/entry.js | 195 | ||||
| -rw-r--r-- | src/zenserver/frontend/html/pages/oplog.js | 176 | ||||
| -rw-r--r-- | src/zenserver/frontend/html/pages/page.js | 128 | ||||
| -rw-r--r-- | src/zenserver/frontend/html/pages/project.js | 92 | ||||
| -rw-r--r-- | src/zenserver/frontend/html/pages/start.js | 94 | ||||
| -rw-r--r-- | src/zenserver/frontend/html/pages/stat.js | 153 | ||||
| -rw-r--r-- | src/zenserver/frontend/html/pages/test.js | 147 | ||||
| -rw-r--r-- | src/zenserver/frontend/html/pages/tree.js | 118 | ||||
| -rw-r--r-- | src/zenserver/frontend/html/util/component.js | 158 | ||||
| -rw-r--r-- | src/zenserver/frontend/html/util/modal.js | 46 | ||||
| -rw-r--r-- | src/zenserver/frontend/html/util/widgets.js | 292 | ||||
| -rw-r--r-- | src/zenserver/frontend/html/zen.css | 48 | ||||
| -rw-r--r-- | src/zenserver/frontend/html/zen.js | 1386 |
16 files changed, 1682 insertions, 1385 deletions
diff --git a/src/zenserver/frontend/html.zip b/src/zenserver/frontend/html.zip Binary files differindex 1be5f0d9a..30d67ea51 100644 --- a/src/zenserver/frontend/html.zip +++ b/src/zenserver/frontend/html.zip diff --git a/src/zenserver/frontend/html/indexer/indexer.js b/src/zenserver/frontend/html/indexer/indexer.js index 8e5003edf..5bbb7c352 100644 --- a/src/zenserver/frontend/html/indexer/indexer.js +++ b/src/zenserver/frontend/html/indexer/indexer.js @@ -48,6 +48,13 @@ class Indexer if (name.indexOf(needle) >= 0) yield name; } + + *enum_names() + { + for (const page of this._pages) + for (const [_, name] of page) + yield name; + } } diff --git a/src/zenserver/frontend/html/indexer/worker.js b/src/zenserver/frontend/html/indexer/worker.js index 581746d6c..b8183cc6f 100644 --- a/src/zenserver/frontend/html/indexer/worker.js +++ b/src/zenserver/frontend/html/indexer/worker.js @@ -21,19 +21,31 @@ export class Message //////////////////////////////////////////////////////////////////////////////// async function map_id_to_key(project_id, oplog, start, end, page_size, stride) { + if (start >= end) + return postMessage(Message.create(Message.MapDone)); + const uri = "/prj/" + project_id + "/oplog/" + oplog + "/entries"; - while (start < end) - { - performance.mark("fetch"); + + const fetch_page = async function(index) { const cbo = new Fetcher() .resource(uri) - .param("start", start) + .param("start", index) .param("count", page_size) .param("fieldfilter", "packagedata,key") .cbo() - const entry_count = Math.min(page_size, -(start - end)); - var result = new Array(entry_count); + const entry_count = Math.min(page_size, -(index - end)); + return [await cbo, entry_count]; + }; + + var fetch = fetch_page(start); + while (fetch !== undefined) + { + performance.mark("fetch"); + + const [cbo, entry_count] = await fetch; + start += stride; + fetch = (start < end) ? fetch_page(start) : undefined; var entries = (await cbo).as_object().find("entries"); if (entries == undefined) @@ -45,6 +57,7 @@ async function map_id_to_key(project_id, oplog, start, end, page_size, stride) performance.mark("build"); var count = 0; + var result = new Array(entry_count); for (var entry of entries) { if (!entry.is_object()) @@ -84,8 +97,6 @@ async function map_id_to_key(project_id, oplog, start, end, page_size, stride) count++; } - start += stride; - if (count == 0) continue; diff --git a/src/zenserver/frontend/html/pages/entry.js b/src/zenserver/frontend/html/pages/entry.js new file mode 100644 index 000000000..b166d0a6f --- /dev/null +++ b/src/zenserver/frontend/html/pages/entry.js @@ -0,0 +1,195 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +"use strict"; + +import { ZenPage } from "./page.js" +import { Fetcher } from "../util/fetcher.js" +import { Table, PropTable, Toolbar, ProgressBar } from "../util/widgets.js" +import { create_indexer } from "../indexer/indexer.js" + +//////////////////////////////////////////////////////////////////////////////// +export class Page extends ZenPage +{ + main() + { + this.set_title("oplog entry"); + + const project = this.get_param("project"); + const oplog = this.get_param("oplog"); + const opkey = this.get_param("opkey"); + + this._entry = new Fetcher() + .resource("prj", project, "oplog", oplog, "entries") + .param("opkey", opkey) + .cbo(); + + this._indexer = this.load_indexer(project, oplog); + + this._build_page(); + } + + async load_indexer(project, oplog, loaded_cb) + { + const progress_bar = this.add_widget(ProgressBar); + progress_bar.set_progress("indexing"); + const indexer = await create_indexer(project, oplog, (...args) => { + progress_bar.set_progress(...args); + }); + progress_bar.destroy(); + return indexer; + } + + async _build_deps(section, tree) + { + const indexer = await this._indexer; + + for (const dep_name in tree) + { + const dep_section = section.add_section(dep_name); + const table = dep_section.add_widget(Table, ["name", "id"], Table.Flag_PackRight); + for (const dep_id of tree[dep_name]) + { + const cell_values = ["", dep_id.toString(16).padStart(16, "0")]; + const row = table.add_row(...cell_values); + + var opkey = indexer.lookup_id(dep_id); + row.get_cell(0).text(opkey).on_click((k) => this.view_opkey(k), opkey); + } + } + } + + async _build_page() + { + var entry = await this._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); + + if (tree == undefined) + return this._display_unsupported(section, entry); + + delete tree["$id"]; + + const sub_section = section.add_section("deps"); + this._build_deps(sub_section, tree); + } + + // data + { + const sub_section = section.add_section("data"); + const table = sub_section.add_widget(Table, ["name", "actions"], Table.Flag_PackRight); + for (const field_name of ["packagedata", "bulkdata"]) + { + var pkg_data = entry.find(field_name); + if (pkg_data == undefined) + continue; + + for (const item of pkg_data.as_array()) + { + var io_hash; + var file_name; + for (const field of item.as_object()) + { + if (field.is_named("data")) io_hash = field.as_value(); + else if (field.is_named("filename")) file_name = field.as_value(); + } + + if (io_hash instanceof Uint8Array) + { + var ret = ""; + for (var x of io_hash) + ret += x.toString(16).padStart(2, "0"); + io_hash = ret; + } + + const row = table.add_row(file_name); + + const project = this.get_param("project"); + const oplog = this.get_param("oplog"); + const link = row.get_cell(0).link( + "/" + ["prj", project, "oplog", oplog, io_hash].join("/") + ); + + const do_nothing = () => void(0); + const action_tb = new Toolbar(row.get_cell(-1), true); + action_tb.left().add("copy-hash").on_click(async (v) => { + await navigator.clipboard.writeText(v); + }, io_hash); + } + } + } + + // props + { + const object = entry.to_js_object(); + var sub_section = section.add_section("props"); + sub_section.add_widget(PropTable).add_object(object); + } + } + + _display_unsupported(section, entry) + { + const object = entry.to_js_object(); + const text = JSON.stringify(object, null, " "); + section.tag("pre").text(text); + } + + _convert_legacy_to_tree(entry) + { + const pkg_data = entry.find("packagedata"); + if (pkg_data == undefined) + return + + const tree = {}; + + var id = 0n; + for (var item of pkg_data.as_array()) + { + var pkg_id = item.as_object().find("id"); + if (pkg_id == undefined) + continue; + + pkg_id = pkg_id.as_value().subarray(0, 8); + for (var i = 7; i >= 0; --i) + { + id <<= 8n; + id |= BigInt(pkg_id[i]); + } + break; + } + tree["$id"] = id; + + const pkgst_entry = entry.find("packagestoreentry").as_object(); + + for (const field of pkgst_entry) + { + const field_name = field.get_name(); + if (!field_name.endsWith("importedpackageids")) + continue; + + var dep_name = field_name.slice(0, -18); + if (dep_name.length == 0) + dep_name = "imported"; + + var out = tree[dep_name] = []; + for (var item of field.as_array()) + out.push(item.as_value(BigInt)); + } + + return tree; + } + + view_opkey(opkey) + { + const params = this._params; + params.set("opkey", opkey); + window.location.search = params; + } +} diff --git a/src/zenserver/frontend/html/pages/oplog.js b/src/zenserver/frontend/html/pages/oplog.js new file mode 100644 index 000000000..f22c2a58f --- /dev/null +++ b/src/zenserver/frontend/html/pages/oplog.js @@ -0,0 +1,176 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +"use strict"; + +import { ZenPage } from "./page.js" +import { Fetcher } from "../util/fetcher.js" +import { Friendly } from "../util/friendly.js" +import { Table, Toolbar, ProgressBar } from "../util/widgets.js" +import { create_indexer } from "../indexer/indexer.js" + +//////////////////////////////////////////////////////////////////////////////// +export class Page extends ZenPage +{ + constructor(...args) + { + super(...args); + + this._index_start = Number(this.get_param("start", 0)) || 0; + this._index_count = Number(this.get_param("count", 50)) || 0; + } + + async main() + { + const project = this.get_param("project"); + const oplog = this.get_param("oplog"); + + var oplog_info = new Fetcher() + .resource("prj", project, "oplog", oplog) + .json(); + + this._indexer = this._load_indexer(project, oplog); + + this.set_title("oplog - " + oplog); + + var section = this.add_section(project + " - " + oplog); + + oplog_info = await oplog_info; + this._index_max = oplog_info["opcount"]; + this._build_nav(section, oplog_info); + + this._entry_table = section.add_widget(Table, ["key"]); + await this._build_table(this._index_start); + } + + async _load_indexer(project, oplog) + { + const progress_bar = this.add_widget(ProgressBar); + progress_bar.set_progress("indexing"); + var indexer = create_indexer(project, oplog, (...args) => { + progress_bar.set_progress(...args); + }); + indexer = await indexer; + progress_bar.destroy(); + return indexer; + } + + _build_nav(section, oplog_info) + { + const nav = section.add_widget(Toolbar); + const left = nav.left(); + left.add("|<") .on_click(() => this._on_next_prev(-10e10)); + left.add("<<").on_click(() => this._on_next_prev(-10)); + left.add("prev") .on_click(() => this._on_next_prev( -1)); + left.add("next") .on_click(() => this._on_next_prev( 1)); + left.add(">>").on_click(() => this._on_next_prev( 10)); + left.add(">|") .on_click(() => this._on_next_prev( 10e10)); + + left.sep(); + for (var count of [10, 25, 50, 100]) + { + var handler = (n) => this._on_change_count(n); + left.add(count).on_click(handler, count); + } + + left.sep(); + left.add("tree").link("", { + "page" : "tree", + "project" : this.get_param("project"), + "oplog" : this.get_param("oplog"), + }); + + const right = nav.right(); + right.add(Friendly.sep(oplog_info["opcount"])); + right.add("(" + Friendly.kib(oplog_info["totalsize"]) + ")"); + right.sep(); + + var search_input = right.add("search:", "label").tag("input") + search_input.on("change", (x) => this._search(x.inner().value), search_input); + } + + async _build_table(index) + { + this._index_count = Math.max(this._index_count, 1); + index = Math.min(index, this._index_max - this._index_count); + index = Math.max(index, 0); + + const project = this.get_param("project"); + const oplog = this.get_param("oplog"); + + var entries = new Fetcher() + .resource("prj", project, "oplog", oplog, "entries") + .param("start", index) + .param("count", this.set_param("count", this._index_count)) + .json(); + + entries = (await entries)["entries"]; + if (entries == undefined) + return; + + if (entries.length == 0) + return; + + this._entry_table.clear(index); + 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"], + }); + } + + this.set_param("start", index); + this._index_start = index; + } + + _on_change_count(value) + { + this._index_count = parseInt(value); + this._build_table(this._index_start); + } + + _on_next_prev(direction) + { + var index = this._index_start + (this._index_count * direction); + index = Math.max(0, index); + this._build_table(index); + } + + async _search(needle) + { + needle = needle.trim(); + if (needle.length < 3) + return; + + this._entry_table.clear(this._index_start); + + const project = this.get_param("project"); + const oplog = this.get_param("oplog"); + + const indexer = await this._indexer; + + var added = 0; + const truncate_at = this.get_param("searchmax") || 250; + for (var name of indexer.search(needle)) + { + var row = this._entry_table.add_row(name); + + row.get_cell(0).link("", { + "page" : "entry", + "project" : project, + "oplog" : oplog, + "opkey" : name, + }); + + if (++added >= truncate_at) + { + this._entry_table.add_row("...truncated"); + break; + } + } + } +} diff --git a/src/zenserver/frontend/html/pages/page.js b/src/zenserver/frontend/html/pages/page.js new file mode 100644 index 000000000..9a9541904 --- /dev/null +++ b/src/zenserver/frontend/html/pages/page.js @@ -0,0 +1,128 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +"use strict"; + +import { WidgetHost } from "../util/widgets.js" + +//////////////////////////////////////////////////////////////////////////////// +export class PageBase extends WidgetHost +{ + constructor(parent, params) + { + super(parent) + this._params = params; + } + + 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; + } + + reload() + { + window.location.reload(); + } +} + + + +//////////////////////////////////////////////////////////////////////////////// +export class ZenPage extends PageBase +{ + 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.get_param("page") || "start"; + if (auto_name == "start") + return; + + const crumbs = this.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()); + } +} diff --git a/src/zenserver/frontend/html/pages/project.js b/src/zenserver/frontend/html/pages/project.js new file mode 100644 index 000000000..05e8efc9f --- /dev/null +++ b/src/zenserver/frontend/html/pages/project.js @@ -0,0 +1,92 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +"use strict"; + +import { ZenPage } from "./page.js" +import { Fetcher } from "../util/fetcher.js" +import { Friendly } from "../util/friendly.js" +import { Modal } from "../util/modal.js" +import { Table, PropTable, Toolbar } from "../util/widgets.js" + +//////////////////////////////////////////////////////////////////////////////// +export class Page 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 = section.add_widget(PropTable); + 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 = section.add_widget( + Table, + ["name", "marker", "size", "ops", "expired", "actions"], + Table.Flag_PackRight + ) + + var count = 0; + for (const oplog of info["oplogs"]) + { + const name = oplog["id"]; + + var info = new Fetcher().resource("prj", project, "oplog", name).json(); + + var row = oplog_table.add_row(name); + + var cell = row.get_cell(0); + this.as_link(cell, "oplog", name) + + cell = row.get_cell(-1); + const action_tb = new Toolbar(cell, true).left(); + this.as_link(action_tb.add("view"), "oplog", name); + this.as_link(action_tb.add("tree"), "tree", name); + action_tb.add("drop").on_click((x) => this.drop_oplog(x), name); + + info = await info; + row.get_cell(1).text(info["markerpath"]); + row.get_cell(2).text(Friendly.kib(info["totalsize"])); + row.get_cell(3).text(Friendly.sep(info["opcount"])); + row.get_cell(4).text(info["expired"]); + } + } + + as_link(component, page, oplog_id) + { + component.link("", { + "page" : page, + "project" : this.get_param("project"), + "oplog" : oplog_id, + }); + } + + 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"); + } +} diff --git a/src/zenserver/frontend/html/pages/start.js b/src/zenserver/frontend/html/pages/start.js new file mode 100644 index 000000000..8c9df62f9 --- /dev/null +++ b/src/zenserver/frontend/html/pages/start.js @@ -0,0 +1,94 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +"use strict"; + +import { ZenPage } from "./page.js" +import { Fetcher } from "../util/fetcher.js" +import { Friendly } from "../util/friendly.js" +import { Table, Toolbar } from "../util/widgets.js" + +//////////////////////////////////////////////////////////////////////////////// +export class Page extends ZenPage +{ + async main() + { + var section = this.add_section("projects"); + + // project list + var columns = [ + "name", + "project_dir", + "engine_dir", + "actions", + ]; + var table = section.add_widget(Table, 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 = section.add_widget(Table, columns, Table.Flag_PackRight); + 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"); + } +} diff --git a/src/zenserver/frontend/html/pages/stat.js b/src/zenserver/frontend/html/pages/stat.js new file mode 100644 index 000000000..c7902d5ed --- /dev/null +++ b/src/zenserver/frontend/html/pages/stat.js @@ -0,0 +1,153 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +"use strict"; + +import { ZenPage } from "./page.js" +import { Fetcher } from "../util/fetcher.js" +import { Friendly } from "../util/friendly.js" +import { PropTable, Toolbar } from "../util/widgets.js" + +//////////////////////////////////////////////////////////////////////////////// +class TemporalStat +{ + 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 += "<pre>"; + 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 += "</pre>"; + } + + return content; + } +} + +//////////////////////////////////////////////////////////////////////////////// +export class Page extends ZenPage +{ + 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 = section.add_widget(Toolbar); + 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); + this._filter_input = tb_right.add("", "label").tag("input"); + this._filter_input.on("change", (x) => this.update_filter(x.inner().value), this._filter_input); + + this._table = section.add_widget(PropTable); + + 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); + } + + var filter = this.get_param("filter"); + + first = this.get_param("view", first); + this.view_category(first); + + 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.update_filter(""); + } + + update_filter(needle) + { + this._filter_input.attr("value", 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 TemporalStat(candidate, as_bytes); + continue; + } + + impl(candidate); + } + } + + for (var name in stats) + impl(stats[name]); + } +} diff --git a/src/zenserver/frontend/html/pages/test.js b/src/zenserver/frontend/html/pages/test.js new file mode 100644 index 000000000..2a84ff163 --- /dev/null +++ b/src/zenserver/frontend/html/pages/test.js @@ -0,0 +1,147 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +"use strict"; + +import { ZenPage } from "./page.js" +import { Table, PropTable, Toolbar, ProgressBar } from "../util/widgets.js" +import { Modal, } from "../util/modal.js" + +//////////////////////////////////////////////////////////////////////////////// +export class Page 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"); + + // swatches + const swatches = this.tag() + .style("position", "absolute") + .style("top", "3.5em") + .style("left", "3.5em") + for (var suffix of ["g0", "g1", "g2", "g3", "g4", + "p0", "p1", "p2", "p3", "p4", + "ln", "er"]) + { + swatches.tag() + .style("float", "left") + .style("width", "2em") + .style("height", "2em") + .style("background-color", `var(--theme_${suffix})`) + .text(suffix); + } + + // 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 = [ + section0.add_widget(Table, cols), + section1.add_widget(Table, cols, Table.Flag_EvenSpacing, 5), + section2.add_widget(Table, cols, Table.Flag_EvenSpacing, -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()); + } + + // spacing tests + { + const spacing_section = section0.add_section("spacing"); + const flags = { + "EvenSpacing" : Table.Flag_EvenSpacing, + "EvenSpacing|BiasLeft" : Table.Flag_EvenSpacing | Table.Flag_BiasLeft, + "PackRight" : Table.Flag_PackRight, + }; + for (const flag_name in flags) + { + const flag = flags[flag_name]; + const another_table = spacing_section.add_widget( + Table, + [flag_name, gen_word(), gen_word(), gen_word(), gen_word()], + flag, + ); + for (var i = 0; i < 3; ++i) + another_table.add_row(gen_para(1, 5), gen_para(1, 3), gen_word(), gen_word(), gen_word()); + } + } + + // prop-table + var pt_section = section0.add_section("prop-table") + var prop_table = pt_section.add_widget(PropTable); + for (var i = 0; i < 7; ++i) + prop_table.add_property(gen_word(), gen_para(1, 20, "/")); + + // 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 = pt_section.add_widget(Toolbar); + 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); + + // progress bar + const progress_bar = this.add_widget(ProgressBar); + setInterval(function() { + var count = 0 + return () => { + count = (count + 1) % 100; + progress_bar.set_progress("testing", count, 100); + }; + }(), 49.3); + + // error + throw Error("deliberate error"); + } +} diff --git a/src/zenserver/frontend/html/pages/tree.js b/src/zenserver/frontend/html/pages/tree.js new file mode 100644 index 000000000..c3cea0eb5 --- /dev/null +++ b/src/zenserver/frontend/html/pages/tree.js @@ -0,0 +1,118 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +"use strict"; + +import { ZenPage } from "./page.js" +import { ProgressBar } from "../util/widgets.js" +import { create_indexer } from "../indexer/indexer.js" + +//////////////////////////////////////////////////////////////////////////////// +export class Page extends ZenPage +{ + main() + { + const project = this.get_param("project"); + const oplog = this.get_param("oplog"); + + this._indexer = this._load_indexer(project, oplog); + + this.set_title("tree - " + oplog); + const section = this.add_section(project + " - " + oplog); + + const list = section.tag().id("tree_root").tag("ul"); + const root = list.tag("li"); + root.attr("part", "/"); + root.tag().text("/"); + this._expand(root); + } + + async _load_indexer(project, oplog) + { + const progress_bar = this.add_widget(ProgressBar); + progress_bar.set_progress("indexing"); + var indexer = create_indexer(project, oplog, (...args) => { + progress_bar.set_progress(...args); + }); + indexer = await indexer; + progress_bar.destroy(); + return indexer; + } + + async _expand(node) + { + var prefix = ""; + for (var item = node;; item = item.parent()) + { + if (item.is("div")) break; + if (!item.is("li")) continue; + prefix = item.attr("part") + prefix; + } + console.log(prefix); + + const indexer = await this._indexer; + + const new_nodes = new Object(); + for (var name of indexer.enum_names()) + { + if (!name.startsWith(prefix)) + continue; + + name = name.substr(prefix.length); + const slash = name.indexOf("/"); + if (slash != -1) + name = name.substr(0, slash + 1); + + if (new_nodes[name] === undefined) + new_nodes[name] = 1; + else + new_nodes[name] += 1; + } + + const by_count = this.get_param("bycount", 0)|0; + const sorted_keys = Object.keys(new_nodes).sort((l, r) => { + const is_node_l = l.endsWith("/"); + const any_nodes = is_node_l + r.endsWith("/"); + if (any_nodes == 0) return r < l; + if (any_nodes == 1) return is_node_l ? -1 : 1; + return by_count ? (new_nodes[r] - new_nodes[l]) : (r < l); + }) + + const list = node.tag("ul"); + for (const name of sorted_keys) + { + const item = list.tag("li").attr("part", name); + const info = item.tag(); + const label = info.tag().text(name); + if (name.endsWith("/")) + { + label.on_click((x) => this.expand_collapse(x), item); + info.tag().text(new_nodes[name]); + continue; + } + + item.attr("leaf", ""); + label.link("", { + "page" : "entry", + "project" : this.get_param("project"), + "oplog" : this.get_param("oplog"), + "opkey" : prefix + name, + }); + info.tag(); + } + + node.attr("expanded", "") + } + + _collapse(node) + { + node.first_child().next_sibling().destroy(); + node.attr("expanded", null); + } + + expand_collapse(node) + { + if (node.attr("expanded") === null) + return this._expand(node); + return this._collapse(node); + } +} diff --git a/src/zenserver/frontend/html/util/component.js b/src/zenserver/frontend/html/util/component.js new file mode 100644 index 000000000..39a9f2fe6 --- /dev/null +++ b/src/zenserver/frontend/html/util/component.js @@ -0,0 +1,158 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +"use strict"; + +//////////////////////////////////////////////////////////////////////////////// +class ComponentBase +{ + constructor(element) + { + if (element instanceof ComponentBase) + element = element._element; + + this._element = element; + } + + inner() + { + return this._element; + } + + parent() + { + return this.new_component(this._element.parentElement); + } + + first_child() + { + return this.new_component(this._element.firstElementChild); + } + + next_sibling() + { + return this.new_component(this._element.nextElementSibling); + } + + destroy() + { + this._element.parentNode.removeChild(this._element); + } +} + +//////////////////////////////////////////////////////////////////////////////// +class ComponentDom extends ComponentBase +{ + is(tag) + { + return this._element.tagName == tag.toUpperCase(); + } + + 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 == undefined) ? "undefined" : 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; + } + + style(key, value) + { + this._element.style[key] = value; + return this; + } + + attr(key, value=undefined) + { + if (value === undefined) + return this._element.getAttribute(key); + else if (value === null) + this._element.removeAttribute(key); + else + 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); + } +} + +//////////////////////////////////////////////////////////////////////////////// +export class Component extends ComponentInteract +{ + new_component(...args) + { + return new Component(...args); + } +} diff --git a/src/zenserver/frontend/html/util/modal.js b/src/zenserver/frontend/html/util/modal.js new file mode 100644 index 000000000..a28b013d1 --- /dev/null +++ b/src/zenserver/frontend/html/util/modal.js @@ -0,0 +1,46 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +"use strict"; + +import { Component } from "./component.js" + +//////////////////////////////////////////////////////////////////////////////// +export 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(); + if (func) + func(...args); + }; + this._buttons.tag().text(name).on("click", thunk); + return this; + } +} diff --git a/src/zenserver/frontend/html/util/widgets.js b/src/zenserver/frontend/html/util/widgets.js new file mode 100644 index 000000000..e567a7a00 --- /dev/null +++ b/src/zenserver/frontend/html/util/widgets.js @@ -0,0 +1,292 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +"use strict"; + +import { Component } from "./component.js" +import { Friendly } from "../util/friendly.js" + +//////////////////////////////////////////////////////////////////////////////// +class Widget extends Component +{ +} + + + +//////////////////////////////////////////////////////////////////////////////// +class TableCell extends Widget +{ + constructor(element, row) + { + super(element); + this._row = row; + } + + get_table() { return this.get_row().get_table(); } + get_row() { return this._row; } +} + +//////////////////////////////////////////////////////////////////////////////// +class TableRow extends Widget +{ + 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); } +} + +//////////////////////////////////////////////////////////////////////////////// +export class Table extends Widget +{ + static Flag_EvenSpacing = 1 << 0; + static Flag_PackRight = 1 << 1; + static Flag_BiasLeft = 1 << 2; + static Flag_FitLeft = 1 << 3; + + constructor(parent, column_names, flags=Table.Flag_EvenSpacing, index_base=0) + { + var root = parent.tag().classify("zen_table"); + super(root); + + var column_style; + if (flags & Table.Flag_FitLeft) column_style = "max-content"; + else if (flags & Table.Flag_BiasLeft) column_style = "2fr"; + else column_style = "1fr"; + for (var i = 1; i < column_names.length; ++i) + { + const style = (flags & Table.Flag_PackRight) ? " auto" : " 1fr"; + column_style += style; + } + + if (index_base >= 0) + { + column_names = ["#", ...column_names]; + column_style = "max-content " + column_style; + } + + root.style("gridTemplateColumns", column_style); + + this._add_row(column_names, false); + + this._index = index_base; + this._num_columns = column_names.length; + 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 = []; + } +} + + + +//////////////////////////////////////////////////////////////////////////////// +export class PropTable extends Table +{ + constructor(parent) + { + super(parent, ["prop", "value"], Table.Flag_FitLeft, -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"); + } + } +} + + + +//////////////////////////////////////////////////////////////////////////////// +export class Toolbar extends Widget +{ + static Side = class extends Widget + { + 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; } +} + + + +//////////////////////////////////////////////////////////////////////////////// +export class ProgressBar extends Widget +{ + constructor(parent) + { + const root = parent.tag().classify("zen_progressbar"); + super(root); + this._label = root.tag(); + root.tag(); // bg + this._bar = root.tag(); + } + + set_progress(what, count=0, end=1) + { + const percent = (((count * 100) / end) | 0).toString() + "%"; + this._bar.style("width", percent); + this._label.text(`${what}... ${count}/${end} (${percent})`); + } +} + + + +//////////////////////////////////////////////////////////////////////////////// +export class WidgetHost +{ + constructor(parent, depth=1) + { + this._parent = parent; + this._depth = depth; + } + + add_section(name) + { + var node = this._parent.tag(); + if (this._depth == 1) + node.classify("zen_sector"); + + node.tag("h" + this._depth).text(name); + return new WidgetHost(node, this._depth + 1); + } + + add_widget(type, ...args) + { + if (!(type.prototype instanceof Widget)) + throw Error("Incorrect widget type"); + + return new type(this._parent, ...args); + } + + tag(...args) + { + return this._parent.tag(...args); + } +} diff --git a/src/zenserver/frontend/html/zen.css b/src/zenserver/frontend/html/zen.css index 033563736..2463a6b5d 100644 --- a/src/zenserver/frontend/html/zen.css +++ b/src/zenserver/frontend/html/zen.css @@ -79,8 +79,11 @@ input { /* links -------------------------------------------------------------------- */ +a { + text-decoration: unset; +} + .zen_action, a { - all: unset; cursor: pointer; color: var(--theme_ln); @@ -401,3 +404,46 @@ input { float: left; min-width: 15%; } + +/* tree --------------------------------------------------------------------- */ + +#tree { + #tree_root { + margin-left: 5em; + margin-right: 10em; + } + ul { + list-style-type: none; + padding-left: 0; + margin-left: 2em; + } + li > div { + display: flex; + border-bottom: 1px solid transparent; + padding-left: 0.3em; + padding-right: 0.3em; + } + li > div > div:last-child { + margin-left: auto; + } + li > div:hover { + background-color: var(--theme_p4); + border-bottom: 1px solid var(--theme_g2); + } + li a { + font-weight: bolder; + } + li::marker { + content: "+"; + color: var(--theme_g1); + } + li[expanded]::marker { + content: "-"; + } + li[leaf]::marker { + content: "|"; + } + li:last-child::marker { + content: "\\"; + } +} diff --git a/src/zenserver/frontend/html/zen.js b/src/zenserver/frontend/html/zen.js index ffeaeb4ee..19f3ec69f 100644 --- a/src/zenserver/frontend/html/zen.js +++ b/src/zenserver/frontend/html/zen.js @@ -2,1361 +2,7 @@ "use strict"; -import { Fetcher } from "./util/fetcher.js" -import { Friendly } from "./util/friendly.js" -import { create_indexer } from "./indexer/indexer.js" - -//////////////////////////////////////////////////////////////////////////////// -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 == undefined) ? "undefined" : 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; - } - - style(key, value) - { - this._element.style[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 -{ - static Flag_EvenSpacing = 1 << 0; - static Flag_PackRight = 1 << 1; - static Flag_BiasLeft = 1 << 2; - static Flag_FitLeft = 1 << 3; - - constructor(parent, column_names, flags=Table.Flag_EvenSpacing, index_base=0) - { - var root = parent.tag().classify("zen_table"); - super(root); - - var column_style; - if (flags & Table.Flag_FitLeft) column_style = "max-content"; - else if (flags & Table.Flag_BiasLeft) column_style = "2fr"; - else column_style = "1fr"; - for (var i = 1; i < column_names.length; ++i) - { - const style = (flags & Table.Flag_PackRight) ? " auto" : " 1fr"; - column_style += style; - } - - if (index_base >= 0) - { - column_names = ["#", ...column_names]; - column_style = "max-content " + column_style; - } - - root.style("gridTemplateColumns", column_style); - - this._add_row(column_names, false); - - this._index = index_base; - this._num_columns = column_names.length; - 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"], Table.Flag_FitLeft, -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 ProgressBar extends Component -{ - constructor(parent) - { - const root = parent.tag().classify("zen_progressbar"); - super(root); - this._label = root.tag(); - root.tag(); // bg - this._bar = root.tag(); - } - - set_progress(what, count=0, end=1) - { - const percent = (((count * 100) / end) | 0).toString() + "%"; - this._bar.style("width", percent); - this._label.text(`${what}... ${count}/${end} (${percent})`); - } -} - - - -//////////////////////////////////////////////////////////////////////////////// -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 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 -{ - main() - { - this.set_title("oplog entry"); - - const project = this.get_param("project"); - const oplog = this.get_param("oplog"); - const opkey = this.get_param("opkey"); - - this._entry = new Fetcher() - .resource("prj", project, "oplog", oplog, "entries") - .param("opkey", opkey) - .cbo(); - - this.load_indexer(project, oplog, () => this._build_page()); - } - - async load_indexer(project, oplog, loaded_cb) - { - if (this._indexer != undefined) - return loaded_cb(); - - const progress_bar = new ProgressBar(this._parent); - progress_bar.set_progress("indexing"); - const indexer = create_indexer(project, oplog, (...args) => { - progress_bar.set_progress(...args); - }); - this._indexer = await indexer; - progress_bar.destroy(); - - loaded_cb(); - } - - async _build_page() - { - var entry = await this._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); - - if (tree == undefined) - return this._display_unsupported(section, 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, ["name", "id"], Table.Flag_PackRight); - for (const dep_id of tree[dep_name]) - { - const cell_values = ["", dep_id.toString(16).padStart(16, "0")]; - const row = table.add_row(...cell_values); - - var opkey = this._indexer.lookup_id(dep_id); - row.get_cell(0).text(opkey).on_click((k) => this.view_opkey(k), opkey); - } - } - } - - // data - { - const sub_section = section.add_section("data"); - const table = new Table(sub_section, ["name", "actions"], Table.Flag_PackRight); - for (const field_name of ["packagedata", "bulkdata"]) - { - var pkg_data = entry.find(field_name); - if (pkg_data == undefined) - continue; - - for (const item of pkg_data.as_array()) - { - var io_hash; - var file_name; - for (const field of item.as_object()) - { - if (field.is_named("data")) io_hash = field.as_value(); - else if (field.is_named("filename")) file_name = field.as_value(); - } - - if (io_hash instanceof Uint8Array) - { - var ret = ""; - for (var x of io_hash) - ret += x.toString(16).padStart(2, "0"); - io_hash = ret; - } - - const row = table.add_row(file_name); - - const project = this.get_param("project"); - const oplog = this.get_param("oplog"); - const link = row.get_cell(0).link( - "/" + ["prj", project, "oplog", oplog, io_hash].join("/") - ); - - const do_nothing = () => void(0); - const action_tb = new Toolbar(row.get_cell(-1), true); - action_tb.left().add("copy-hash").on_click(async (v) => { - await navigator.clipboard.writeText(v); - }, io_hash); - } - } - } - - // props - { - const object = entry.to_js_object(); - var sub_section = section.add_section("props"); - new PropTable(sub_section).add_object(object); - } - } - - _display_unsupported(section, entry) - { - const object = entry.to_js_object(); - const text = JSON.stringify(object, null, " "); - section.tag("pre").text(text); - } - - _convert_legacy_to_tree(entry) - { - const pkg_data = entry.find("packagedata"); - if (pkg_data == undefined) - return - - const tree = {}; - - var id = 0n; - for (var item of pkg_data.as_array()) - { - var pkg_id = item.as_object().find("id"); - if (pkg_id == undefined) - continue; - - pkg_id = pkg_id.as_value().subarray(0, 8); - for (var i = 7; i >= 0; --i) - { - id <<= 8n; - id |= BigInt(pkg_id[i]); - } - break; - } - tree["$id"] = id; - - const pkgst_entry = entry.find("packagestoreentry").as_object(); - - for (const field of pkgst_entry) - { - const field_name = field.get_name(); - if (!field_name.endsWith("importedpackageids")) - continue; - - var dep_name = field_name.slice(0, -18); - if (dep_name.length == 0) - dep_name = "imported"; - - var out = tree[dep_name] = []; - for (var item of field.as_array()) - out.push(item.as_value(BigInt)); - } - - return tree; - } - - view_opkey(opkey) - { - const params = this._params; - params.set("opkey", opkey); - window.location.search = params; - } -} - - - -//////////////////////////////////////////////////////////////////////////////// -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._indexer = this._create_indexer(project, 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(); - } - - async _create_indexer(project, oplog) - { - const progress_bar = new ProgressBar(this._parent); - progress_bar.set_progress("indexing"); - var indexer = create_indexer(project, oplog, (...args) => { - progress_bar.set_progress(...args); - }); - indexer = await indexer; - progress_bar.destroy(); - return indexer; - } - - _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); - } - - var search_input = nav.right().add("search:", "label").tag("input") - search_input.on("change", (x) => this._search(x.inner().value), search_input); - } - - 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(); - } - - async _search(needle) - { - needle = needle.trim(); - if (needle.length < 3) - return; - - this._entry_table.clear(this._index_start); - - const project = this.get_param("project"); - const oplog = this.get_param("oplog"); - - const indexer = await this._indexer; - - var added = 0; - const truncate_at = this.get_param("searchmax") || 250; - for (var name of indexer.search(needle)) - { - var row = this._entry_table.add_row(name); - - row.get_cell(0).link("", { - "page" : "entry", - "project" : project, - "oplog" : oplog, - "opkey" : name, - }); - - if (++added >= truncate_at) - { - this._entry_table.add_row("...truncated"); - break; - } - } - } -} - - - -//////////////////////////////////////////////////////////////////////////////// -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", "marker", "size", "ops", "expired", "actions"], - Table.Flag_PackRight - ) - - var count = 0; - for (const oplog of info["oplogs"]) - { - const name = oplog["id"]; - - var info = new Fetcher().resource("prj", project, "oplog", name).json(); - - 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); - - info = await info; - row.get_cell(1).text(info["markerpath"]); - row.get_cell(2).text(Friendly.kib(info["totalsize"])); - row.get_cell(3).text(Friendly.sep(info["opcount"])); - row.get_cell(4).text(info["expired"]); - } - } - - 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"); - - // swatches - const swatches = this._parent.tag() - .style("position", "absolute") - .style("top", "3.5em") - .style("left", "3.5em") - for (var suffix of ["g0", "g1", "g2", "g3", "g4", - "p0", "p1", "p2", "p3", "p4", - "ln", "er"]) - { - var swatch = swatches.tag() - .style("float", "left") - .style("width", "2em") - .style("height", "2em") - .style("background-color", `var(--theme_${suffix})`) - .text(suffix); - } - - // 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, Table.Flag_EvenSpacing, 5), - new Table(section2, cols, Table.Flag_EvenSpacing, -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()); - } - - // spacing tests - { - const spacing_section = section0.add_section("spacing"); - const flags = { - "EvenSpacing" : Table.Flag_EvenSpacing, - "EvenSpacing|BiasLeft" : Table.Flag_EvenSpacing | Table.Flag_BiasLeft, - "PackRight" : Table.Flag_PackRight, - }; - for (const flag_name in flags) - { - const flag = flags[flag_name]; - const another_table = new Table( - spacing_section, - [flag_name, gen_word(), gen_word(), gen_word(), gen_word()], - flag, - ); - for (var i = 0; i < 3; ++i) - another_table.add_row(gen_para(1, 5), gen_para(1, 3), 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, 20, "/")); - - // 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); - - // progress bar - const progress_bar = new ProgressBar(this._parent); - setInterval(function() { - var count = 0 - return () => { - count = (count + 1) % 100; - progress_bar.set_progress("testing", count, 100); - }; - }(), 49.3); - - // error - throw Error("deliberate error"); - } -} - - - -//////////////////////////////////////////////////////////////////////////////// -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, Table.Flag_PackRight); - 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 += "<pre>"; - 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 += "</pre>"; - } - - 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); - this._filter_input = tb_right.add("", "label").tag("input"); - this._filter_input.on("change", (x) => this.update_filter(x.inner().value), this._filter_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); - } - - var filter = this.get_param("filter"); - - first = this.get_param("view", first); - this.view_category(first); - - 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.update_filter(""); - } - - update_filter(needle) - { - this._filter_input.attr("value", 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]); - } -} - - +import { Component } from "./util/component.js" //////////////////////////////////////////////////////////////////////////////// function display_error(message, stack) @@ -1378,32 +24,20 @@ window.addEventListener("unhandledrejection", function(evt) { display_error(reason.message, reason.stack); }); - - //////////////////////////////////////////////////////////////////////////////// async function main() { - const params = new URLSearchParams(window.location.search); - const page = params.get("page"); - - const body = new Component(document.body).id(page); + const body = new Component(document.body); 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; - } - - impl.main(); + const params = new URLSearchParams(window.location.search); + var page = params.get("page") || "start"; + page = page.replace(".", ""); + page = page.replace("/", ""); + page = page.replace("\\", ""); + root.id(page); + const module = await import(`./pages/${page}.js`); + new module.Page(root, params).main(); } main(); |