aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDan Engelbrecht <[email protected]>2026-04-15 20:58:32 +0200
committerDan Engelbrecht <[email protected]>2026-04-15 21:00:22 +0200
commit790eb8a75b4a059cb710ba2582036973d45375ab (patch)
tree92eb467b02bd58b56a6d11b1388f36235c3cae14
parent5.8.5-pre0 (diff)
downloadzen-de/dashboard-copy-button.tar.xz
zen-de/dashboard-copy-button.zip
add dashboard copy button on select information linesde/dashboard-copy-button
-rw-r--r--CHANGELOG.md1
-rw-r--r--src/zenserver/frontend/html/pages/cache.js4
-rw-r--r--src/zenserver/frontend/html/pages/compute.js13
-rw-r--r--src/zenserver/frontend/html/pages/hub.js10
-rw-r--r--src/zenserver/frontend/html/pages/orchestrator.js5
-rw-r--r--src/zenserver/frontend/html/pages/projects.js5
-rw-r--r--src/zenserver/frontend/html/pages/workspaces.js2
-rw-r--r--src/zenserver/frontend/html/util/widgets.js44
-rw-r--r--src/zenserver/frontend/html/zen.css29
9 files changed, 104 insertions, 9 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8e47dd74a..34c47155a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@
- Bugfix: `builds download` with cache upload enabled no longer holds downloaded blobs in memory when boost-worker-memory is active; blobs are written to disk before upload
- Bugfix: Removed obsolete `--cache-prime-only` flag from `builds download`
- Bugfix: Hub provision requests for already-provisioned instances now reset the inactivity timer
+- Improvement: Dashboard copy-to-clipboard buttons on hub module IDs and ports, compute worker/action IDs and queue tokens, orchestrator client IDs, cache namespace names and directories, project names and directories, and workspace IDs
## 5.8.4
- Feature: Hub bulk deprovision endpoint (`POST /hub/deprovision`) tears down all provisioned and hibernated modules in a single request
diff --git a/src/zenserver/frontend/html/pages/cache.js b/src/zenserver/frontend/html/pages/cache.js
index 58e2023f9..683f7df4f 100644
--- a/src/zenserver/frontend/html/pages/cache.js
+++ b/src/zenserver/frontend/html/pages/cache.js
@@ -6,7 +6,7 @@ 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, Pager } from "../util/widgets.js"
+import { Table, Toolbar, Pager, add_copy_button } from "../util/widgets.js"
////////////////////////////////////////////////////////////////////////////////
export class Page extends ZenPage
@@ -111,6 +111,8 @@ export class Page extends ZenPage
const cell = row.get_cell(0);
cell.tag().text(item.namespace).on_click(() => this.view_namespace(item.namespace));
+ add_copy_button(cell.inner(), item.namespace);
+ add_copy_button(row.get_cell(1).inner(), data["Configuration"]["RootDir"]);
const action_cell = row.get_cell(-1);
const action_tb = new Toolbar(action_cell, true);
diff --git a/src/zenserver/frontend/html/pages/compute.js b/src/zenserver/frontend/html/pages/compute.js
index 2eb4d4e9b..c2257029e 100644
--- a/src/zenserver/frontend/html/pages/compute.js
+++ b/src/zenserver/frontend/html/pages/compute.js
@@ -5,7 +5,7 @@
import { ZenPage } from "./page.js"
import { Fetcher } from "../util/fetcher.js"
import { Friendly } from "../util/friendly.js"
-import { Table } from "../util/widgets.js"
+import { Table, add_copy_button } from "../util/widgets.js"
const MAX_HISTORY_POINTS = 60;
@@ -352,8 +352,9 @@ export class Page extends ZenPage
id,
);
- // Worker ID column: monospace for hex readability
+ // Worker ID column: monospace for hex readability, copy button
row.get_cell(5).style("fontFamily", "'SF Mono', 'Cascadia Mono', Consolas, 'DejaVu Sans Mono', monospace");
+ add_copy_button(row.get_cell(5).inner(), id);
// Make name clickable to expand detail
const cell = row.get_cell(0);
@@ -524,7 +525,7 @@ export class Page extends ZenPage
: q.state === "draining" ? "draining"
: q.is_complete ? "complete" : "active";
- this._queues_table.add_row(
+ const qrow = this._queues_table.add_row(
id,
status,
String(q.active_count ?? 0),
@@ -534,6 +535,10 @@ export class Page extends ZenPage
String(q.cancelled_count ?? 0),
q.queue_token || "-",
);
+ if (q.queue_token)
+ {
+ add_copy_button(qrow.get_cell(7).inner(), q.queue_token);
+ }
}
}
@@ -590,7 +595,9 @@ export class Page extends ZenPage
// use monospace for readability, and show full value on hover
const mono = "'SF Mono', 'Cascadia Mono', Consolas, 'DejaVu Sans Mono', monospace";
row.get_cell(7).style("textAlign", "right").style("fontFamily", mono).attr("title", workerId);
+ if (workerId !== "-") { add_copy_button(row.get_cell(7).inner(), workerId); }
row.get_cell(8).style("textAlign", "right").style("fontFamily", mono).attr("title", actionId);
+ if (actionId !== "-") { add_copy_button(row.get_cell(8).inner(), actionId); }
}
}
diff --git a/src/zenserver/frontend/html/pages/hub.js b/src/zenserver/frontend/html/pages/hub.js
index 3cbfe6092..b2bca9324 100644
--- a/src/zenserver/frontend/html/pages/hub.js
+++ b/src/zenserver/frontend/html/pages/hub.js
@@ -6,7 +6,7 @@ import { ZenPage } from "./page.js"
import { Fetcher } from "../util/fetcher.js"
import { Friendly } from "../util/friendly.js"
import { Modal } from "../util/modal.js"
-import { flash_highlight } from "../util/widgets.js"
+import { flash_highlight, copy_button } from "../util/widgets.js"
////////////////////////////////////////////////////////////////////////////////
const STABLE_STATES = new Set(["provisioned", "hibernated", "crashed"]);
@@ -355,6 +355,7 @@ export class Page extends ZenPage
}
row.state_text.nodeValue = state;
row.port_text.nodeValue = m.port ? String(m.port) : "";
+ row.copy_port_btn.style.display = m.port ? "" : "none";
if (m.state_change_time)
{
const state_label = state.charAt(0).toUpperCase() + state.slice(1);
@@ -427,6 +428,8 @@ export class Page extends ZenPage
id_wrap.style.cssText = "display:inline-flex;align-items:center;font-family:monospace;font-size:14px;";
id_wrap.appendChild(btn_expand);
id_wrap.appendChild(document.createTextNode("\u00A0" + id));
+ const copy_id_btn = copy_button(id);
+ id_wrap.appendChild(copy_id_btn);
td_id.appendChild(id_wrap);
tr.appendChild(td_id);
@@ -448,6 +451,9 @@ export class Page extends ZenPage
td_port.style.cssText = "font-variant-numeric:tabular-nums;";
const port_node = document.createTextNode(port ? String(port) : "");
td_port.appendChild(port_node);
+ const copy_port_btn = copy_button(() => port_node.nodeValue);
+ copy_port_btn.style.display = port ? "" : "none";
+ td_port.appendChild(copy_port_btn);
tr.appendChild(td_port);
const td_action = document.createElement("td");
@@ -528,7 +534,7 @@ export class Page extends ZenPage
metrics_td.appendChild(metrics_grid);
metrics_tr.appendChild(metrics_td);
- row = { tr, metrics_tr, idx: td_idx, cb, dot, state_text: state_node, port_text: port_node, btn_expand, btn_open: btn_o, btn_hibernate: btn_h, btn_wake: btn_w, btn_deprov: btn_d, btn_oblit: btn_x, metric_nodes, state_since_node, state_age_node, state_since_label, state_age_label };
+ row = { tr, metrics_tr, idx: td_idx, cb, dot, state_text: state_node, port_text: port_node, copy_port_btn, btn_expand, btn_open: btn_o, btn_hibernate: btn_h, btn_wake: btn_w, btn_deprov: btn_d, btn_oblit: btn_x, metric_nodes, state_since_node, state_age_node, state_since_label, state_age_label };
this._row_cache.set(id, row);
}
diff --git a/src/zenserver/frontend/html/pages/orchestrator.js b/src/zenserver/frontend/html/pages/orchestrator.js
index 30f6a8122..d11306998 100644
--- a/src/zenserver/frontend/html/pages/orchestrator.js
+++ b/src/zenserver/frontend/html/pages/orchestrator.js
@@ -5,7 +5,7 @@
import { ZenPage } from "./page.js"
import { Fetcher } from "../util/fetcher.js"
import { Friendly } from "../util/friendly.js"
-import { Table } from "../util/widgets.js"
+import { Table, add_copy_button } from "../util/widgets.js"
////////////////////////////////////////////////////////////////////////////////
export class Page extends ZenPage
@@ -298,12 +298,13 @@ export class Page extends ZenPage
for (const c of clients)
{
- this._clients_table.add_row(
+ const crow = this._clients_table.add_row(
c.id || "",
c.hostname || "",
c.address || "",
this._format_last_seen(c.dt),
);
+ if (c.id) { add_copy_button(crow.get_cell(0).inner(), c.id); }
}
}
diff --git a/src/zenserver/frontend/html/pages/projects.js b/src/zenserver/frontend/html/pages/projects.js
index af7c5396a..2e76a80f1 100644
--- a/src/zenserver/frontend/html/pages/projects.js
+++ b/src/zenserver/frontend/html/pages/projects.js
@@ -6,7 +6,7 @@ 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, Pager } from "../util/widgets.js"
+import { Table, Toolbar, Pager, add_copy_button } from "../util/widgets.js"
////////////////////////////////////////////////////////////////////////////////
export class Page extends ZenPage
@@ -177,16 +177,19 @@ export class Page extends ZenPage
const cell = row.get_cell(0);
cell.tag().text(project.Id).on_click(() => this.view_project(project.Id));
+ add_copy_button(cell.inner(), project.Id);
if (project.ProjectRootDir)
{
row.get_cell(1).tag("a").text(project.ProjectRootDir)
.attr("href", "vscode://" + project.ProjectRootDir.replace(/\\/g, "/"));
+ add_copy_button(row.get_cell(1).inner(), project.ProjectRootDir);
}
if (project.EngineRootDir)
{
row.get_cell(2).tag("a").text(project.EngineRootDir)
.attr("href", "vscode://" + project.EngineRootDir.replace(/\\/g, "/"));
+ add_copy_button(row.get_cell(2).inner(), project.EngineRootDir);
}
const action_cell = row.get_cell(-1);
diff --git a/src/zenserver/frontend/html/pages/workspaces.js b/src/zenserver/frontend/html/pages/workspaces.js
index 1668e096f..db02e8be1 100644
--- a/src/zenserver/frontend/html/pages/workspaces.js
+++ b/src/zenserver/frontend/html/pages/workspaces.js
@@ -4,6 +4,7 @@
import { ZenPage } from "./page.js"
import { Fetcher } from "../util/fetcher.js"
+import { copy_button } from "../util/widgets.js"
////////////////////////////////////////////////////////////////////////////////
export class Page extends ZenPage
@@ -157,6 +158,7 @@ export class Page extends ZenPage
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);
diff --git a/src/zenserver/frontend/html/util/widgets.js b/src/zenserver/frontend/html/util/widgets.js
index b8fc720c1..651686a11 100644
--- a/src/zenserver/frontend/html/util/widgets.js
+++ b/src/zenserver/frontend/html/util/widgets.js
@@ -14,6 +14,50 @@ export function flash_highlight(element)
}
////////////////////////////////////////////////////////////////////////////////
+export function copy_button(value_or_fn)
+{
+ if (!navigator.clipboard)
+ {
+ const stub = document.createElement("span");
+ stub.style.display = "none";
+ return stub;
+ }
+
+ let reset_timer = 0;
+ const btn = document.createElement("button");
+ btn.className = "zen-copy-btn";
+ btn.title = "Copy to clipboard";
+ btn.textContent = "\u29C9";
+ btn.addEventListener("click", async (e) => {
+ e.stopPropagation();
+ const v = typeof value_or_fn === "function" ? value_or_fn() : value_or_fn;
+ if (!v) { return; }
+ try
+ {
+ await navigator.clipboard.writeText(v);
+ clearTimeout(reset_timer);
+ btn.classList.add("zen-copy-ok");
+ btn.textContent = "\u2713";
+ reset_timer = setTimeout(() => { btn.classList.remove("zen-copy-ok"); btn.textContent = "\u29C9"; }, 800);
+ }
+ catch (_e) { /* clipboard not available */ }
+ });
+ return btn;
+}
+
+// Wraps the existing children of `element` plus a copy button into an
+// inline-flex nowrap container so the button never wraps to a new line.
+export function add_copy_button(element, value_or_fn)
+{
+ if (!navigator.clipboard) { return; }
+ const wrap = document.createElement("span");
+ wrap.className = "zen-copy-wrap";
+ while (element.firstChild) { wrap.appendChild(element.firstChild); }
+ wrap.appendChild(copy_button(value_or_fn));
+ element.appendChild(wrap);
+}
+
+////////////////////////////////////////////////////////////////////////////////
class Widget extends Component
{
}
diff --git a/src/zenserver/frontend/html/zen.css b/src/zenserver/frontend/html/zen.css
index 8d4e60472..d3c6c9036 100644
--- a/src/zenserver/frontend/html/zen.css
+++ b/src/zenserver/frontend/html/zen.css
@@ -1816,6 +1816,35 @@ tr:last-child td {
color: var(--theme_bright);
}
+.zen-copy-btn {
+ background: transparent;
+ border: 1px solid var(--theme_g2);
+ border-radius: 4px;
+ color: var(--theme_g1);
+ cursor: pointer;
+ font-size: 12px;
+ line-height: 1;
+ padding: 2px 5px;
+ margin-left: 6px;
+ vertical-align: middle;
+ flex-shrink: 0;
+ transition: background 0.1s, color 0.1s;
+}
+.zen-copy-btn:hover {
+ background: var(--theme_g2);
+ color: var(--theme_bright);
+}
+.zen-copy-btn.zen-copy-ok {
+ color: var(--theme_ok);
+ border-color: var(--theme_ok);
+}
+
+.zen-copy-wrap {
+ display: inline-flex;
+ align-items: center;
+ white-space: nowrap;
+}
+
.module-metrics-row td {
padding: 6px 10px 10px 42px;
background: var(--theme_g3);