diff options
| author | Martin Ridgers <[email protected]> | 2024-11-11 10:31:34 +0100 |
|---|---|---|
| committer | GitHub Enterprise <[email protected]> | 2024-11-11 10:31:34 +0100 |
| commit | 05d1044045539557dfe4e9c8996737d83f9dee89 (patch) | |
| tree | 00907e9a5306318e8a9d169348348b7a5cc1f32d /src | |
| parent | Update VERSION.txt (diff) | |
| download | zen-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.zip | bin | 119799 -> 136697 bytes | |||
| -rw-r--r-- | src/zenserver/frontend/html/index.html | 5 | ||||
| -rw-r--r-- | src/zenserver/frontend/html/indexer/cache.js | 65 | ||||
| -rw-r--r-- | src/zenserver/frontend/html/indexer/indexer.js | 193 | ||||
| -rw-r--r-- | src/zenserver/frontend/html/indexer/worker.js | 131 | ||||
| -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.js | 76 | ||||
| -rw-r--r-- | src/zenserver/frontend/html/util/friendly.js | 23 | ||||
| -rw-r--r-- | src/zenserver/frontend/html/zen.css | 542 | ||||
| -rw-r--r-- | src/zenserver/frontend/html/zen.js | 305 |
10 files changed, 1000 insertions, 372 deletions
diff --git a/src/zenserver/frontend/html.zip b/src/zenserver/frontend/html.zip Binary files differindex cda479fc8..1be5f0d9a 100644 --- a/src/zenserver/frontend/html.zip +++ b/src/zenserver/frontend/html.zip 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(); |