aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMartin Ridgers <[email protected]>2024-09-18 15:07:12 +0200
committerMartin Ridgers <[email protected]>2024-09-24 10:57:34 +0200
commita8d228b8af5135b01cc964e604d50111a8697471 (patch)
tree151d5a4300b4309c1c4f99b7a0db462837bfa4d5 /src
parentUpdated stale comment (diff)
downloadzen-a8d228b8af5135b01cc964e604d50111a8697471.tar.xz
zen-a8d228b8af5135b01cc964e604d50111a8697471.zip
Initial version of in-proc HTML dashboard
Diffstat (limited to 'src')
-rw-r--r--src/zenserver/frontend/html.zipbin2328 -> 82196 bytes
-rw-r--r--src/zenserver/frontend/html/favicon.icobin0 -> 65288 bytes
-rw-r--r--src/zenserver/frontend/html/index.html59
-rw-r--r--src/zenserver/frontend/html/zen.css187
-rw-r--r--src/zenserver/frontend/html/zen.js595
5 files changed, 787 insertions, 54 deletions
diff --git a/src/zenserver/frontend/html.zip b/src/zenserver/frontend/html.zip
index fa2f2febf..b2b5ccfe3 100644
--- a/src/zenserver/frontend/html.zip
+++ b/src/zenserver/frontend/html.zip
Binary files differ
diff --git a/src/zenserver/frontend/html/favicon.ico b/src/zenserver/frontend/html/favicon.ico
new file mode 100644
index 000000000..1cfa301a2
--- /dev/null
+++ b/src/zenserver/frontend/html/favicon.ico
Binary files differ
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 : "&nbsp;";
+ 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;
+ }
+}
+
+/*
+_________ _______ __
+\____ /___ ___ / ___// |__ ___ ______ ____
+ / __/ __ \ / \ \___ \\_ __// \\_ \/ __ \
+ / \ __// | \/ \| | ( - )| |\/\ __/
+/______/\___/\__|__/\______/|__| \___/ |__| \___|
+*/