diff options
Diffstat (limited to 'src/zenserver/frontend/html/pages/objectstore.js')
| -rw-r--r-- | src/zenserver/frontend/html/pages/objectstore.js | 232 |
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); + } +} |