// 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 { Modal } from "../util/modal.js" import { Table, Toolbar } from "../util/widgets.js" //////////////////////////////////////////////////////////////////////////////// export class Page extends ZenPage { async main() { // Discover which services are available const api_data = await new Fetcher().resource("/api/").json(); const available = new Set((api_data.services || []).map(s => s.base_uri)); // project list var project_table = null; if (available.has("/prj/")) { var section = this.add_section("Cooked Projects"); section.tag().classify("dropall").text("drop-all").on_click(() => this.drop_all("projects")); var columns = [ "name", "project_dir", "engine_dir", "actions", ]; project_table = section.add_widget(Table, columns); var projects = await new Fetcher().resource("/prj/list").json(); projects.sort((a, b) => (b.LastAccessTime || 0) - (a.LastAccessTime || 0)); projects = projects.slice(0, 25); projects.sort((a, b) => a.Id.localeCompare(b.Id)); for (const project of projects) { var row = project_table.add_row( "", project.ProjectRootDir, project.EngineRootDir, ); var cell = row.get_cell(0); cell.tag().text(project.Id).on_click((x) => this.view_project(x), project.Id); var cell = row.get_cell(-1); var action_tb = new Toolbar(cell, true); action_tb.left().add("view").on_click((x) => this.view_project(x), project.Id); action_tb.left().add("drop").on_click((x) => this.drop_project(x), project.Id); row.attr("zs_name", project.Id); } } // cache var cache_table = null; if (available.has("/z$/")) { var section = this.add_section("Cache"); section.tag().classify("dropall").text("drop-all").on_click(() => this.drop_all("z$")); var columns = [ "namespace", "dir", "buckets", "entries", "size disk", "size mem", "actions", ]; var zcache_info = await new Fetcher().resource("/z$/").json(); cache_table = section.add_widget(Table, columns, Table.Flag_FitLeft|Table.Flag_PackRight); for (const namespace of zcache_info["Namespaces"] || []) { new Fetcher().resource(`/z$/${namespace}/`).json().then((data) => { const row = cache_table.add_row( "", data["Configuration"]["RootDir"], data["Buckets"].length, data["EntryCount"], Friendly.bytes(data["StorageSize"].DiskSize), Friendly.bytes(data["StorageSize"].MemorySize) ); var cell = row.get_cell(0); cell.tag().text(namespace).on_click(() => this.view_zcache(namespace)); row.get_cell(1).tag().text(namespace); cell = row.get_cell(-1); const action_tb = new Toolbar(cell, true); action_tb.left().add("view").on_click(() => this.view_zcache(namespace)); action_tb.left().add("drop").on_click(() => this.drop_zcache(namespace)); row.attr("zs_name", namespace); }); } } // stats tiles const safe_lookup = (obj, path, pretty=undefined) => { const ret = path.split(".").reduce((a,b) => a && a[b], obj); if (ret === undefined) return undefined; return pretty ? pretty(ret) : ret; }; var section = this.add_section("Stats"); section.tag().classify("dropall").text("metrics dashboard →").on_click(() => { window.location = "?page=metrics"; }); var providers_data = await new Fetcher().resource("stats").json(); var provider_list = providers_data["providers"] || []; var all_stats = {}; await Promise.all(provider_list.map(async (provider) => { all_stats[provider] = await new Fetcher().resource("stats", provider).json(); })); this._stats_grid = section.tag().classify("grid").classify("stats-tiles"); this._safe_lookup = safe_lookup; this._render_stats(all_stats); // version var ver_tag = this.tag().id("version"); var version = new Fetcher().resource("health", "version"); version.param("detailed", "true"); version.text().then((data) => ver_tag.text(data)); this._project_table = project_table; this._cache_table = cache_table; // WebSocket for live stats updates this._connect_stats_ws(); } _connect_stats_ws() { try { const proto = location.protocol === "https:" ? "wss:" : "ws:"; const ws = new WebSocket(`${proto}//${location.host}/stats`); try { this._ws_paused = localStorage.getItem("zen-ws-paused") === "true"; } catch (e) { this._ws_paused = false; } document.addEventListener("zen-ws-toggle", (e) => { this._ws_paused = e.detail.paused; }); ws.onmessage = (ev) => { if (this._ws_paused) { return; } try { const all_stats = JSON.parse(ev.data); this._render_stats(all_stats); } catch (e) { /* ignore parse errors */ } }; ws.onclose = () => { this._stats_ws = null; }; ws.onerror = () => { ws.close(); }; this._stats_ws = ws; } catch (e) { /* WebSocket not available */ } } _render_stats(all_stats) { const grid = this._stats_grid; const safe_lookup = this._safe_lookup; // Clear existing tiles grid.inner().innerHTML = ""; // HTTP tile — aggregate request stats across all providers { const tile = grid.tag().classify("card").classify("stats-tile"); tile.tag().classify("card-title").text("HTTP"); const columns = tile.tag().classify("tile-columns"); // Left column: request stats const left = columns.tag().classify("tile-metrics"); let total_requests = 0; let total_rate = 0; for (const p in all_stats) { total_requests += (safe_lookup(all_stats[p], "requests.count") || 0); total_rate += (safe_lookup(all_stats[p], "requests.rate_1") || 0); } this._add_tile_metric(left, Friendly.sep(total_requests), "total requests", true); if (total_rate > 0) this._add_tile_metric(left, Friendly.sep(total_rate, 1) + "/s", "req/sec (1m)"); // Right column: websocket stats const ws = all_stats["http"] ? (all_stats["http"]["websockets"] || {}) : {}; const right = columns.tag().classify("tile-metrics"); this._add_tile_metric(right, Friendly.sep(ws.active_connections || 0), "ws connections", true); const ws_frames = (ws.frames_received || 0) + (ws.frames_sent || 0); if (ws_frames > 0) this._add_tile_metric(right, Friendly.sep(ws_frames), "ws frames"); const ws_bytes = (ws.bytes_received || 0) + (ws.bytes_sent || 0); if (ws_bytes > 0) this._add_tile_metric(right, Friendly.bytes(ws_bytes), "ws traffic"); tile.on_click(() => { window.location = "?page=metrics"; }); } // Cache tile (z$) if (all_stats["z$"]) { const s = all_stats["z$"]; const tile = grid.tag().classify("card").classify("stats-tile"); tile.tag().classify("card-title").text("Cache"); const body = tile.tag().classify("tile-metrics"); const hits = safe_lookup(s, "cache.hits") || 0; const misses = safe_lookup(s, "cache.misses") || 0; const ratio = (hits + misses) > 0 ? ((hits / (hits + misses)) * 100).toFixed(1) + "%" : "-"; this._add_tile_metric(body, ratio, "hit ratio", true); this._add_tile_metric(body, safe_lookup(s, "cache.size.disk", Friendly.bytes) || "-", "disk"); this._add_tile_metric(body, safe_lookup(s, "cache.size.memory", Friendly.bytes) || "-", "memory"); tile.on_click(() => { window.location = "?page=stat&provider=z$"; }); } // Project Store tile (prj) if (all_stats["prj"]) { const s = all_stats["prj"]; const tile = grid.tag().classify("card").classify("stats-tile"); tile.tag().classify("card-title").text("Project Store"); const body = tile.tag().classify("tile-metrics"); this._add_tile_metric(body, safe_lookup(s, "requests.count", Friendly.sep) || "-", "requests", true); this._add_tile_metric(body, safe_lookup(s, "store.size.disk", Friendly.bytes) || "-", "disk"); tile.on_click(() => { window.location = "?page=stat&provider=prj"; }); } // Build Store tile (builds) if (all_stats["builds"]) { const s = all_stats["builds"]; const tile = grid.tag().classify("card").classify("stats-tile"); tile.tag().classify("card-title").text("Build Store"); const body = tile.tag().classify("tile-metrics"); this._add_tile_metric(body, safe_lookup(s, "requests.count", Friendly.sep) || "-", "requests", true); this._add_tile_metric(body, safe_lookup(s, "store.size.disk", Friendly.bytes) || "-", "disk"); tile.on_click(() => { window.location = "?page=stat&provider=builds"; }); } // Proxy tile if (all_stats["proxy"]) { const s = all_stats["proxy"]; const tile = grid.tag().classify("card").classify("stats-tile"); tile.tag().classify("card-title").text("Proxy"); const body = tile.tag().classify("tile-metrics"); const mappings = s.mappings || []; let totalActive = 0; let totalBytes = 0; for (const m of mappings) { totalActive += (m.activeConnections || 0); totalBytes += (m.bytesFromClient || 0) + (m.bytesToClient || 0); } this._add_tile_metric(body, Friendly.sep(totalActive), "active connections", true); this._add_tile_metric(body, Friendly.sep(mappings.length), "mappings"); this._add_tile_metric(body, Friendly.bytes(totalBytes), "traffic"); tile.on_click(() => { window.location = "?page=proxy"; }); } // Workspace tile (ws) if (all_stats["ws"]) { const s = all_stats["ws"]; const tile = grid.tag().classify("card").classify("stats-tile"); tile.tag().classify("card-title").text("Workspace"); const body = tile.tag().classify("tile-metrics"); this._add_tile_metric(body, safe_lookup(s, "requests.count", Friendly.sep) || "-", "requests", true); this._add_tile_metric(body, safe_lookup(s, "workspaces.filescount", Friendly.sep) || "-", "files"); tile.on_click(() => { window.location = "?page=stat&provider=ws"; }); } } _add_tile_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); } view_stat(provider) { window.location = "?page=stat&provider=" + provider; } view_project(project_id) { window.location = "?page=project&project=" + project_id; } drop_project(project_id) { const drop = async () => { await new Fetcher().resource("prj", project_id).delete(); this.reload(); }; new Modal() .title("Confirmation") .message(`Drop project '${project_id}'?`) .option("Yes", () => drop()) .option("No"); } view_zcache(namespace) { window.location = "?page=zcache&namespace=" + namespace; } drop_zcache(namespace) { const drop = async () => { await new Fetcher().resource("z$", namespace).delete(); this.reload(); }; new Modal() .title("Confirmation") .message(`Drop zcache '${namespace}'?`) .option("Yes", () => drop()) .option("No"); } async drop_all_projects() { for (const row of this._project_table) { const project_id = row.attr("zs_name"); await new Fetcher().resource("prj", project_id).delete(); } this.reload(); } async drop_all_zcache() { for (const row of this._cache_table) { const namespace = row.attr("zs_name"); await new Fetcher().resource("z$", namespace).delete(); } this.reload(); } drop_all(what) { const drop = async () => { if (what == "projects") return this.drop_all_projects(); if (what == "z$") return this.drop_all_zcache(); }; new Modal() .title("Confirmation") .message(`Drop every item from '${what}'?`) .option("Yes", () => drop()) .option("No"); } }