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/zenserver/frontend/html/pages | |
| 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/zenserver/frontend/html/pages')
| -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 |
8 files changed, 1103 insertions, 0 deletions
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); + } +} |