diff options
Diffstat (limited to 'src/zenserver/frontend/html/pages/workspaces.js')
| -rw-r--r-- | src/zenserver/frontend/html/pages/workspaces.js | 239 |
1 files changed, 239 insertions, 0 deletions
diff --git a/src/zenserver/frontend/html/pages/workspaces.js b/src/zenserver/frontend/html/pages/workspaces.js new file mode 100644 index 000000000..db02e8be1 --- /dev/null +++ b/src/zenserver/frontend/html/pages/workspaces.js @@ -0,0 +1,239 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +"use strict"; + +import { ZenPage } from "./page.js" +import { Fetcher } from "../util/fetcher.js" +import { copy_button } from "../util/widgets.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)); + id_wrap.appendChild(copy_button(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) + { + stats = this._merge_last_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); + } + } +} |