aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMartin Ridgers <[email protected]>2024-11-11 10:31:34 +0100
committerGitHub Enterprise <[email protected]>2024-11-11 10:31:34 +0100
commit05d1044045539557dfe4e9c8996737d83f9dee89 (patch)
tree00907e9a5306318e8a9d169348348b7a5cc1f32d /src
parentUpdate VERSION.txt (diff)
downloadzen-05d1044045539557dfe4e9c8996737d83f9dee89.tar.xz
zen-05d1044045539557dfe4e9c8996737d83f9dee89.zip
Self-hosted dashboard: Searchable oplog and links between oplog entry dependencies (#213)v5.5.12-pre0
* Consistent use of semicolons * Added fallback if oplog entry assumptions do not hold * 'marker' and 'expired' cells were incorrectly friendly * Two spaces when there should only be one * Robustness against .text(undefined) calls * A single step into JavaScript modules * Turned Fetcher into a module * Friendly into a module * Specialise Cbo field name comparison as TextDecoder() is very slow * Prefer is_named() over get_name() * Incorrect logic checking if a server reply was okay * Try and make sure it's always numbers that flow through Friendly * Added a progress bar component * Swap key and package hash columns * CbObject cloning * Dark and light themes depending on browser settings * Adjust styling of input boxes * Add theme swatches to test page * Turns out one can nest CSS selectors * Separate swatch for links/actions * Generate theme by lerping intermediate colours * Clearer progress bar * Chromium was complaining about label-less input elements * Promise-based cache using an IndexedDb * WebWorker for generating map of package ids to names * Indexer class for building, loading, and saving map of ids to names * Added links to oplog entries of an entry's dependencies * This doesn't need to be decorated as async any longer * Implemented oplog searching * View and drop make no sense on package data payloads * Rudimentary search result truncation * Updated changelog * Updated HTML zip archive
Diffstat (limited to 'src')
-rw-r--r--src/zenserver/frontend/html.zipbin119799 -> 136697 bytes
-rw-r--r--src/zenserver/frontend/html/index.html5
-rw-r--r--src/zenserver/frontend/html/indexer/cache.js65
-rw-r--r--src/zenserver/frontend/html/indexer/indexer.js193
-rw-r--r--src/zenserver/frontend/html/indexer/worker.js131
-rw-r--r--src/zenserver/frontend/html/util/compactbinary.js (renamed from src/zenserver/frontend/html/compactbinary.js)32
-rw-r--r--src/zenserver/frontend/html/util/fetcher.js76
-rw-r--r--src/zenserver/frontend/html/util/friendly.js23
-rw-r--r--src/zenserver/frontend/html/zen.css542
-rw-r--r--src/zenserver/frontend/html/zen.js305
10 files changed, 1000 insertions, 372 deletions
diff --git a/src/zenserver/frontend/html.zip b/src/zenserver/frontend/html.zip
index cda479fc8..1be5f0d9a 100644
--- a/src/zenserver/frontend/html.zip
+++ b/src/zenserver/frontend/html.zip
Binary files differ
diff --git a/src/zenserver/frontend/html/index.html b/src/zenserver/frontend/html/index.html
index fad4bf902..6a736e914 100644
--- a/src/zenserver/frontend/html/index.html
+++ b/src/zenserver/frontend/html/index.html
@@ -10,9 +10,6 @@
</script>
<link rel="shortcut icon" href="favicon.ico">
<link rel="stylesheet" type="text/css" href="zen.css" />
- <script src="compactbinary.js"></script>
- <script src="zen.js"></script>
+ <script type="module" src="zen.js"></script>
</head>
-<body onload="main()">
-</body>
</html>
diff --git a/src/zenserver/frontend/html/indexer/cache.js b/src/zenserver/frontend/html/indexer/cache.js
new file mode 100644
index 000000000..390aa948d
--- /dev/null
+++ b/src/zenserver/frontend/html/indexer/cache.js
@@ -0,0 +1,65 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+"use strict";
+
+////////////////////////////////////////////////////////////////////////////////
+export class Cache
+{
+ constructor(db_name, ...store_names)
+ {
+ this._db_name = db_name;
+ this._store_names = store_names;
+ this._version = 1;
+ this._db = this._open();
+ }
+
+ put(store_name, key, value)
+ {
+ const executor = async (resolve, reject) => {
+ const db = await this._db;
+ const transaction = db.transaction(store_name, "readwrite");
+ const store = transaction.objectStore(store_name);
+ const request = store.put(value, key);
+ request.onerror = (evt) => reject(Error("put transaction error"));
+ request.onsuccess = (evt) => resolve(true);
+ };
+ return new Promise(executor);
+ }
+
+ get(store_name, key)
+ {
+ const executor = async (resolve, reject) => {
+ const db = await this._db;
+ const transaction = db.transaction(store_name, "readonly");
+ const store = transaction.objectStore(store_name);
+ const request = store.get(key);
+ request.onerror = (evt) => reject(Error("get transaction error"));
+ request.onsuccess = (evt) => {
+ if (request.result)
+ resolve(request.result);
+ else
+ resolve(false);
+ };
+ };
+ return new Promise(executor);
+ }
+
+ _open()
+ {
+ const executor = (resolve, reject) => {
+ const request = indexedDB.open(this._db_name, this._version);
+ request.onerror = (evt) => reject(Error("Failed to open IndexedDb"));
+ request.onsuccess = (evt) => resolve(evt.target.result);
+ request.onupgradeneeded = (evt) => {
+ const db = evt.target.result;
+
+ for (const store_name of db.objectStoreNames)
+ db.deleteObjectStore(store_name)
+
+ for (const store_name of this._store_names)
+ db.createObjectStore(store_name);
+ };
+ };
+ return new Promise(executor);
+ }
+}
diff --git a/src/zenserver/frontend/html/indexer/indexer.js b/src/zenserver/frontend/html/indexer/indexer.js
new file mode 100644
index 000000000..8e5003edf
--- /dev/null
+++ b/src/zenserver/frontend/html/indexer/indexer.js
@@ -0,0 +1,193 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+"use strict";
+
+import { Cache } from "./cache.js"
+import { Message } from "./worker.js"
+import { Fetcher } from "../util/fetcher.js"
+
+////////////////////////////////////////////////////////////////////////////////
+class Indexer
+{
+ constructor(pages)
+ {
+ this._pages = pages;
+ }
+
+ lookup_id(entry_id)
+ {
+ const bin_search = function(page) {
+ var l = 0;
+ var r = page.length;
+ while (l < r)
+ {
+ const mid = l + ((r - l) >> 1);
+ const d = entry_id - page[mid][0];
+ if (d < 0n) r = mid;
+ else if (d > 0n) l = mid + 1;
+ else return mid;
+ }
+
+ return -1;
+ };
+
+ for (const page of this._pages)
+ {
+ const index = bin_search(page);
+ if (index >= 0)
+ return page[index][1];
+ }
+
+ return "";
+ }
+
+ *search(needle)
+ {
+ for (const page of this._pages)
+ for (const [_, name] of page)
+ if (name.indexOf(needle) >= 0)
+ yield name;
+ }
+}
+
+
+
+////////////////////////////////////////////////////////////////////////////////
+async function save(progress_cb, oplog_info, pages)
+{
+ const project_id = oplog_info["project"];
+ const cache = new Cache(project_id, "pages");
+
+ const page_count = pages.length;
+ const puts = new Array(page_count);
+ for (var i = 0; i < page_count; ++i)
+ puts[i] = cache.put("pages", i, pages[i]);
+
+ var okay = true
+ for (var i = 0; i < page_count; ++i)
+ {
+ okay &= await puts[i];
+ progress_cb("saving", i + 1, page_count);
+ }
+ if (!okay)
+ return false;
+
+ cache.put("pages", "$", {
+ "page_count" : pages.length,
+ "total_size" : oplog_info["totalsize"],
+ "op_count" : oplog_info["opcount"],
+ "timestamp" : (Date.now() / 1000) | 0,
+ });
+
+ return true
+}
+
+////////////////////////////////////////////////////////////////////////////////
+async function build(progress_cb, oplog_info)
+{
+ const project_id = oplog_info["project"];
+ const oplog = oplog_info["id"];
+ const init_msg = Message.create(Message.Init, project_id, oplog);
+
+ const worker_n = Math.min(navigator.hardwareConcurrency / 2, 6);
+ const page_size = 48 << 10;
+ const stride = page_size * worker_n;
+ const end = oplog_info["opcount"];
+ var entry_count = 0;
+
+ const pages = new Array();
+
+ const executor = function(index, resolve, reject) {
+ const worker = new Worker("indexer/worker.js", { type: "module" });
+ worker.onerror = (evt) => reject(Error("Worker error"));
+ worker.onmessage = (evt) => {
+ const [msg_id, ...params] = evt.data;
+ switch (msg_id)
+ {
+ case Message.MapDone:
+ resolve();
+ worker.terminate();
+ break;
+
+ case Message.MapPage:
+ const [page] = params;
+ pages.push(page);
+ entry_count += page.length;
+ progress_cb("parsing", entry_count, end);
+ break;
+ }
+ }
+ worker.postMessage(init_msg);
+
+ const start = page_size * index;
+ const map_msg = Message.create(Message.Map, start, end, page_size, stride);
+ worker.postMessage(map_msg);
+ };
+
+ const workers = []
+ for (var i = 0; i < worker_n; ++i)
+ {
+ const worker = new Promise((...args) => executor(i, ...args));
+ workers.push(worker);
+ }
+
+ for (const worker of workers)
+ await worker;
+
+ return pages;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+async function load(progress_cb, oplog_info)
+{
+ const project_id = oplog_info["project"];
+ const cache = new Cache(project_id, "pages");
+ const meta = await cache.get("pages", "$");
+
+ var hit = false;
+ if (meta)
+ {
+ const yesterday = (Date.now() / 1000) - (24 * 60 * 60);
+ hit = true;
+ hit &= (meta["total_size"] == oplog_info["totalsize"]);
+ hit &= (meta["op_count"] == oplog_info["opcount"]);
+ hit &= (meta["timestamp"] >= yesterday);
+ }
+ if (!hit)
+ return null;
+
+ const page_count = meta["page_count"];
+ const gets = new Array(page_count);
+ const pages = new Array(page_count);
+ for (var i = 0; i < page_count; ++i)
+ gets[i] = cache.get("pages", i);
+
+ progress_cb("loading", 0, page_count);
+ for (var i = 0; i < page_count; ++i)
+ {
+ pages[i] = await gets[i];
+ progress_cb("loading", i + 1, page_count);
+ }
+
+ return pages;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+export async function create_indexer(project_id, oplog, progress_cb)
+{
+ if (!window.Worker)
+ throw Error("browser does not support web workers");
+
+ const oplog_info = await new Fetcher()
+ .resource("prj", project_id, "oplog", oplog)
+ .json();
+
+ var pages = await load(progress_cb, oplog_info);
+ if (!pages)
+ {
+ pages = await build(progress_cb, oplog_info);
+ await save(progress_cb, oplog_info, pages);
+ }
+
+ return new Indexer(pages);
+}
diff --git a/src/zenserver/frontend/html/indexer/worker.js b/src/zenserver/frontend/html/indexer/worker.js
new file mode 100644
index 000000000..581746d6c
--- /dev/null
+++ b/src/zenserver/frontend/html/indexer/worker.js
@@ -0,0 +1,131 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+"use strict";
+
+import { Fetcher } from "../util/fetcher.js"
+
+////////////////////////////////////////////////////////////////////////////////
+export class Message
+{
+ static None = 0; //
+ static Init = 1; // project_id, oplog
+ static Map = 2; // start, end, page_size, stride
+ static MapPage = 3; // page
+ static MapDone = 4; //
+
+ static create(msg, ...args) { return [msg, ...args]; }
+}
+
+
+
+////////////////////////////////////////////////////////////////////////////////
+async function map_id_to_key(project_id, oplog, start, end, page_size, stride)
+{
+ const uri = "/prj/" + project_id + "/oplog/" + oplog + "/entries";
+ while (start < end)
+ {
+ performance.mark("fetch");
+ const cbo = new Fetcher()
+ .resource(uri)
+ .param("start", start)
+ .param("count", page_size)
+ .param("fieldfilter", "packagedata,key")
+ .cbo()
+
+ const entry_count = Math.min(page_size, -(start - end));
+ var result = new Array(entry_count);
+
+ var entries = (await cbo).as_object().find("entries");
+ if (entries == undefined)
+ break;
+
+ entries = entries.as_array();
+ if (entries.num() == 0)
+ break;
+
+ performance.mark("build");
+ var count = 0;
+ for (var entry of entries)
+ {
+ if (!entry.is_object())
+ continue
+ entry = entry.as_object();
+
+ var key = undefined;
+ var pkg_data = undefined;
+ for (const field of entry)
+ {
+ if (field.is_named("key")) key = field;
+ else if (field.is_named("packagedata")) pkg_data = field;
+ }
+ if (key == undefined || pkg_data == undefined)
+ continue;
+
+ var id = 0n;
+ for (var item of pkg_data.as_array())
+ {
+ var pkg_id = item.as_object().find("id");
+ if (pkg_id == undefined)
+ continue;
+
+ pkg_id = pkg_id.as_value().subarray(0, 8);
+ for (var i = 7; i >= 0; --i)
+ {
+ id <<= 8n;
+ id |= BigInt(pkg_id[i]);
+ }
+ break;
+ }
+
+ if (id == 0)
+ continue;
+
+ result[count] = [id, key.as_value()];
+ count++;
+ }
+
+ start += stride;
+
+ if (count == 0)
+ continue;
+
+ if (count != result.length)
+ result = result.slice(0, count);
+
+ performance.mark("sort");
+ result.sort(function(l, r) { return Number(l[0] - r[0]); });
+
+ const msg = Message.create(Message.MapPage, result);
+ postMessage(msg);
+ }
+
+ postMessage(Message.create(Message.MapDone));
+}
+
+////////////////////////////////////////////////////////////////////////////////
+function worker_scope()
+{
+ var project_id;
+ var oplog;
+
+ return (evt) => {
+ const [msg_id, ...params] = evt.data;
+ switch (msg_id)
+ {
+ case Message.Init:
+ [project_id, oplog] = params;
+ break;
+
+ case Message.Map:
+ var [start, end, page_size, stride] = params;
+ map_id_to_key(project_id, oplog, start, end, page_size, stride);
+ break;
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+if (typeof DedicatedWorkerGlobalScope != "undefined" && self instanceof DedicatedWorkerGlobalScope)
+{
+ onmessage = worker_scope();
+}
diff --git a/src/zenserver/frontend/html/compactbinary.js b/src/zenserver/frontend/html/util/compactbinary.js
index 1247e556d..366ec6aff 100644
--- a/src/zenserver/frontend/html/compactbinary.js
+++ b/src/zenserver/frontend/html/util/compactbinary.js
@@ -130,7 +130,7 @@ class CbFieldView
constructor()
{
this._type = CbFieldType.None;
- this._name = "";
+ this._name = undefined;
this._data_view = undefined;
}
}
@@ -156,7 +156,7 @@ CbFieldView.prototype._from_data = function(data_view, type=CbFieldType.HasField
if (CbFieldTypeOps.has_field_name(type))
{
const [n, varint_len] = VarInt.read_uint(data_view);
- this._name = new TextDecoder().decode(data_view.subarray(varint_len, n + varint_len));
+ this._name = data_view.subarray(varint_len, n + varint_len);
data_view = data_view.subarray(n + varint_len);
}
@@ -188,7 +188,19 @@ CbFieldView.prototype.get_type = function()
////////////////////////////////////////////////////////////////////////////////
CbFieldView.prototype.get_name = function()
{
- return this._name;
+ return new TextDecoder().decode(this._name);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+CbFieldView.prototype.is_named = function(rhs)
+{
+ if (!this._name) return false;
+ if (rhs.length != this._name.length) return false;
+ for (var i = 0; i < rhs.length; ++i)
+ if (rhs.charCodeAt(i) != this._name[i])
+ return false;
+
+ return true;
}
////////////////////////////////////////////////////////////////////////////////
@@ -317,6 +329,16 @@ CbFieldView.prototype.as_value = function(int_type=Number)
cb_assert(false);
}
+////////////////////////////////////////////////////////////////////////////////
+CbFieldView.prototype.clone = function()
+{
+ const ret = new CbFieldView()
+ ret._type = this._type;
+ ret._name = ret._name;
+ ret._data_view = new Uint8Array(this._data_view);
+ return ret;
+}
+
////////////////////////////////////////////////////////////////////////////////
@@ -386,7 +408,7 @@ CbObjectView.prototype.to_js_object = function()
CbObjectView.prototype.find = function(name)
{
for (const field of this)
- if (field.get_name() == name)
+ if (field.is_named(name))
return field;
}
@@ -432,7 +454,7 @@ CbArrayView.prototype.num = function()
////////////////////////////////////////////////////////////////////////////////
-class CbObject extends CbFieldView
+export class CbObject extends CbFieldView
{
constructor(uint8_array)
{
diff --git a/src/zenserver/frontend/html/util/fetcher.js b/src/zenserver/frontend/html/util/fetcher.js
new file mode 100644
index 000000000..45f597404
--- /dev/null
+++ b/src/zenserver/frontend/html/util/fetcher.js
@@ -0,0 +1,76 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+"use strict";
+
+import { CbObject } from "./compactbinary.js"
+
+////////////////////////////////////////////////////////////////////////////////
+export class Fetcher
+{
+ constructor()
+ {
+ this._resource = "";
+ this._query = {};
+ }
+
+ resource(...parts)
+ {
+ var value = parts.join("/");
+ if (!value.startsWith("/"))
+ value= "/" + value;
+ this._resource = value;
+ return this;
+ }
+
+ param(name, value)
+ {
+ this._query[name] = value;
+ return this;
+ }
+
+ async json()
+ {
+ const response = await this._get("application/json");
+ return response ? (await response.json()) : {};
+ }
+
+ async cbo()
+ {
+ 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)
+ {
+ suffix += suffix ? "&" : "?";
+ suffix += key + "=" + this._query[key];
+ }
+ return this._resource + suffix;
+ }
+
+ 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;
+ }
+}
diff --git a/src/zenserver/frontend/html/util/friendly.js b/src/zenserver/frontend/html/util/friendly.js
new file mode 100644
index 000000000..6eee3a5b8
--- /dev/null
+++ b/src/zenserver/frontend/html/util/friendly.js
@@ -0,0 +1,23 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+"use strict";
+
+////////////////////////////////////////////////////////////////////////////////
+export 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"; }
+}
diff --git a/src/zenserver/frontend/html/zen.css b/src/zenserver/frontend/html/zen.css
index a725ffb79..033563736 100644
--- a/src/zenserver/frontend/html/zen.css
+++ b/src/zenserver/frontend/html/zen.css
@@ -1,23 +1,47 @@
/* Copyright Epic Games, Inc. All Rights Reserved. */
-/* page --------------------------------------------------------------------- */
-
-:root {
- --theme_g0: #000;
- --theme_g1: #555;
- --theme_g2: #999;
- --theme_g3: #f4f4f4;
- --theme_g4: #fff;
-
- --theme_p0: #069;
- --theme_p1: #58b;
- --theme_p2: #cce;
- --theme_p3: #dde;
- --theme_p4: #eeeef7;
-
- --theme_er: #fcc;
+/* theme -------------------------------------------------------------------- */
+
+@media (prefers-color-scheme: light) {
+ :root {
+ --theme_g0: #000;
+ --theme_g4: #fff;
+ --theme_g1: color-mix(in oklab, var(--theme_g0), var(--theme_g4) 45%);
+ --theme_g2: color-mix(in oklab, var(--theme_g0), var(--theme_g4) 80%);
+ --theme_g3: color-mix(in oklab, var(--theme_g0), var(--theme_g4) 96%);
+
+ --theme_p0: #069;
+ --theme_p4: hsl(210deg 40% 94%);
+ --theme_p1: color-mix(in oklab, var(--theme_p0), var(--theme_p4) 35%);
+ --theme_p2: color-mix(in oklab, var(--theme_p0), var(--theme_p4) 60%);
+ --theme_p3: color-mix(in oklab, var(--theme_p0), var(--theme_p4) 85%);
+
+ --theme_ln: var(--theme_p0);
+ --theme_er: #fcc;
+ }
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --theme_g0: #ddd;
+ --theme_g4: #222;
+ --theme_g1: color-mix(in oklab, var(--theme_g0), var(--theme_g4) 35%);
+ --theme_g2: color-mix(in oklab, var(--theme_g0), var(--theme_g4) 65%);
+ --theme_g3: color-mix(in oklab, var(--theme_g0), var(--theme_g4) 88%);
+
+ --theme_p0: #479;
+ --theme_p4: #333;
+ --theme_p1: color-mix(in oklab, var(--theme_p0), var(--theme_p4) 35%);
+ --theme_p2: color-mix(in oklab, var(--theme_p0), var(--theme_p4) 60%);
+ --theme_p3: color-mix(in oklab, var(--theme_p0), var(--theme_p4) 85%);
+
+ --theme_ln: #feb;
+ --theme_er: #622;
+ }
}
+/* page --------------------------------------------------------------------- */
+
body, input {
font-family: consolas, monospace;
font-size: 11pt;
@@ -27,12 +51,18 @@ body {
overflow-y: scroll;
margin: 0;
background-color: var(--theme_g4);
+ color: var(--theme_g0);
}
pre {
margin: 0;
}
+input {
+ background-color: var(--theme_g3);
+ border: 1px solid var(--theme_g2);
+}
+
* {
box-sizing: border-box;
}
@@ -41,57 +71,57 @@ pre {
max-width: 130em;
min-width: 80em;
margin: auto;
-}
-#container > div {
- margin: 1.0em 2.2em 1.5em 2.2em;
+ > div {
+ margin: 1.0em 2.2em 1.5em 2.2em;
+ }
}
/* links -------------------------------------------------------------------- */
.zen_action, a {
- all: unset;
- cursor: pointer;
- color: var(--theme_p0);
-}
+ all: unset;
+ cursor: pointer;
+ color: var(--theme_ln);
-.zen_action:hover, a:hover {
- text-decoration: underline var(--theme_p1);
+ &:hover {
+ text-decoration: underline var(--theme_ln);
+ }
}
/* sector ------------------------------------------------------------------- */
-h1, h2, h3 {
- white-space: nowrap;
-}
-
-h1 {
- font-size: 1.5em;
- width: 100%;
- border-bottom: 1px solid var(--theme_g2);
-}
-
-h2 {
- font-size: 1.25em;
- margin-bottom: 0.5em;
-}
-
-h3 {
- font-size: 1.1em;
- margin: 0em;
- padding: 0.4em;
- background-color: var(--theme_p4);
- border-left: 5px solid var(--theme_p2);
- font-weight: normal;
-}
-
.zen_sector {
+ h1, h2, h3 {
+ white-space: nowrap;
+ }
+
+ h1 {
+ font-size: 1.5em;
+ width: 100%;
+ border-bottom: 1px solid var(--theme_g2);
+ }
+
+ h2 {
+ font-size: 1.25em;
+ margin-bottom: 0.5em;
+ }
+
+ h3 {
+ font-size: 1.1em;
+ margin: 0em;
+ padding: 0.4em;
+ background-color: var(--theme_p4);
+ border-left: 5px solid var(--theme_p2);
+ font-weight: normal;
+ }
+
margin-bottom: 3em;
+ > *:not(h1) {
+ margin-left: 2em;
+ }
}
-.zen_sector > *:not(h1) {
- margin-left: 2em;
-}
/* table -------------------------------------------------------------------- */
@@ -100,229 +130,269 @@ h3 {
border: 1px solid var(--theme_g2);
border-left-style: none;
margin-bottom: 1.2em;
-}
-.zen_table > div {
- display: contents;
-}
+ > div {
+ display: contents;
+ }
-.zen_table > div:nth-of-type(odd) {
- background-color: var(--theme_g3);
-}
+ > div:nth-of-type(odd) {
+ background-color: var(--theme_g3);
+ }
-.zen_table > div:first-of-type {
- font-weight: bold;
- background-color: var(--theme_p3);
-}
+ > div:first-of-type {
+ font-weight: bold;
+ background-color: var(--theme_p3);
+ }
-.zen_table > div:hover {
- background-color: var(--theme_p4);
-}
+ > div:hover {
+ background-color: var(--theme_p4);
+ }
-.zen_table > hidden {
- visibility: hidden;
- display: none;
-}
+ > hidden {
+ visibility: hidden;
+ display: none;
+ }
-.zen_table > div > div {
- padding: 0.3em;
- padding-left: 0.75em;
- padding-right: 0.75em;
- align-content: center;
- border-left: 1px solid var(--theme_g2);
- overflow: auto;
- overflow-wrap: break-word;
- background-color: inherit;
+ > div > div {
+ padding: 0.3em;
+ padding-left: 0.75em;
+ padding-right: 0.75em;
+ align-content: center;
+ border-left: 1px solid var(--theme_g2);
+ overflow: auto;
+ overflow-wrap: break-word;
+ background-color: inherit;
+ }
}
/* toolbar ------------------------------------------------------------------ */
.zen_toolbar {
- display: flex;
- margin-top: 0.5em;
- margin-bottom: 0.6em;
-}
+ display: flex;
+ margin-top: 0.5em;
+ margin-bottom: 0.6em;
-.zen_toolbar.zen_toolbar_inline {
- margin: unset;
-}
+ > div {
+ display: flex;
+ align-items: center;
+ }
-.zen_toolbar > div {
- display: flex;
- align-items: center;
-}
+ > div > .zen_toolbar_sep {
+ color: var(--theme_g2);
+ }
-.zen_toolbar > div > .zen_toolbar_sep {
- color: var(--theme_g2);
-}
+ > div:last-child {
+ margin-left: auto;
+ }
-.zen_toolbar > div:last-child {
- margin-left: auto;
-}
+ > div > div {
+ padding-right: 0.7em;
+ }
-.zen_toolbar > div > div {
- padding-right: 0.7em;
-}
+ > div:last-child > :last-child {
+ padding-right: 0;
+ }
-.zen_toolbar > div:last-child > :last-child {
- padding-right: 0;
+ &.zen_toolbar_inline {
+ margin: unset;
+ }
}
+
/* modal -------------------------------------------------------------------- */
.zen_modal {
- position: fixed;
- z-index: 1;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- display: flex;
- justify-content: center;
- align-items: center;
- backdrop-filter: blur(5px);
-}
-
-.zen_modal .zen_modal_bg {
- position: absolute;
- z-index: -1;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: var(--theme_g0);
- opacity: 0.4;
-}
-
-.zen_modal > div {
- border-radius: 0.5em;
- background-color: var(--theme_g4);
- opacity: 1.0;
- 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 var(--theme_g2);
- padding: 1.2em 0em 0.5em 0em;
- color: var(--theme_g1);
-}
-
-.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;
- align-content: center;
- border-radius: 0.3em;
- background-color: var(--theme_p3);
- width: 6em;
- cursor: pointer;
-}
-
-.zen_modal .zen_modal_buttons > div:hover {
- background-color: var(--theme_p4);
-}
-
-.zen_modal .zen_modal_message {
- padding: 2em;
- min-height: 8em;
- align-content: center;
+ position: fixed;
+ z-index: 1;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ backdrop-filter: blur(5px);
+
+ .zen_modal_bg {
+ position: absolute;
+ z-index: -1;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: var(--theme_g0);
+ opacity: 0.4;
+ }
+
+ > div {
+ border-radius: 0.5em;
+ background-color: var(--theme_g4);
+ opacity: 1.0;
+ width: 35em;
+ padding: 0em 2em 2em 2em;
+ }
+
+ > div > div {
+ text-align: center;
+ }
+
+ .zen_modal_title {
+ font-size: 1.2em;
+ border-bottom: 1px solid var(--theme_g2);
+ padding: 1.2em 0em 0.5em 0em;
+ color: var(--theme_g1);
+ }
+
+ .zen_modal_buttons {
+ display: flex;
+ justify-content: center;
+ padding-bottom: 0em;
+
+ > div {
+ margin: 0em 1em 0em 1em;
+ padding: 1em;
+ align-content: center;
+ border-radius: 0.3em;
+ background-color: var(--theme_p3);
+ width: 6em;
+ cursor: pointer;
+ }
+
+ > div:hover {
+ background-color: var(--theme_p4);
+ }
+ }
+
+ .zen_modal_message {
+ padding: 2em;
+ min-height: 8em;
+ align-content: center;
+ }
+}
+
+/* progress bar ------------------------------------------------------------- */
+
+.zen_progressbar {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 0.5em;
+
+ > div:first-of-type {
+ /* label */
+ padding: 0.3em;
+ padding-top: 0.8em;
+ background-color: var(--theme_p4);
+ width: max-content;
+ font-size: 0.8em;
+ }
+
+ > div:last-of-type {
+ /* bar */
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 0%;
+ height: 100%;
+ background-color: var(--theme_p1);
+ }
+
+ > div:nth-of-type(2) {
+ /* bg */
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: var(--theme_p3);
+ }
}
/* crumbs ------------------------------------------------------------------- */
#crumbs {
- display: flex;
- position: relative;
- top: -1em;
-}
+ display: flex;
+ position: relative;
+ top: -1em;
-#crumbs > div {
- padding-right: 0.5em;
-}
+ > div {
+ padding-right: 0.5em;
+ }
-#crumbs > div:nth-child(odd)::after {
- content: ":";
- font-weight: bolder;
- color: var(--theme_p2);
+ > div:nth-child(odd)::after {
+ content: ":";
+ font-weight: bolder;
+ color: var(--theme_p2);
+ }
}
/* branding ----------------------------------------------------------------- */
#branding {
- font-size: 10pt;
- font-weight: bolder;
- margin-bottom: 2.6em;
- position: relative;
-}
-
-#logo {
- width: min-content;
- margin: auto;
- user-select: none;
- position: relative;
-}
-
-#logo #go_home {
- width: 100%;
- height: 100%;
- position: absolute;
- top: 0;
- left: 0;
-}
-
-#logo:hover {
- filter: drop-shadow(0 0.15em 0.1em var(--theme_p2));
-}
-
-#ue_logo {
- position: absolute;
- top: 1em;
- right: 0;
- width: 5em;
- margin: auto;
+ font-size: 10pt;
+ font-weight: bolder;
+ margin-bottom: 2.6em;
+ position: relative;
+
+ #logo {
+ width: min-content;
+ margin: auto;
+ user-select: none;
+ position: relative;
+
+ #go_home {
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ top: 0;
+ left: 0;
+ }
+ }
+
+ #logo:hover {
+ filter: drop-shadow(0 0.15em 0.1em var(--theme_p2));
+ }
+
+ #ue_logo {
+ position: absolute;
+ top: 1em;
+ right: 0;
+ width: 5em;
+ margin: auto;
+ }
}
/* error -------------------------------------------------------------------- */
#error {
- position: fixed;
- bottom: 0;
- z-index: 1;
- color: var(--theme_g0);
- background-color: var(--theme_er);
- padding: 1.0em 2em 2em 2em;
- width: 100%;
- border-top: 1px solid var(--theme_g0);
- display: flex;
-}
-
-#error > div:nth-child(1) {
- font-size: 2.5em;
- font-weight: bolder;
- font-family: serif;
- transform: rotate(-13deg);
- color: var(--theme_p0);
-}
-
-#error > div:nth-child(2) {
- margin-left: 2em;
-}
-
-#error > div:nth-child(2) > pre:nth-child(2) {
- margin-top: 0.5em;
- font-size: 0.8em;
- color: var(--theme_g1);
+ position: fixed;
+ bottom: 0;
+ z-index: 1;
+ color: var(--theme_g0);
+ background-color: var(--theme_er);
+ padding: 1.0em 2em 2em 2em;
+ width: 100%;
+ border-top: 1px solid var(--theme_g0);
+ display: flex;
+
+ > div:nth-child(1) {
+ font-size: 2.5em;
+ font-weight: bolder;
+ font-family: serif;
+ transform: rotate(-13deg);
+ color: var(--theme_p0);
+ }
+
+ > div:nth-child(2) {
+ margin-left: 2em;
+ }
+
+ > div:nth-child(2) > pre:nth-child(2) {
+ margin-top: 0.5em;
+ font-size: 0.8em;
+ color: var(--theme_g1);
+ }
}
/* stats -------------------------------------------------------------------- */
diff --git a/src/zenserver/frontend/html/zen.js b/src/zenserver/frontend/html/zen.js
index 4d3895d1f..ffeaeb4ee 100644
--- a/src/zenserver/frontend/html/zen.js
+++ b/src/zenserver/frontend/html/zen.js
@@ -2,27 +2,9 @@
"use strict";
-////////////////////////////////////////////////////////////////////////////////
-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"; }
-}
-
-
+import { Fetcher } from "./util/fetcher.js"
+import { Friendly } from "./util/friendly.js"
+import { create_indexer } from "./indexer/indexer.js"
////////////////////////////////////////////////////////////////////////////////
class ComponentBase
@@ -70,7 +52,7 @@ class ComponentDom extends ComponentBase
text(value)
{
- value = value.toString();
+ value = (value == undefined) ? "undefined" : value.toString();
this._element.innerHTML = (value != "") ? value : "";
return this;
}
@@ -162,7 +144,7 @@ class TableCell extends Component
constructor(element, row)
{
super(element);
- this._row = row
+ this._row = row;
}
get_table() { return this.get_row().get_table(); }
@@ -254,7 +236,7 @@ class Table extends Component
cells.push("");
var ret = [];
- var row = this.tag()
+ var row = this.tag();
row = new TableRow(row, this, index, ret);
for (const cell of cells)
{
@@ -265,7 +247,7 @@ class Table extends Component
if (this._index >= 0)
ret.shift();
- return row
+ return row;
}
add_row(...args)
@@ -333,7 +315,7 @@ class PropTable extends Table
filter(...needles)
{
for (var row of this)
- row.retag("div")
+ row.retag("div");
if (needles.length == 0)
return;
@@ -365,8 +347,8 @@ class Modal
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._title = rect.tag().classify("zen_modal_title");
+ this._content = rect.tag().classify("zen_modal_message");
this._buttons = rect.tag().classify("zen_modal_buttons");
}
@@ -412,8 +394,8 @@ class Toolbar extends Component
if (inline)
root.classify("zen_toolbar_inline");
- this._left = new Toolbar.Side(root.tag())
- this._right = new Toolbar.Side(root.tag())
+ this._left = new Toolbar.Side(root.tag());
+ this._right = new Toolbar.Side(root.tag());
}
left() { return this._left; }
@@ -423,95 +405,44 @@ class Toolbar extends Component
////////////////////////////////////////////////////////////////////////////////
-class Sectormatron extends Component
+class ProgressBar extends Component
{
- constructor(parent, depth=1)
+ constructor(parent)
{
- super(parent);
- this._depth = depth;
+ const root = parent.tag().classify("zen_progressbar");
+ super(root);
+ this._label = root.tag();
+ root.tag(); // bg
+ this._bar = root.tag();
}
- add_section(name)
+ set_progress(what, count=0, end=1)
{
- 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);
+ const percent = (((count * 100) / end) | 0).toString() + "%";
+ this._bar.style("width", percent);
+ this._label.text(`${what}... ${count}/${end} (${percent})`);
}
}
////////////////////////////////////////////////////////////////////////////////
-class Fetcher
+class Sectormatron extends Component
{
- constructor()
- {
- this._resource = "";
- this._query = {};
- }
-
- resource(...parts)
- {
- var value = parts.join("/");
- if (!value.startsWith("/"))
- value= "/" + value;
- this._resource = value;
- return this;
- }
-
- param(name, value)
- {
- this._query[name] = value;
- return this;
- }
-
- async json()
- {
- const response = await this._get("application/json");
- return response ? (await response.json()) : {};
- }
-
- async cbo()
- {
- 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()
+ constructor(parent, depth=1)
{
- var suffix = "";
- for (var key in this._query)
- {
- suffix += suffix ? "&" : "?";
- suffix += key + "=" + this._query[key];
- }
- return this._resource + suffix;
+ super(parent);
+ this._depth = depth;
}
- async _get(accept="*")
+ add_section(name)
{
- const resource = this._build_uri();
- const response = await fetch(resource, {
- "method" : "GET",
- "headers" : { "Accept": accept },
- });
+ var node = this.tag();
+ if (this._depth == 1)
+ node.classify("zen_sector");
- if (response.status >= 200 || response.status <= 299)
- return response;
+ node.tag("h" + this._depth).text(name);
+ return new Sectormatron(node, this._depth + 1);
}
}
@@ -555,7 +486,7 @@ class Page
const url = new URL(window.location);
for (var [key, xfer] of this._params)
- url.searchParams.set(key, xfer)
+ url.searchParams.set(key, xfer);
history.replaceState(null, "", url);
return value;
@@ -651,7 +582,7 @@ class ZenPage extends Page
////////////////////////////////////////////////////////////////////////////////
class Entry extends ZenPage
{
- async main()
+ main()
{
this.set_title("oplog entry");
@@ -659,11 +590,33 @@ class Entry extends ZenPage
const oplog = this.get_param("oplog");
const opkey = this.get_param("opkey");
- var entry = new Fetcher()
+ this._entry = new Fetcher()
.resource("prj", project, "oplog", oplog, "entries")
.param("opkey", opkey)
- .cbo()
- entry = await entry;
+ .cbo();
+
+ this.load_indexer(project, oplog, () => this._build_page());
+ }
+
+ async load_indexer(project, oplog, loaded_cb)
+ {
+ if (this._indexer != undefined)
+ return loaded_cb();
+
+ const progress_bar = new ProgressBar(this._parent);
+ progress_bar.set_progress("indexing");
+ const indexer = create_indexer(project, oplog, (...args) => {
+ progress_bar.set_progress(...args);
+ });
+ this._indexer = await indexer;
+ progress_bar.destroy();
+
+ loaded_cb();
+ }
+
+ async _build_page()
+ {
+ var entry = await this._entry;
entry = entry.as_object().find("entry").as_object();
const name = entry.find("key").as_value();
@@ -675,18 +628,23 @@ class Entry extends ZenPage
if (tree == undefined)
tree = this._convert_legacy_to_tree(entry);
+ if (tree == undefined)
+ return this._display_unsupported(section, 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"], Table.Flag_FitLeft);
+ const table = new Table(dep_section, ["name", "id"], Table.Flag_PackRight);
for (const dep_id of tree[dep_name])
{
- const cell_values = [dep_id.toString(16)];
+ const cell_values = ["", dep_id.toString(16).padStart(16, "0")];
const row = table.add_row(...cell_values);
- row.get_cell(0).on_click(() => void(0));
+
+ var opkey = this._indexer.lookup_id(dep_id);
+ row.get_cell(0).text(opkey).on_click((k) => this.view_opkey(k), opkey);
}
}
}
@@ -707,8 +665,8 @@ class Entry extends ZenPage
var file_name;
for (const field of item.as_object())
{
- if (field.get_name() == "data") io_hash = field.as_value();
- else if (field.get_name() == "filename") file_name = field.as_value();
+ if (field.is_named("data")) io_hash = field.as_value();
+ else if (field.is_named("filename")) file_name = field.as_value();
}
if (io_hash instanceof Uint8Array)
@@ -732,26 +690,35 @@ class Entry extends ZenPage
action_tb.left().add("copy-hash").on_click(async (v) => {
await navigator.clipboard.writeText(v);
}, io_hash);
- action_tb.left().add("view");
- action_tb.left().add("drop");
}
}
}
// props
{
- const object = entry.to_js_object();
+ const object = entry.to_js_object();
var sub_section = section.add_section("props");
new PropTable(sub_section).add_object(object);
}
}
+ _display_unsupported(section, entry)
+ {
+ const object = entry.to_js_object();
+ const text = JSON.stringify(object, null, " ");
+ section.tag("pre").text(text);
+ }
+
_convert_legacy_to_tree(entry)
{
+ const pkg_data = entry.find("packagedata");
+ if (pkg_data == undefined)
+ return
+
const tree = {};
var id = 0n;
- for (var item of entry.find("packagedata").as_array())
+ for (var item of pkg_data.as_array())
{
var pkg_id = item.as_object().find("id");
if (pkg_id == undefined)
@@ -771,7 +738,7 @@ class Entry extends ZenPage
for (const field of pkgst_entry)
{
- const field_name = field.get_name()
+ const field_name = field.get_name();
if (!field_name.endsWith("importedpackageids"))
continue;
@@ -786,6 +753,13 @@ class Entry extends ZenPage
return tree;
}
+
+ view_opkey(opkey)
+ {
+ const params = this._params;
+ params.set("opkey", opkey);
+ window.location.search = params;
+ }
}
@@ -807,16 +781,30 @@ class Oplog extends ZenPage
const project = this.get_param("project");
const oplog = this.get_param("oplog");
+ this._indexer = this._create_indexer(project, oplog);
+
this.set_title("oplog - " + oplog);
var section = this.add_section(project + " - " + oplog);
- this._build_nav(section)
+ this._build_nav(section);
this._entry_table = new Table(section, ["key"]);
await this._build_table();
}
+ async _create_indexer(project, oplog)
+ {
+ const progress_bar = new ProgressBar(this._parent);
+ progress_bar.set_progress("indexing");
+ var indexer = create_indexer(project, oplog, (...args) => {
+ progress_bar.set_progress(...args);
+ });
+ indexer = await indexer;
+ progress_bar.destroy();
+ return indexer;
+ }
+
_build_nav(section)
{
var nav = new Toolbar(section);
@@ -832,8 +820,8 @@ class Oplog extends ZenPage
nav.left().add(count).on_click(handler, count);
}
- nav.right().add("search:", "label");
- nav.right().add("", "input").attr("disabled", "");
+ var search_input = nav.right().add("search:", "label").tag("input")
+ search_input.on("change", (x) => this._search(x.inner().value), search_input);
}
async _build_table()
@@ -851,7 +839,7 @@ class Oplog extends ZenPage
entries = (await entries)["entries"];
if (entries == undefined)
- return
+ return;
for (const entry of entries)
{
@@ -878,6 +866,40 @@ class Oplog extends ZenPage
this._index_start = Math.max(0, index);
this._build_table();
}
+
+ async _search(needle)
+ {
+ needle = needle.trim();
+ if (needle.length < 3)
+ return;
+
+ this._entry_table.clear(this._index_start);
+
+ const project = this.get_param("project");
+ const oplog = this.get_param("oplog");
+
+ const indexer = await this._indexer;
+
+ var added = 0;
+ const truncate_at = this.get_param("searchmax") || 250;
+ for (var name of indexer.search(needle))
+ {
+ var row = this._entry_table.add_row(name);
+
+ row.get_cell(0).link("", {
+ "page" : "entry",
+ "project" : project,
+ "oplog" : oplog,
+ "opkey" : name,
+ });
+
+ if (++added >= truncate_at)
+ {
+ this._entry_table.add_row("...truncated");
+ break;
+ }
+ }
+ }
}
@@ -934,10 +956,10 @@ class Project extends ZenPage
action_tb.left().add("drop").on_click((x) => this.drop_oplog(x), name);
info = await info;
- row.get_cell(1).text(Friendly.sep(info["markerpath"]));
+ row.get_cell(1).text(info["markerpath"]);
row.get_cell(2).text(Friendly.kib(info["totalsize"]));
row.get_cell(3).text(Friendly.sep(info["opcount"]));
- row.get_cell(4).text(Friendly.sep(info["expired"]));
+ row.get_cell(4).text(info["expired"]);
}
}
@@ -954,7 +976,7 @@ class Project extends ZenPage
.title("Confirmation")
.message(`Drop oplog '${oplog_id}'?`)
.option("Yes", () => drop())
- .option("No")
+ .option("No");
}
}
@@ -973,10 +995,10 @@ class Test extends ZenPage
};
return function(a=5, b=10) {
const co = "aeioubcdfghjklmnpqrstvwxyz";
- var ret = ""
+ var ret = "";
for (var i = 0, n = r(a,b); i < n; ++i)
ret += co[r(0, co.length)];
- return ret
+ return ret;
};
})();
var gen_para = function(a=5, b=10, s=" ") {
@@ -988,6 +1010,23 @@ class Test extends ZenPage
this.set_title("test");
+ // swatches
+ const swatches = this._parent.tag()
+ .style("position", "absolute")
+ .style("top", "3.5em")
+ .style("left", "3.5em")
+ for (var suffix of ["g0", "g1", "g2", "g3", "g4",
+ "p0", "p1", "p2", "p3", "p4",
+ "ln", "er"])
+ {
+ var swatch = swatches.tag()
+ .style("float", "left")
+ .style("width", "2em")
+ .style("height", "2em")
+ .style("background-color", `var(--theme_${suffix})`)
+ .text(suffix);
+ }
+
// section
var section0 = this.add_section("section");
var section1 = section0.add_section("sub-section");
@@ -1068,6 +1107,16 @@ class Test extends ZenPage
toolbar.right().add("tbitem2").on_click(tb_item_clicked, 33, -55);
toolbar.right().add("tbitem3").on_click(tb_item_clicked, 44, -88);
+ // progress bar
+ const progress_bar = new ProgressBar(this._parent);
+ setInterval(function() {
+ var count = 0
+ return () => {
+ count = (count + 1) % 100;
+ progress_bar.set_progress("testing", count, 100);
+ };
+ }(), 49.3);
+
// error
throw Error("deliberate error");
}
@@ -1236,7 +1285,7 @@ class Stat extends ZenPage
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);
- this._filter_input = tb_right.add("", "input")
+ this._filter_input = tb_right.add("", "label").tag("input");
this._filter_input.on("change", (x) => this.update_filter(x.inner().value), this._filter_input);
this._table = new PropTable(section);
@@ -1356,3 +1405,5 @@ async function main()
impl.main();
}
+
+main();