aboutsummaryrefslogtreecommitdiff
path: root/src/zenserver/frontend/html/pages/objectstore.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/zenserver/frontend/html/pages/objectstore.js')
-rw-r--r--src/zenserver/frontend/html/pages/objectstore.js232
1 files changed, 232 insertions, 0 deletions
diff --git a/src/zenserver/frontend/html/pages/objectstore.js b/src/zenserver/frontend/html/pages/objectstore.js
new file mode 100644
index 000000000..69e0a91b3
--- /dev/null
+++ b/src/zenserver/frontend/html/pages/objectstore.js
@@ -0,0 +1,232 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+"use strict";
+
+import { ZenPage } from "./page.js"
+import { Fetcher } from "../util/fetcher.js"
+import { Friendly } from "../util/friendly.js"
+import { Table } from "../util/widgets.js"
+
+////////////////////////////////////////////////////////////////////////////////
+export class Page extends ZenPage
+{
+ generate_crumbs() {}
+
+ async main()
+ {
+ this.set_title("object store");
+
+ const section = this.add_section("Object Store");
+ this._stats_host = section;
+
+ const buckets_section = this.add_section("Buckets");
+ this._buckets_host = buckets_section;
+
+ await this._update();
+ this._poll_timer = setInterval(() => this._update(), 5000);
+ }
+
+ async _update()
+ {
+ try
+ {
+ const data = await new Fetcher().resource("/obj/").json();
+ this._render(data);
+ }
+ catch (e) { /* service unavailable */ }
+ }
+
+ _render(data)
+ {
+ const buckets = data.buckets || [];
+
+ // Stats summary
+ {
+ const host = this._stats_host;
+ if (!this._stats_grid)
+ {
+ this._stats_grid = host.tag().classify("grid").classify("stats-tiles");
+ }
+ const grid = this._stats_grid;
+ grid.inner().innerHTML = "";
+
+ const total_objects = buckets.reduce((sum, b) => sum + (b.object_count || 0), 0);
+ const total_size = buckets.reduce((sum, b) => sum + (b.size || 0), 0);
+
+ {
+ const tile = grid.tag().classify("card").classify("stats-tile");
+ tile.tag().classify("card-title").text("Buckets");
+ const body = tile.tag().classify("tile-metrics");
+ this._metric(body, Friendly.sep(buckets.length), "total", true);
+ }
+
+ {
+ const tile = grid.tag().classify("card").classify("stats-tile");
+ tile.tag().classify("card-title").text("Objects");
+ const body = tile.tag().classify("tile-metrics");
+ this._metric(body, Friendly.sep(total_objects), "total", true);
+ }
+
+ {
+ const tile = grid.tag().classify("card").classify("stats-tile");
+ tile.tag().classify("card-title").text("Storage");
+ const body = tile.tag().classify("tile-metrics");
+ this._metric(body, Friendly.bytes(total_size), "total size", true);
+ }
+
+ {
+ const tile = grid.tag().classify("card").classify("stats-tile");
+ tile.tag().classify("card-title").text("Served");
+ const body = tile.tag().classify("tile-metrics");
+ this._metric(body, Friendly.bytes(data.total_bytes_served || 0), "total bytes served", true);
+ }
+ }
+
+ // Buckets table
+ {
+ const host = this._buckets_host;
+ if (this._buckets_table)
+ {
+ this._buckets_table.clear();
+ }
+ else
+ {
+ this._buckets_table = host.add_widget(
+ Table,
+ ["name", "objects", "size"],
+ Table.Flag_FitLeft
+ );
+ }
+
+ if (buckets.length === 0)
+ {
+ return;
+ }
+
+ for (const bucket of buckets)
+ {
+ const row = this._buckets_table.add_row(
+ bucket.name || "-",
+ Friendly.sep(bucket.object_count || 0),
+ Friendly.bytes(bucket.size || 0),
+ );
+
+ const row_elem = row.inner();
+ for (const cell of row_elem.children)
+ {
+ cell.style.cursor = "pointer";
+ cell.addEventListener("click", () => this._toggle_bucket(bucket.name, row));
+ }
+ }
+ }
+ }
+
+ async _toggle_bucket(name, row)
+ {
+ // If already expanded, collapse
+ if (this._expanded_bucket === name)
+ {
+ if (this._expanded_el)
+ {
+ this._expanded_el.remove();
+ this._expanded_el = null;
+ }
+ this._expanded_bucket = null;
+ return;
+ }
+
+ // Collapse any previous
+ if (this._expanded_el)
+ {
+ this._expanded_el.remove();
+ this._expanded_el = null;
+ }
+
+ this._expanded_bucket = name;
+
+ try
+ {
+ const data = await new Fetcher().resource("/obj/bucket/" + encodeURIComponent(name) + "/").json();
+ const result = data.ListBucketResult || {};
+ const contents = result.Contents || [];
+
+ const detail = document.createElement("div");
+ detail.className = "objectstore-bucket-detail";
+
+ if (contents.length === 0)
+ {
+ detail.textContent = "Bucket is empty.";
+ }
+ else
+ {
+ const tbl = document.createElement("table");
+ tbl.className = "objectstore-objects-table";
+
+ const thead = document.createElement("thead");
+ const hdr = document.createElement("tr");
+ for (const col of ["key", "size"])
+ {
+ const th = document.createElement("th");
+ th.textContent = col;
+ hdr.appendChild(th);
+ }
+ thead.appendChild(hdr);
+ tbl.appendChild(thead);
+
+ const tbody = document.createElement("tbody");
+ const limit = Math.min(contents.length, 100);
+ for (let i = 0; i < limit; i++)
+ {
+ const obj = contents[i];
+ const tr = document.createElement("tr");
+
+ const td_key = document.createElement("td");
+ td_key.textContent = obj.Key || "-";
+ tr.appendChild(td_key);
+
+ const td_size = document.createElement("td");
+ td_size.textContent = Friendly.bytes(obj.Size || 0);
+ td_size.style.textAlign = "right";
+ tr.appendChild(td_size);
+
+ tbody.appendChild(tr);
+ }
+ tbl.appendChild(tbody);
+ detail.appendChild(tbl);
+
+ if (contents.length > limit)
+ {
+ const more = document.createElement("div");
+ more.style.opacity = "0.7";
+ more.style.fontSize = "0.85em";
+ more.style.marginTop = "4px";
+ more.textContent = `Showing ${limit} of ${contents.length} objects.`;
+ detail.appendChild(more);
+ }
+ }
+
+ // Insert after the row's last cell
+ const last_cell = row.inner().lastElementChild;
+ if (last_cell)
+ {
+ last_cell.after(detail);
+ }
+ this._expanded_el = detail;
+ }
+ catch (e)
+ {
+ this._expanded_bucket = null;
+ }
+ }
+
+ _metric(parent, value, label, hero = false)
+ {
+ const m = parent.tag().classify("tile-metric");
+ if (hero)
+ {
+ m.classify("tile-metric-hero");
+ }
+ m.tag().classify("metric-value").text(value);
+ m.tag().classify("metric-label").text(label);
+ }
+}