// Copyright Epic Games, Inc. All Rights Reserved. "use strict"; import { ZenPage } from "./page.js" import { Fetcher } from "../util/fetcher.js" //////////////////////////////////////////////////////////////////////////////// export class Page extends ZenPage { async main() { this.set_title("workspaces"); // Workspace Service Stats const stats_section = this._collapsible_section("Workspace Service Stats"); this._stats_grid = stats_section.tag().classify("grid").classify("stats-tiles"); const stats = await new Fetcher().resource("stats", "ws").json().catch(() => null); if (stats) { this._render_stats(stats); } this.connect_stats_ws((all_stats) => { const s = all_stats["ws"]; if (s) { this._render_stats(s); } }); const section = this.add_section("Workspaces"); const host = section.tag(); // Toolbar: refresh button const toolbar = host.tag().classify("module-bulk-bar"); this._btn_refresh = toolbar.tag("button").classify("module-bulk-btn").inner(); this._btn_refresh.textContent = "\u21BB Refresh"; this._btn_refresh.addEventListener("click", () => this._do_refresh()); // Workspace table (raw DOM — in-place row updates require stable element refs) const table = document.createElement("table"); table.className = "module-table"; const thead = document.createElement("thead"); const hrow = document.createElement("tr"); for (const label of ["WORKSPACE ID", "ROOT PATH"]) { const th = document.createElement("th"); th.textContent = label; hrow.appendChild(th); } thead.appendChild(hrow); table.appendChild(thead); this._tbody = document.createElement("tbody"); table.appendChild(this._tbody); host.inner().appendChild(table); // State this._expanded = new Set(); // workspace ids with shares panel open this._row_cache = new Map(); // workspace id -> row refs, for in-place DOM updates this._loading = false; await this._load(); } async _load() { if (this._loading) { return; } this._loading = true; this._btn_refresh.disabled = true; try { const data = await new Fetcher().resource("/ws/").json(); const workspaces = data.workspaces || []; this._render(workspaces); } catch (e) { /* service unavailable */ } finally { this._loading = false; this._btn_refresh.disabled = false; } } async _do_refresh() { if (this._loading) { return; } this._btn_refresh.disabled = true; try { await new Fetcher().resource("/ws/refresh").text(); } catch (e) { /* ignore */ } await this._load(); } _render(workspaces) { const ws_map = new Map(workspaces.map(w => [w.id, w])); // Remove rows for workspaces no longer present for (const [id, row] of this._row_cache) { if (!ws_map.has(id)) { row.tr.remove(); row.detail_tr.remove(); this._row_cache.delete(id); this._expanded.delete(id); } } // Create or update rows, then reorder tbody to match response order. // appendChild on an existing node moves it, so iterating in response order // achieves correct ordering without touching rows already in the right position. for (const ws of workspaces) { const id = ws.id || ""; const shares = ws.shares || []; let row = this._row_cache.get(id); if (row) { // Update in-place — preserves DOM node identity so expanded state is kept row.root_path_node.nodeValue = ws.root_path || ""; row.detail_tr.style.display = this._expanded.has(id) ? "" : "none"; row.btn_expand.textContent = this._expanded.has(id) ? "\u25BE" : "\u25B8"; const shares_json = JSON.stringify(shares); if (shares_json !== row.shares_json) { row.shares_json = shares_json; this._render_shares(row.sh_tbody, shares); } } else { // Create new workspace row const tr = document.createElement("tr"); const detail_tr = document.createElement("tr"); detail_tr.className = "module-metrics-row"; detail_tr.style.display = this._expanded.has(id) ? "" : "none"; const btn_expand = document.createElement("button"); btn_expand.className = "module-expand-btn"; btn_expand.textContent = this._expanded.has(id) ? "\u25BE" : "\u25B8"; btn_expand.addEventListener("click", () => { if (this._expanded.has(id)) { this._expanded.delete(id); detail_tr.style.display = "none"; btn_expand.textContent = "\u25B8"; } else { this._expanded.add(id); detail_tr.style.display = ""; btn_expand.textContent = "\u25BE"; } }); const id_wrap = document.createElement("span"); id_wrap.className = "ws-id-wrap"; id_wrap.appendChild(btn_expand); id_wrap.appendChild(document.createTextNode("\u00A0" + id)); const td_id = document.createElement("td"); td_id.appendChild(id_wrap); tr.appendChild(td_id); const root_path_node = document.createTextNode(ws.root_path || ""); const td_root = document.createElement("td"); td_root.appendChild(root_path_node); tr.appendChild(td_root); // Detail row: nested shares table const sh_table = document.createElement("table"); sh_table.className = "module-table ws-share-table"; const sh_thead = document.createElement("thead"); const sh_hrow = document.createElement("tr"); for (const label of ["SHARE ID", "SHARE PATH", "ALIAS"]) { const th = document.createElement("th"); th.textContent = label; sh_hrow.appendChild(th); } sh_thead.appendChild(sh_hrow); sh_table.appendChild(sh_thead); const sh_tbody = document.createElement("tbody"); sh_table.appendChild(sh_tbody); const detail_td = document.createElement("td"); detail_td.colSpan = 2; detail_td.className = "ws-detail-cell"; detail_td.appendChild(sh_table); detail_tr.appendChild(detail_td); this._render_shares(sh_tbody, shares); row = { tr, detail_tr, root_path_node, sh_tbody, btn_expand, shares_json: JSON.stringify(shares) }; this._row_cache.set(id, row); } this._tbody.appendChild(row.tr); this._tbody.appendChild(row.detail_tr); } } _render_stats(stats) { const grid = this._stats_grid; grid.inner().innerHTML = ""; // HTTP Requests tile this._render_http_requests_tile(grid, stats.requests); } _render_shares(sh_tbody, shares) { sh_tbody.innerHTML = ""; if (shares.length === 0) { const tr = document.createElement("tr"); const td = document.createElement("td"); td.colSpan = 3; td.className = "ws-no-shares-cell"; td.textContent = "No shares"; tr.appendChild(td); sh_tbody.appendChild(tr); return; } for (const share of shares) { const tr = document.createElement("tr"); for (const text of [share.id || "", share.share_path || "", share.alias || ""]) { const td = document.createElement("td"); td.textContent = text; tr.appendChild(td); } sh_tbody.appendChild(tr); } } }