aboutsummaryrefslogtreecommitdiff
path: root/src/zenserver/frontend/html/zen.js
diff options
context:
space:
mode:
authorMartin Ridgers <[email protected]>2024-10-03 13:54:59 +0200
committerGitHub Enterprise <[email protected]>2024-10-03 13:54:59 +0200
commit61ec0fbdf3f87a41486e5d5dfde5d23e91941b42 (patch)
tree66775461556506cd39c84256efe3f119c29fecfc /src/zenserver/frontend/html/zen.js
parentcache get command (#183) (diff)
downloadzen-61ec0fbdf3f87a41486e5d5dfde5d23e91941b42.tar.xz
zen-61ec0fbdf3f87a41486e5d5dfde5d23e91941b42.zip
- Improvement: Self-hosted dashboard (#181)
Self-hosted dashboard gets oplog entry view and a stats browser
Diffstat (limited to 'src/zenserver/frontend/html/zen.js')
-rw-r--r--src/zenserver/frontend/html/zen.js840
1 files changed, 684 insertions, 156 deletions
diff --git a/src/zenserver/frontend/html/zen.js b/src/zenserver/frontend/html/zen.js
index 6bd74de0c..3fac5f312 100644
--- a/src/zenserver/frontend/html/zen.js
+++ b/src/zenserver/frontend/html/zen.js
@@ -1,10 +1,35 @@
+"use strict";
+
////////////////////////////////////////////////////////////////////////////////
-class Component
+class Friendly
+{
+ static sep(value, prec=0)
+ {
+ return value.toLocaleString("en", {
+ style: "decimal",
+ minimumFractionDigits : prec,
+ maximumFractionDigits : prec,
+ });
+ }
+
+ static k(x) { return Friendly.sep((x + 999) / Math.pow(10, 3), 0) + "K"; }
+ static m(x) { return Friendly.sep( x / Math.pow(10, 6), 1) + "M"; }
+ static g(x) { return Friendly.sep( x / Math.pow(10, 6), 2) + "G"; }
+ static kib(x) { return Friendly.sep((x + 1023) / (1 << 10), 0) + " KiB"; }
+ static mib(x) { return Friendly.sep( x / (1 << 20), 1) + " MiB"; }
+ static gib(x) { return Friendly.sep( x / (1 << 30), 2) + " GiB"; }
+}
+
+
+
+////////////////////////////////////////////////////////////////////////////////
+class ComponentBase
{
constructor(element)
{
- if (element instanceof Component)
+ if (element instanceof ComponentBase)
element = element._element;
+
this._element = element;
}
@@ -17,17 +42,33 @@ class Component
{
this._element.parentNode.removeChild(this._element);
}
+}
+////////////////////////////////////////////////////////////////////////////////
+class ComponentDom extends ComponentBase
+{
tag(tag="div")
{
var element = document.createElement(tag);
this._element.appendChild(element);
- return new Component(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 = String(value);
+ value = value.toString();
this._element.innerHTML = (value != "") ? value : "";
return this;
}
@@ -40,10 +81,7 @@ class Component
classify(value)
{
- var cur = this._element.className;
- cur += cur ? " " : "";
- cur += value;
- this._element.className = cur;
+ this._element.classList.add(value);
return this;
}
@@ -58,7 +96,11 @@ class Component
this._element.setAttribute(key, value);
return this;
}
+}
+////////////////////////////////////////////////////////////////////////////////
+class ComponentInteract extends ComponentDom
+{
link(resource=undefined, query_params={})
{
if (resource != undefined)
@@ -80,31 +122,71 @@ class Component
return this;
}
- on(what, func)
+ on(what, func, ...args)
{
const thunk = (src) => {
if (src.target != this._element)
return;
- func(src.target);
+ func(...args);
src.stopPropagation();
};
+
this._element.addEventListener(what, thunk);
return this;
}
- on_click(func)
+ on_click(func, ...args)
{
this.classify("zen_action");
- return this.on("click", func);
+ return this.on("click", func, ...args);
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+class Component extends ComponentInteract
+{
+ new_component(...args)
+ {
+ return new Component(...args);
}
}
////////////////////////////////////////////////////////////////////////////////
-class Cell extends Component
+class TableCell extends Component
{
+ constructor(element, row)
+ {
+ super(element);
+ this._row = row
+ }
+
+ get_table() { return this.get_row().get_table(); }
+ get_row() { return this._row; }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+class TableRow extends Component
+{
+ 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); }
}
////////////////////////////////////////////////////////////////////////////////
@@ -123,35 +205,63 @@ class Table extends Component
root.css_var("zen_columns", this._num_columns);
this._num_columns = column_names.length;
- this.add_row(column_names, false);
+ this._add_row(column_names, false);
+
+ this._rows = [];
+ }
+
+ *[Symbol.iterator]()
+ {
+ for (var row of this._rows)
+ yield row;
}
- add_row(cells, indexed=true)
+ get_row(index)
{
+ return this._rows.at(index);
+ }
+
+ _add_row(cells, indexed=true)
+ {
+ var index = -1;
if (indexed && this._index >= 0)
- cells = [this._index++, ...cells];
+ {
+ 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().classify("zen_row");
+ var row = this.tag()
+ row = new TableRow(row, this, index, ret);
for (const cell of cells)
{
- var leaf = row.tag().classify("zen_cell").text(cell);
- ret.push(new Cell(leaf));
+ var leaf = row.tag().text(cell);
+ ret.push(new TableCell(leaf, row));
}
- var bias = (this._index >= 0) ? 1 : 0;
- return ret.slice(bias);
+ 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 = index;
+ this._index = (this._index >= 0) ? index : -1;
+ this._rows = [];
}
}
@@ -163,14 +273,62 @@ class PropTable extends Table
constructor(parent)
{
super(parent, ["prop", "value"], -1);
+ this.classify("zen_proptable");
}
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;
+ 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");
+ }
}
}
@@ -205,11 +363,11 @@ class Modal
return this;
}
- option(name, func)
+ option(name, func, ...args)
{
- const thunk = (src) => {
+ const thunk = () => {
this._root.destroy();
- func(src);
+ func(...args);
};
this._buttons.tag().text(name).on("click", thunk);
return this;
@@ -268,35 +426,74 @@ class Sectormatron extends Component
////////////////////////////////////////////////////////////////////////////////
-async function zen_fetch(resource, method="GET")
+class Fetcher
{
- var response = await fetch(resource, {
- "method" : method,
- "headers" : { "Accept": "application/json" },
- });
+ constructor()
+ {
+ this._resource = "";
+ this._query = {};
+ }
- return await response.json();
-}
+ resource(...parts)
+ {
+ var value = parts.join("/");
+ if (!value.startsWith("/"))
+ value= "/" + value;
+ this._resource = value;
+ return this;
+ }
-////////////////////////////////////////////////////////////////////////////////
-function zen_flatten(object, ret=null, prefix="")
-{
- if (ret == null)
- ret = new Object();
+ param(name, value)
+ {
+ this._query[name] = value;
+ return this;
+ }
+
+ async json()
+ {
+ const response = await this._get("application/json");
+ return response ? (await response.json()) : {};
+ }
- for (const key in object)
+ async cbo()
{
- const value = object[key];
- if (value instanceof Object)
+ const response = await this._get("application/x-ue-cb");
+ if (!response)
+ return null;
+
+ const buffer = await response.arrayBuffer();
+ const data = new Uint8Array(buffer);
+ return new CbObject(data);
+ }
+
+ async delete()
+ {
+ const resource = this._build_uri();
+ const response = await fetch(resource, { "method" : "DELETE" });
+ }
+
+ _build_uri()
+ {
+ var suffix = "";
+ for (var key in this._query)
{
- zen_flatten(value, ret, key + ".");
- continue;
+ suffix += suffix ? "&" : "?";
+ suffix += key + "=" + this._query[key];
}
-
- ret[prefix + key] = value;
+ return this._resource + suffix;
}
- return ret;
+ async _get(accept="*")
+ {
+ const resource = this._build_uri();
+ const response = await fetch(resource, {
+ "method" : "GET",
+ "headers" : { "Accept": accept },
+ });
+
+ if (response.status >= 200 || response.status <= 299)
+ return response;
+ }
}
@@ -313,25 +510,102 @@ class Page
set_title(name)
{
- document.title = "zen - " + 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);
- return (ret != undefined) ? ret : fallback;
+ 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;
}
add_section(name)
{
return this._sectormatron.add_section(name);
}
+
+ reload()
+ {
+ window.location.reload();
+ }
+}
+
+
+
+////////////////////////////////////////////////////////////////////////////////
+class ZenPage extends Page
+{
+ constructor(...args)
+ {
+ super(...args);
+ super.set_title("zen");
+ this.generate_crumbs();
+ }
+
+ set_title(...args)
+ {
+ super.set_title(...args);
+ }
+
+ generate_crumbs()
+ {
+ const auto_name = this.constructor.name;
+ if (auto_name == "Start")
+ return;
+
+ const crumbs = this._parent.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());
+ }
}
////////////////////////////////////////////////////////////////////////////////
-class Entry extends Page
+class Entry extends ZenPage
{
async main()
{
@@ -339,36 +613,102 @@ class Entry extends Page
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}`;
+ const opkey = this.get_param("opkey");
+
+ var entry = new Fetcher()
+ .resource("prj", project, "oplog", oplog, "entries")
+ .param("opkey", opkey)
+ .cbo()
+ entry = await entry;
+ entry = entry.as_object().find("entry").as_object();
+
+ const name = entry.find("key").as_value();
+ var section = this.add_section(name);
- var entry = await zen_fetch(uri);
- var text = JSON.stringify(entry, null, 2);
- this._parent.tag("pre").text(text);
+ // tree
+ {
+ var tree = entry.find("$tree");
+ if (tree == undefined)
+ tree = this._convert_legacy_to_tree(entry);
+
+ delete tree["$id"];
+
+ const sub_section = section.add_section("deps");
+ for (const dep_name in tree)
+ {
+ const dep_section = sub_section.add_section(dep_name);
+ const table = new Table(dep_section, ["id", "name"]);
+ for (const dep_id of tree[dep_name])
+ {
+ const cell_values = [dep_id.toString(16)];
+ const row = table.add_row(cell_values);
+ row.get_cell(0).on_click(() => void(0));
+ }
+ }
+ }
+
+ // props
+ {
+ const object = entry.to_js_object();
+ var sub_section = section.add_section("props");
+ new PropTable(sub_section).add_object(object);
+ }
+ }
+
+ _convert_legacy_to_tree(entry)
+ {
+ const tree = {};
+
+ var id = 0n;
+ for (var item of entry.find("packagedata").as_array())
+ {
+ var pkg_id = item.as_object().find("id");
+ if (pkg_id == undefined)
+ continue;
+
+ for (var x of pkg_id.as_value().subarray(0, 8))
+ {
+ id <<= 8n;
+ id |= BigInt(x);
+ }
+ break;
+ }
+ tree["$id"] = id;
+
+ const pkgst_entry = entry.find("packagestoreentry").as_object();
+ const imported = pkgst_entry.find("importedpackageids");
+ if (imported == undefined)
+ return tree;
+
+ var out = tree["imported"] = [];
+ for (var item of imported.as_array())
+ out.push(item.as_value(BigInt));
+
+ return tree;
}
}
////////////////////////////////////////////////////////////////////////////////
-class Oplog extends Page
+class Oplog extends ZenPage
{
constructor(...args)
{
super(...args);
- this._index_start = this.get_param("start", 0);
- this._index_count = this.get_param("start", 50);
+ this._index_start = Number(this.get_param("start", 0));
+ this._index_count = Number(this.get_param("count", 50));
this._entry_table = undefined;
}
async main()
{
- this.set_title("oplog");
-
const project = this.get_param("project");
const oplog = this.get_param("oplog");
+ this.set_title("oplog - " + oplog);
+
var section = this.add_section(project + " - " + oplog);
this._build_nav(section)
@@ -380,17 +720,19 @@ class Oplog extends Page
_build_nav(section)
{
var nav = new Toolbar(section);
- nav.left().add("prev").on_click(() => this._on_next_prev(-1));
- nav.left().add("next").on_click(() => this._on_next_prev( 1));
+ nav.left().add("&lt;&lt;").on_click(() => this._on_next_prev(-10));
+ nav.left().add("prev") .on_click(() => this._on_next_prev( -1));
+ nav.left().add("next") .on_click(() => this._on_next_prev( 1));
+ nav.left().add("&gt;&gt;").on_click(() => this._on_next_prev( 10));
nav.left().sep();
for (var count of [10, 25, 50, 100])
{
- var handler = (e) => this._on_change_count(e.innerHTML);
- nav.left().add(count).on_click(handler);
+ var handler = (n) => this._on_change_count(n);
+ nav.left().add(count).on_click(handler, count);
}
- nav.right().add("search:", "span");
+ nav.right().add("search:", "label");
nav.right().add("", "input").attr("disabled", "");
}
@@ -399,25 +741,29 @@ class Oplog extends Page
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);
+ var entries = new Fetcher()
+ .resource("prj", project, "oplog", oplog, "entries")
+ .param("start", this.set_param("start", this._index_start))
+ .param("count", this.set_param("count", this._index_count))
+ .json();
this._entry_table.clear(this._index_start);
- for (const entry of (await entries)["entries"])
+ entries = (await entries)["entries"];
+ if (entries == undefined)
+ return
+
+ for (const entry of entries)
{
- var cells = this._entry_table.add_row([
+ var row = this._entry_table.add_row([
entry["key"]
]);
- cells[0].link("", {
+ row.get_cell(0).link("", {
"page" : "entry",
"project" : project,
"oplog" : oplog,
- "key" : entry["key"],
+ "opkey" : entry["key"],
});
}
}
@@ -439,19 +785,18 @@ class Oplog extends Page
////////////////////////////////////////////////////////////////////////////////
-class Project extends Page
+class Project extends ZenPage
{
async main()
{
- this.set_title("project");
-
// info
var section = this.add_section("info");
const project = this.get_param("project");
- const prefix = "/prj/" + project;
- var info = await zen_fetch(prefix);
+ this.set_title("project - " + project);
+
+ var info = await new Fetcher().resource("prj", project).json();
var prop_table = new PropTable(section);
for (const key in info)
{
@@ -471,73 +816,44 @@ class Project extends Page
{
const name = oplog["id"];
- var cells = oplog_table.add_row([
+ var row = oplog_table.add_row([
name,
]);
- var cell = cells[0];
+ var cell = row.get_cell(0);
cell.link("", {
"page" : "oplog",
"project" : project,
"oplog" : name,
});
- var action_tb = new Toolbar(cells.at(-1), true);
- action_tb.left().add("drop").attr("zen_param", name).on_click((e) => this._on_drop(e));
- }
-
- // files
- /*
- section = this.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;
- }
+ cell = row.get_cell(-1);
+ var action_tb = new Toolbar(cell, true);
+ action_tb.left().add("drop").on_click((x) => this.drop_oplog(x), name);
}
- */
}
- drop()
+ drop_oplog(oplog_id)
{
- alert("\\o/");
- }
+ const drop = async () => {
+ await new Fetcher()
+ .resource("prj", this.get_param("project"), "oplog", oplog_id)
+ .delete();
+ this.reload();
+ };
- _on_drop(e)
- {
new Modal()
.title("Confirmation")
- .message(`Drop oplog '${e.getAttribute("zen_param")}'?`)
- .option("Yes", () => this.drop())
- .option("No", () => void(0))
+ .message(`Drop oplog '${oplog_id}'?`)
+ .option("Yes", () => drop())
+ .option("No")
}
}
////////////////////////////////////////////////////////////////////////////////
-class Test extends Page
+class Test extends ZenPage
{
main()
{
@@ -613,27 +929,57 @@ class Test extends Page
side.add("tb_item2");
}
- var cell = prop_table.add_property("toolbar", "");
- toolbar = new Toolbar(cell[1], true);
- toolbar.left().add("tbitem0");
- toolbar.left().add("tbitem1");
- toolbar.right().add("tbitem2");
- toolbar.right().add("tbitem3");
+ 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);
+
+ this._cbo_test();
// error
throw Error("deliberate error");
}
+
+ async _cbo_test()
+ {
+ var data = new Uint8Array(await (await fetch("/prj/list")).arrayBuffer());
+ for (var item of new CbObject(data).as_array())
+ {
+ for (var subitem of item.as_object())
+ {
+ console.log(subitem.get_name(), subitem.as_value());
+ }
+ }
+
+ data = new Uint8Array(await (await fetch("/stats")).arrayBuffer());
+ {
+ var item = new CbObject(data).as_object().find("providers").as_array();
+ console.log(item.num());
+ for (var subitem of item)
+ {
+ console.log(subitem.as_value());
+ data = new Uint8Array(await (await fetch("/stats/" + subitem.as_value())).arrayBuffer());
+ for (var ssitem of new CbObject(data).as_object())
+ {
+ console.log(ssitem.get_name(), ssitem.as_value());
+ }
+ }
+ }
+ }
}
////////////////////////////////////////////////////////////////////////////////
-class Start extends Page
+class Start extends ZenPage
{
async main()
{
- this.set_title("main");
-
var section = this.add_section("projects");
// project list
@@ -645,43 +991,224 @@ class Start extends Page
];
var table = new Table(section, columns);
- for (const project of await zen_fetch("/prj/list"))
+ for (const project of await new Fetcher().resource("/prj/list").json())
{
- var cells = table.add_row([
- project.Id,
+ var row = table.add_row([
+ "",
project.ProjectRootDir,
project.EngineRootDir,
]);
- cells[0].link("", {"page" : "project", "project" : project.Id});
+ var cell = row.get_cell(0);
+ cell.tag().text(project.Id).on_click((x) => this.view_project(x), project.Id);
- var action_tb = new Toolbar(cells.at(-1), true);
- action_tb.left().add("view").on_click(() => void(0));
- action_tb.left().add("drop").on_click(() => this.on_drop());
+ 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");
- var providers = zen_fetch("/stats");
+ columns = [
+ "name",
+ "req count",
+ "size disk",
+ "size mem",
+ "cid total",
+ ];
+ const stats_table = new Table(section, columns);
+ var providers = new Fetcher().resource("stats").json();
for (var provider of (await providers)["providers"])
{
- var stats = zen_fetch("/stats/" + provider);
+ 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);
+ }
+ }
- var section_provider = section.add_section(provider);
- var table = new PropTable(section_provider);
+ view_stat(provider)
+ {
+ window.location = "?page=stat&provider=" + provider;
+ }
- stats = zen_flatten(await stats);
- for (const key in stats)
- table.add_property(key, stats[key]);
+ 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");
+ }
+}
+
+
+
+////////////////////////////////////////////////////////////////////////////////
+class Stat extends ZenPage
+{
+ static TemporalStat = class
+ {
+ constructor(data)
+ {
+ this._data = data;
}
+
+ toString()
+ {
+ const columns = [
+ [],
+ ["rate;"], [],
+ ["t;"], [], [],
+ ];
+ const data = this._data;
+ for (var key in data)
+ {
+ var value = key + ": " + Friendly.sep(data[key], 2);
+ value = value.replace("rate_", "");
+ value = value.replace("t_", "");
+
+ if (key.startsWith("rate_")) columns[2].push(value);
+ else if (key.startsWith("t_p")) columns[5].push(value);
+ else if (key.startsWith("t_")) columns[4].push(value);
+ else columns[0].push(Friendly.sep(data[key]));
+ }
+
+ var line_count = 0
+ for (var column of columns)
+ line_count = Math.max(line_count, column.length);
+
+ const widths = [13, 5, 19, 2, 23, -1];
+
+ var content = "";
+ for (var i = 0; i < line_count; ++i)
+ {
+ for (var j in columns)
+ {
+ const column = columns[j];
+ var cell = (column.length > i) ? column[i] : "";
+ var lead = cell.indexOf(":");
+ if (lead >= 0)
+ cell = " ".repeat(7 - lead) + cell;
+ if (widths[j] > 0)
+ cell = cell.padEnd(widths[j]);
+ content += cell;
+ }
+ content += "\n";
+ }
+
+ return "<pre>" + content + "</pre>";
+ }
+ }
+
+ 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 = new Toolbar(section);
+ 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);
+ {
+ var input = tb_right.add("", "input")
+ input.on("change", (x) => this.update_filter(x.inner().value), input);
+ }
+
+ this._table = new PropTable(section);
+
+ 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);
+ }
+
+ first = this.get_param("view", first);
+ this.view_category(first);
+
+ var filter = this.get_param("filter");
+ if (filter)
+ this.update_filter(filter);
}
- on_drop()
+ view_category(name)
{
- new Modal().title("Confirmation").message("TODO").option("Okay");
+ 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.set_param("filter", "");
+ }
+
+ update_filter(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)
+ {
+ node[name] = new Stat.TemporalStat(candidate);
+ continue;
+ }
+
+ impl(candidate);
+ }
+ }
+
+ for (var name in stats)
+ impl(stats[name]);
}
}
+
+
////////////////////////////////////////////////////////////////////////////////
function add_branding(parent)
{
@@ -709,11 +1236,12 @@ async function main_guarded()
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 == "test") impl = new Test(root, params);
- if (page == undefined) impl = new Start(root, params);
+ if (page == "project") impl = new Project(root, params);
+ else if (page == "stat") impl = new Stat(root, params);
+ else if (page == "oplog") impl = new Oplog(root, params);
+ else if (page == "entry") impl = new Entry(root, params);
+ else if (page == "test") impl = new Test(root, params);
+ else if (page == undefined) impl = new Start(root, params);
if (impl == undefined)
{