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/util | |
| 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/util')
| -rw-r--r-- | src/zenserver/frontend/html/util/component.js | 158 | ||||
| -rw-r--r-- | src/zenserver/frontend/html/util/modal.js | 46 | ||||
| -rw-r--r-- | src/zenserver/frontend/html/util/widgets.js | 292 |
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); + } +} |