aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMartin Ridgers <[email protected]>2024-11-18 08:41:46 +0100
committerGitHub Enterprise <[email protected]>2024-11-18 08:41:46 +0100
commitcca69117b7ffac5cdd8933148ed9c94dd241528d (patch)
treeba9dfce342e86d9cbdf6cf54059e1e7d618eecee /src
parentoplog prep gc fix (#216) (diff)
downloadzen-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.zipbin136697 -> 146336 bytes
-rw-r--r--src/zenserver/frontend/html/indexer/indexer.js7
-rw-r--r--src/zenserver/frontend/html/indexer/worker.js27
-rw-r--r--src/zenserver/frontend/html/pages/entry.js195
-rw-r--r--src/zenserver/frontend/html/pages/oplog.js176
-rw-r--r--src/zenserver/frontend/html/pages/page.js128
-rw-r--r--src/zenserver/frontend/html/pages/project.js92
-rw-r--r--src/zenserver/frontend/html/pages/start.js94
-rw-r--r--src/zenserver/frontend/html/pages/stat.js153
-rw-r--r--src/zenserver/frontend/html/pages/test.js147
-rw-r--r--src/zenserver/frontend/html/pages/tree.js118
-rw-r--r--src/zenserver/frontend/html/util/component.js158
-rw-r--r--src/zenserver/frontend/html/util/modal.js46
-rw-r--r--src/zenserver/frontend/html/util/widgets.js292
-rw-r--r--src/zenserver/frontend/html/zen.css48
-rw-r--r--src/zenserver/frontend/html/zen.js1386
16 files changed, 1682 insertions, 1385 deletions
diff --git a/src/zenserver/frontend/html.zip b/src/zenserver/frontend/html.zip
index 1be5f0d9a..30d67ea51 100644
--- a/src/zenserver/frontend/html.zip
+++ b/src/zenserver/frontend/html.zip
Binary files differ
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("|&lt;") .on_click(() => this._on_next_prev(-10e10));
+ left.add("&lt;&lt;").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("&gt;&gt;").on_click(() => this._on_next_prev( 10));
+ left.add("&gt;|") .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("&lt;&lt;").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("&gt;&gt;").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();