aboutsummaryrefslogtreecommitdiff
path: root/src/zenserver/frontend/html/util
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/zenserver/frontend/html/util
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/zenserver/frontend/html/util')
-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
3 files changed, 496 insertions, 0 deletions
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);
+ }
+}