diff options
| author | Martin Ridgers <[email protected]> | 2024-09-18 15:07:12 +0200 |
|---|---|---|
| committer | Martin Ridgers <[email protected]> | 2024-09-24 10:57:34 +0200 |
| commit | a8d228b8af5135b01cc964e604d50111a8697471 (patch) | |
| tree | 151d5a4300b4309c1c4f99b7a0db462837bfa4d5 /src | |
| parent | Updated stale comment (diff) | |
| download | zen-a8d228b8af5135b01cc964e604d50111a8697471.tar.xz zen-a8d228b8af5135b01cc964e604d50111a8697471.zip | |
Initial version of in-proc HTML dashboard
Diffstat (limited to 'src')
| -rw-r--r-- | src/zenserver/frontend/html.zip | bin | 2328 -> 82196 bytes | |||
| -rw-r--r-- | src/zenserver/frontend/html/favicon.ico | bin | 0 -> 65288 bytes | |||
| -rw-r--r-- | src/zenserver/frontend/html/index.html | 59 | ||||
| -rw-r--r-- | src/zenserver/frontend/html/zen.css | 187 | ||||
| -rw-r--r-- | src/zenserver/frontend/html/zen.js | 595 |
5 files changed, 787 insertions, 54 deletions
diff --git a/src/zenserver/frontend/html.zip b/src/zenserver/frontend/html.zip Binary files differindex fa2f2febf..b2b5ccfe3 100644 --- a/src/zenserver/frontend/html.zip +++ b/src/zenserver/frontend/html.zip diff --git a/src/zenserver/frontend/html/favicon.ico b/src/zenserver/frontend/html/favicon.ico Binary files differnew file mode 100644 index 000000000..1cfa301a2 --- /dev/null +++ b/src/zenserver/frontend/html/favicon.ico diff --git a/src/zenserver/frontend/html/index.html b/src/zenserver/frontend/html/index.html index 96b69a643..8bc690c22 100644 --- a/src/zenserver/frontend/html/index.html +++ b/src/zenserver/frontend/html/index.html @@ -1,60 +1,11 @@ +<!-- Copyright Epic Games, Inc. All Rights Reserved. --> <!DOCTYPE html> <html> <head> - <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" integrity="sha384-F3w7mX95PdgyTmZZMECAngseQB83DfGTowi0iMjiWaeVhAn4FJkqJByhZMI3AhiU" crossorigin="anonymous"> - <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.min.js" integrity="sha384-skAcpIdS7UcVUC05LJ9Dxay8AXcDYfBJqt1CJ85S/CFujBsIzCIv+l9liuYLaMQ/" crossorigin="anonymous"></script> - <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.css"> - <style type="text/css"> - body { - background-color: #fafafa; - } - </style> - <script type="text/javascript"> - const getCacheStats = () => { - const opts = { headers: { "Accept": "application/json" } }; - const queryString = window.location.search; - fetch("/stats/z$" + queryString, opts) - .then(response => { - if (!response.ok) { - throw Error(response.statusText); - } - return response.json(); - }) - .then(json => { - document.getElementById("status").innerHTML = "connected" - document.getElementById("stats").innerHTML = JSON.stringify(json, null, 4); - }) - .catch(error => { - document.getElementById("status").innerHTML = "disconnected" - document.getElementById("stats").innerHTML = "" - console.log(error); - }) - .finally(() => { - window.setTimeout(getCacheStats, 1000); - }); - }; - getCacheStats(); - </script> + <link rel="shortcut icon" href="favicon.ico"> + <link rel="stylesheet" type="text/css" href="zen.css" /> + <script src="zen.js"></script> </head> -<body> - <div class="container"> - <div class="row"> - <div class="text-center mt-5"> - <pre> -__________ _________ __ -\____ / ____ ____ / _____/_/ |_ ____ _______ ____ - / / _/ __ \ / \ \_____ \ \ __\ / _ \ \_ __ \_/ __ \ - / /_ \ ___/ | | \ / \ | | ( <_> ) | | \/\ ___/ -/_______ \ \___ >|___| //_______ / |__| \____/ |__| \___ > - \/ \/ \/ \/ \/ - </pre> - <pre id="status"/> - </div> - </div> - <div class="row"> - <pre class="mb-0">Z$:</pre> - <pre id="stats"></pre> - </div> - </div> +<body onload="main()"> </body> </html> diff --git a/src/zenserver/frontend/html/zen.css b/src/zenserver/frontend/html/zen.css new file mode 100644 index 000000000..e764d02ce --- /dev/null +++ b/src/zenserver/frontend/html/zen.css @@ -0,0 +1,187 @@ +/* page --------------------------------------------------------------------- */ + +body, input { + font-family: consolas, monospace; + font-size: 11pt; +} + +body { + overflow-y: scroll; + margin: 0; +} + +* { + box-sizing: border-box; +} + +#container { + margin: 1.0em 2.2em 1.5em 2.2em; +} + +/* links -------------------------------------------------------------------- */ + +.zen_action, a { + all: unset; + cursor: pointer; + color: #069; +} + +.zen_action:hover, a:hover { + text-decoration: underline #c88; +} + +/* sector ------------------------------------------------------------------- */ + +h1 { + font-size: 1.5em; + width: 100%; + border-bottom: 1px solid #ccc; +} + +h2 { + font-size: 1.25em; + margin-bottom: 0.5em; +} + +h3 { + font-size: 1.1em; + margin: 0em; + padding: 0.4em; + background-color: #eef; + border-left: 5px solid #cce; + font-weight: normal; +} + +.zen_sector { + margin-bottom: 3em; +} + +.zen_sector > *:not(h1) { + margin-left: 2em; +} + +/* table -------------------------------------------------------------------- */ + +.zen_table { + display: grid; + grid-template-columns: max-content repeat(calc(var(--zen_columns) - 1), 1fr); + border: 1px solid #aaa; + border-left-style: none; +} + +.zen_table > .zen_row { + display: contents; +} + +.zen_table > .zen_row:nth-child(odd) > .zen_cell { + background-color: #f4f4f4; +} + +.zen_table > .zen_row:first-child > .zen_cell { + font-weight: bold; + background-color: #dde; +} + +.zen_table > .zen_row > .zen_cell { + padding: 0.3em; + padding-left: 0.75em; + padding-right: 0.75em; + border-left: 1px solid #aaa; +} + +/* modal -------------------------------------------------------------------- */ + +.zen_modal { + position: fixed; + z-index: 1; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: #0018; + display: flex; + justify-content: center; + align-items: center; +} + +.zen_modal > div { + border-radius: 0.5em; + background-color: white; + width: 35em; + padding: 0em 2em 2em 2em; +} + +.zen_modal > div > div { + text-align: center; +} + +.zen_modal .zen_modal_title { + font-size: 1.2em; + border-bottom: 1px solid #ccc; + padding: 1.2em 0em 0.5em 0em; + color: #444; +} + +.zen_modal .zen_modal_buttons { + display: flex; + justify-content: center; + padding-bottom: 0em; +} + +.zen_modal .zen_modal_buttons > div { + margin: 0em 1em 0em 1em; + padding: 1em; + border-radius: 0.3em; + background-color: #dde; + width: 6em; + cursor: pointer; +} + +.zen_modal .zen_modal_buttons > div:hover { + background-color: #eef; +} + +.zen_modal .zen_modal_message { + padding: 2em; + min-height: 8em; + align-content: center; +} + +/* branding ----------------------------------------------------------------- */ + +#branding { + font-size: 10pt; + font-weight: bolder; + margin-bottom: 2.6em; +} + +#logo { + width: min-content; + margin: auto; +} + +#ue_logo { + position: absolute; + top: 2em; + right: 2em; + width: 5em; + margin: auto; +} + +/* error -------------------------------------------------------------------- */ + +#error { + position: fixed; + bottom: 0; + z-index: 1; + color: #000; + background-color: #fcc; + padding: 1.0em 2em 2em 2em; + width: 100%; + border-top: 1px solid black; +} + +#error > pre:nth-child(2) { + font-size: 0.8em; + color: #555; +}; diff --git a/src/zenserver/frontend/html/zen.js b/src/zenserver/frontend/html/zen.js new file mode 100644 index 000000000..a338739be --- /dev/null +++ b/src/zenserver/frontend/html/zen.js @@ -0,0 +1,595 @@ +//////////////////////////////////////////////////////////////////////////////// +class Component +{ + constructor(element) + { + if (element instanceof Component) + element = element._element; + this._element = element; + } + + destroy() + { + this._element.parentNode.removeChild(this._element); + } + + tag(tag="div") + { + var element = document.createElement(tag); + this._element.appendChild(element); + return new Component(element); + } + + text(value) + { + value = String(value); + this._element.innerHTML = (value != "") ? value : " "; + return this; + } + + id(value) + { + this._element.id = value; + return this; + } + + classify(value) + { + var cur = this._element.className; + cur += cur ? " " : ""; + cur += value; + this._element.className = cur; + return this; + } + + css_var(key, value) + { + this._element.style.setProperty("--" + key, value); + return this; + } + + attr(key, value) + { + this._element.setAttribute(key, value); + return this; + } + + 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) + { + const thunk = (src) => { + if (src.target != this._element) + return; + + func(src.target); + src.stopPropagation(); + }; + this._element.addEventListener(what, thunk); + return this; + } +} + + + +//////////////////////////////////////////////////////////////////////////////// +class Cell extends Component +{ +} + +//////////////////////////////////////////////////////////////////////////////// +class Table extends Component +{ + constructor(parent, column_names, index_base=0) + { + var root = parent.tag().classify("zen_table"); + super(root); + + this._index = index_base; + if (index_base >= 0) + column_names = ["#", ...column_names]; + + this._num_columns = column_names.length; + root.css_var("zen_columns", this._num_columns); + + this._num_columns = column_names.length; + this.add_row(column_names, false); + } + + add_row(cells, indexed=true) + { + if (indexed && this._index >= 0) + cells = [this._index++, ...cells]; + + cells = cells.slice(0, this._num_columns); + while (cells.length < this._num_columns) + cells.push(""); + + var ret = []; + var row = this.tag().classify("zen_row"); + for (const cell of cells) + { + var leaf = row.tag().classify("zen_cell").text(cell); + ret.push(new Cell(leaf)); + } + + var bias = (this._index >= 0) ? 1 : 0; + return ret.slice(bias); + } + + clear(index=0) + { + const elem = this._element; + elem.replaceChildren(elem.firstElementChild); + this._index = index; + } +} + + + +//////////////////////////////////////////////////////////////////////////////// +class PropTable extends Table +{ + constructor(parent) + { + super(parent, ["prop", "value"], -1); + } + + add_property(key, value) + { + var ret = this.add_row([key, value]); + ret[0].classify("zen_prop_key"); + ret[1].classify("zen_prop_value"); + return ret; + } +} + + + +//////////////////////////////////////////////////////////////////////////////// +class Modal +{ + constructor() + { + const body = new Component(document.body); + this._root = body.tag().classify("zen_modal"); + this._root.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) + { + const thunk = (src) => { + this._root.destroy(); + func(src); + }; + this._buttons.tag().text(name).on("click", thunk); + return this; + } +} + + + +//////////////////////////////////////////////////////////////////////////////// +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); + } +} + + + +//////////////////////////////////////////////////////////////////////////////// +async function zen_fetch(resource, method="GET") +{ + var response = await fetch(resource, { + "method" : method, + "headers" : { "Accept": "application/json" }, + }); + + return await response.json(); +} + +//////////////////////////////////////////////////////////////////////////////// +function zen_title(name) +{ + document.title = "zen - " + name; +} + +//////////////////////////////////////////////////////////////////////////////// +function zen_flatten(object, ret=null, prefix="") +{ + if (ret == null) + ret = new Object(); + + for (const key in object) + { + const value = object[key]; + if (value instanceof Object) + { + zen_flatten(value, ret, key + "."); + continue; + } + + ret[prefix + key] = value; + } + + return ret; +} + + + +//////////////////////////////////////////////////////////////////////////////// +class Page +{ + constructor(parent, params) + { + this._parent = parent; + this._params = params; + } + + get_param(name, fallback=undefined) + { + var ret = this._params.get(name); + return (ret != undefined) ? ret : fallback; + } +} + + + +//////////////////////////////////////////////////////////////////////////////// +class Entry extends Page +{ + async main() + { + zen_title("oplog entry"); + + const project = this.get_param("project"); + const oplog = this.get_param("oplog"); + const key = this.get_param("key"); + const uri = `/prj/${project}/oplog/${oplog}/entries?opkey=${key}`; + + var entry = await zen_fetch(uri); + var text = JSON.stringify(entry, null, 2); + this._parent.tag("pre").text(text); + } +} + + + +//////////////////////////////////////////////////////////////////////////////// +class Oplog extends Page +{ + constructor(...args) + { + super(...args); + + this._index_start = this.get_param("start", 0); + this._index_count = this.get_param("start", 50); + this._entry_table = undefined; + } + + async main() + { + zen_title("oplog"); + + const project = this.get_param("project"); + const oplog = this.get_param("oplog"); + + var builder = new Sectormatron(this._parent); + var section = builder.add_section(project + " - " + oplog); + + this._entry_table = new Table(section, ["key"]); + await this._build_table(); + } + + async _build_table() + { + const project = this.get_param("project"); + const oplog = this.get_param("oplog"); + + var uri = `/prj/${project}/oplog/${oplog}/entries`; + uri += "?start=" + this._index_start; + uri += "&count=" + this._index_count; + + var entries = zen_fetch(uri); + + this._entry_table.clear(this._index_start); + + var count = 12; + for (const entry of (await entries)["entries"]) + { + var cells = this._entry_table.add_row([ + entry["key"] + ]); + + cells[0].link("", { + "page" : "entry", + "project" : project, + "oplog" : oplog, + "key" : entry["key"], + }); + + --count; + if (count == 0) + break; + } + } + + _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(); + } +} + + + +//////////////////////////////////////////////////////////////////////////////// +class Project extends Page +{ + async main() + { + zen_title("project"); + + var builder = new Sectormatron(this._parent); + + // info + var section = builder.add_section("info"); + + const project = this.get_param("project"); + const prefix = "/prj/" + project; + + var info = await zen_fetch(prefix); + var prop_table = new PropTable(section); + for (const key in info) + { + if (key == "oplogs") + continue; + + prop_table.add_property(key, info[key]); + } + + // oplog + section = builder.add_section("oplogs"); + + var oplog_table = new Table(section, ["name", "actions"]) + + var count = 0; + for (const oplog of info["oplogs"]) + { + const name = oplog["id"]; + + var cells = oplog_table.add_row([ + name, + "drop", + ]); + + var cell = cells[0]; + cell.link("", { + "page" : "oplog", + "project" : project, + "oplog" : name, + }); + + cell = cells.at(-1); + cell.attr("zen_param", name).on("click", (e) => this._on_drop(e)); + } + + // files + /* + section = builder.add_section("files"); + for (const oplog of info["oplogs"]) + { + const name = oplog["id"]; + + var files = await zen_fetch(prefix + "/oplog/" + name + "/files"); + if (files["files"].length == 0) + continue; + + var sub_section = section.add_section(name); + var table = new Table(sub_section, [ + "id", + "clientpath", + "serverpath", + ]); + var count = 0; + for (const file of files["files"]) + { + table.add_row([ + file["id"], + file["clientpath"], + file["serverpath"], + ]); + + if (++count > 10) + break; + } + } + */ + } + + drop() + { + alert("\\o/"); + } + + _on_drop(e) + { + new Modal() + .title("Confirmation") + .message(`Drop oplog '${e.getAttribute("zen_param")}'?`) + .option("Yes", () => this.drop()) + .option("No", () => void(0)) + } +} + + + +//////////////////////////////////////////////////////////////////////////////// +class Start extends Page +{ + async main() + { + zen_title("main"); + + var builder = new Sectormatron(this._parent); + var section = builder.add_section("projects"); + + // project list + var columns = [ + "name", + "project_dir", + "engine_dir", + "actions", + ]; + var table = new Table(section, columns); + + for (const project of await zen_fetch("/prj/list")) + { + var cells = table.add_row([ + project.Id, + project.ProjectRootDir, + project.EngineRootDir, + "drop", + ]); + + cells[0].link("", {"page" : "project", "project" : project.Id}); + } + + // stats + section = builder.add_section("stats"); + var providers = zen_fetch("/stats"); + for (var provider of (await providers)["providers"]) + { + var stats = zen_fetch("/stats/" + provider); + + var section_provider = section.add_section(provider); + var table = new PropTable(section_provider); + + stats = zen_flatten(await stats); + for (const key in stats) + table.add_property(key, stats[key]); + } + } +} + +//////////////////////////////////////////////////////////////////////////////// +function add_branding(parent) +{ + var root = parent.tag().id("branding"); + + root.tag("pre").id("logo").text( + "_________ _______ __\n" + + "\\____ /___ ___ / ___// |__ ___ ______ ____\n" + + " / __/ __ \\ / \\ \\___ \\\\_ __// \\\\_ \\/ __ \\\n" + + " / \\ __// | \\/ \\| | ( - )| |\\/\\ __/\n" + + "/______/\\___/\\__|__/\\______/|__| \\___/ |__| \\___|" + ); + + root.tag("img").attr("src", "favicon.ico").id("ue_logo"); +} + +//////////////////////////////////////////////////////////////////////////////// +async function main_guarded() +{ + const root = new Component(document.body).tag().id("container"); + + add_branding(root); + + const params = new URLSearchParams(window.location.search); + const page = params.get("page"); + var impl = undefined; + + if (page == "project") impl = new Project(root, params); + if (page == "oplog") impl = new Oplog(root, params); + if (page == "entry") impl = new Entry(root, params); + if (page == undefined) impl = new Start(root, params); + + if (impl == undefined) + { + root.tag().text("unknown page"); + return; + } + + return impl.main(); +} + +//////////////////////////////////////////////////////////////////////////////// +async function main() +{ + try + { + return await main_guarded(); + } + catch (e) + { + var pane = new Component(document.body).tag().id("error"); + pane.tag("pre").text(e); + pane.tag("pre").text(e.stack); + throw e; + } +} + +/* +_________ _______ __ +\____ /___ ___ / ___// |__ ___ ______ ____ + / __/ __ \ / \ \___ \\_ __// \\_ \/ __ \ + / \ __// | \/ \| | ( - )| |\/\ __/ +/______/\___/\__|__/\______/|__| \___/ |__| \___| +*/ |