diff options
| author | Stefan Boberg <[email protected]> | 2026-04-11 13:37:19 +0200 |
|---|---|---|
| committer | GitHub Enterprise <[email protected]> | 2026-04-11 13:37:19 +0200 |
| commit | b481ba4cb40e8c8e1781d1fa74b2fc5c89564a0f (patch) | |
| tree | 21860bcdc05665e2a9b2ef50b911156cbc0fe4db /src/zenserver/frontend | |
| parent | Separate action and worker chunk stores for compute service (diff) | |
| parent | hub deprovision all (#938) (diff) | |
| download | zen-sb/memory-cid-store.tar.xz zen-sb/memory-cid-store.zip | |
Merge branch 'main' into sb/memory-cid-storesb/memory-cid-store
Diffstat (limited to 'src/zenserver/frontend')
| -rw-r--r-- | src/zenserver/frontend/html/pages/cache.js | 5 | ||||
| -rw-r--r-- | src/zenserver/frontend/html/pages/hub.js | 66 | ||||
| -rw-r--r-- | src/zenserver/frontend/html/pages/projects.js | 5 | ||||
| -rw-r--r-- | src/zenserver/frontend/html/pages/start.js | 10 | ||||
| -rw-r--r-- | src/zenserver/frontend/html/util/widgets.js | 64 | ||||
| -rw-r--r-- | src/zenserver/frontend/html/zen.css | 47 |
6 files changed, 184 insertions, 13 deletions
diff --git a/src/zenserver/frontend/html/pages/cache.js b/src/zenserver/frontend/html/pages/cache.js index 93059b81c..c6567f0be 100644 --- a/src/zenserver/frontend/html/pages/cache.js +++ b/src/zenserver/frontend/html/pages/cache.js @@ -56,7 +56,8 @@ export class Page extends ZenPage this._cache_table = section.add_widget(Table, columns, Table.Flag_FitLeft|Table.Flag_PackRight|Table.Flag_AlignNumeric); - this._cache_pager = new Pager(section, 25, () => this._render_cache_page()); + this._cache_pager = new Pager(section, 25, () => this._render_cache_page(), + Pager.make_search_fn(() => this._cache_data, item => item.namespace)); const cache_drop_link = document.createElement("span"); cache_drop_link.className = "dropall zen_action"; cache_drop_link.style.position = "static"; @@ -64,6 +65,7 @@ export class Page extends ZenPage cache_drop_link.addEventListener("click", () => this.drop_all()); this._cache_pager.prepend(cache_drop_link); + const loading = Pager.loading(section); const zcache_info = await new Fetcher().resource("/z$/").json(); const namespaces = zcache_info["Namespaces"] || []; const results = await Promise.allSettled( @@ -75,6 +77,7 @@ export class Page extends ZenPage .sort((a, b) => a.namespace.localeCompare(b.namespace)); this._cache_pager.set_total(this._cache_data.length); this._render_cache_page(); + loading.remove(); // Namespace detail area (inside namespaces section so it collapses together) this._namespace_host = section; diff --git a/src/zenserver/frontend/html/pages/hub.js b/src/zenserver/frontend/html/pages/hub.js index 7ae1deb5c..3cbfe6092 100644 --- a/src/zenserver/frontend/html/pages/hub.js +++ b/src/zenserver/frontend/html/pages/hub.js @@ -6,6 +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" //////////////////////////////////////////////////////////////////////////////// const STABLE_STATES = new Set(["provisioned", "hibernated", "crashed"]); @@ -159,8 +160,36 @@ export class Page extends ZenPage this._btn_next.addEventListener("click", () => this._go_page(this._page + 1)); this._btn_provision = _make_bulk_btn("+", "Provision", () => this._show_provision_modal()); this._btn_obliterate = _make_bulk_btn("\uD83D\uDD25", "Obliterate", () => this._show_obliterate_modal()); + this._search_input = document.createElement("input"); + this._search_input.type = "text"; + this._search_input.className = "module-pager-search"; + this._search_input.placeholder = "Search module\u2026"; + this._search_input.addEventListener("keydown", (e) => + { + if (e.key === "Enter") + { + const term = this._search_input.value.trim().toLowerCase(); + if (!term) { return; } + const idx = this._modules_data.findIndex(m => + (m.moduleId || "").toLowerCase().includes(term) + ); + if (idx >= 0) + { + const id = this._modules_data[idx].moduleId; + this._navigate_to_module(id); + this._flash_module(id); + } + else + { + this._search_input.style.outline = "2px solid var(--theme_fail)"; + setTimeout(() => { this._search_input.style.outline = ""; }, 1000); + } + } + }); + pager.appendChild(this._btn_provision); pager.appendChild(this._btn_obliterate); + pager.appendChild(this._search_input); pager.appendChild(this._btn_prev); pager.appendChild(this._pager_label); pager.appendChild(this._btn_next); @@ -173,8 +202,11 @@ export class Page extends ZenPage this._row_cache = new Map(); // moduleId → row refs, for in-place DOM updates this._updating = false; this._page = 0; - this._page_size = 50; + this._page_size = 25; this._expanded = new Set(); // moduleIds with open metrics panel + this._pending_highlight = null; // moduleId to navigate+flash after next poll + this._pending_highlight_timer = null; + this._loading = mod_section.tag().classify("pager-loading").text("Loading\u2026").inner(); await this._update(); this._poll_timer = setInterval(() => this._update(), 2000); @@ -193,6 +225,15 @@ export class Page extends ZenPage this._render_capacity(stats); this._render_modules(status); + if (this._loading) { this._loading.remove(); this._loading = null; } + if (this._pending_highlight && this._module_map.has(this._pending_highlight)) + { + const id = this._pending_highlight; + this._pending_highlight = null; + clearTimeout(this._pending_highlight_timer); + this._navigate_to_module(id); + this._flash_module(id); + } } catch (e) { /* service unavailable */ } finally { this._updating = false; } @@ -844,14 +885,19 @@ export class Page extends ZenPage submit_label: "Provision", on_submit: async (moduleId) => { const resp = await fetch(`/hub/modules/${encodeURIComponent(moduleId)}/provision`, { method: "POST" }); - if (resp.ok) + if (!resp.ok) { - this._navigate_to_module(moduleId); - return true; + const msg = await resp.text(); + error_div.textContent = msg || ("HTTP " + resp.status); + return false; } - const msg = await resp.text(); - error_div.textContent = msg || ("HTTP " + resp.status); - return false; + // Endpoint returns compact binary (CbObjectWriter), not text + if (resp.status === 200 || resp.status === 202) + { + this._pending_highlight = moduleId; + this._pending_highlight_timer = setTimeout(() => { this._pending_highlight = null; }, 5000); + } + return true; } }); } @@ -885,4 +931,10 @@ export class Page extends ZenPage } } + _flash_module(id) + { + const cached = this._row_cache.get(id); + if (cached) { flash_highlight(cached.tr); } + } + } diff --git a/src/zenserver/frontend/html/pages/projects.js b/src/zenserver/frontend/html/pages/projects.js index 52d5dbb88..e613086a9 100644 --- a/src/zenserver/frontend/html/pages/projects.js +++ b/src/zenserver/frontend/html/pages/projects.js @@ -49,7 +49,8 @@ export class Page extends ZenPage this._project_table = section.add_widget(Table, columns, Table.Flag_FitLeft|Table.Flag_PackRight|Table.Flag_Sortable|Table.Flag_AlignNumeric); - this._project_pager = new Pager(section, 25, () => this._render_projects_page()); + this._project_pager = new Pager(section, 25, () => this._render_projects_page(), + Pager.make_search_fn(() => this._projects_data, p => p.Id)); const drop_link = document.createElement("span"); drop_link.className = "dropall zen_action"; drop_link.style.position = "static"; @@ -57,10 +58,12 @@ export class Page extends ZenPage drop_link.addEventListener("click", () => this.drop_all()); this._project_pager.prepend(drop_link); + const loading = Pager.loading(section); this._projects_data = await new Fetcher().resource("/prj/list").json(); this._projects_data.sort((a, b) => a.Id.localeCompare(b.Id)); this._project_pager.set_total(this._projects_data.length); this._render_projects_page(); + loading.remove(); // Project detail area (inside projects section so it collapses together) this._project_host = section; diff --git a/src/zenserver/frontend/html/pages/start.js b/src/zenserver/frontend/html/pages/start.js index 14ec4bd4a..9a3eb6de3 100644 --- a/src/zenserver/frontend/html/pages/start.js +++ b/src/zenserver/frontend/html/pages/start.js @@ -62,7 +62,8 @@ export class Page extends ZenPage ]; this._project_table = section.add_widget(Table, columns); - this._project_pager = new Pager(section, 25, () => this._render_projects_page()); + this._project_pager = new Pager(section, 25, () => this._render_projects_page(), + Pager.make_search_fn(() => this._projects_data, p => p.Id)); const drop_link = document.createElement("span"); drop_link.className = "dropall zen_action"; drop_link.style.position = "static"; @@ -70,10 +71,12 @@ export class Page extends ZenPage drop_link.addEventListener("click", () => this.drop_all("projects")); this._project_pager.prepend(drop_link); + const prj_loading = Pager.loading(section); this._projects_data = await new Fetcher().resource("/prj/list").json(); this._projects_data.sort((a, b) => a.Id.localeCompare(b.Id)); this._project_pager.set_total(this._projects_data.length); this._render_projects_page(); + prj_loading.remove(); } // cache @@ -92,7 +95,8 @@ export class Page extends ZenPage ]; this._cache_table = section.add_widget(Table, columns, Table.Flag_FitLeft|Table.Flag_PackRight); - this._cache_pager = new Pager(section, 25, () => this._render_cache_page()); + this._cache_pager = new Pager(section, 25, () => this._render_cache_page(), + Pager.make_search_fn(() => this._cache_data, item => item.namespace)); const cache_drop_link = document.createElement("span"); cache_drop_link.className = "dropall zen_action"; cache_drop_link.style.position = "static"; @@ -100,6 +104,7 @@ export class Page extends ZenPage cache_drop_link.addEventListener("click", () => this.drop_all("z$")); this._cache_pager.prepend(cache_drop_link); + const cache_loading = Pager.loading(section); const zcache_info = await new Fetcher().resource("/z$/").json(); const namespaces = zcache_info["Namespaces"] || []; const results = await Promise.allSettled( @@ -111,6 +116,7 @@ export class Page extends ZenPage .sort((a, b) => a.namespace.localeCompare(b.namespace)); this._cache_pager.set_total(this._cache_data.length); this._render_cache_page(); + cache_loading.remove(); } // version diff --git a/src/zenserver/frontend/html/util/widgets.js b/src/zenserver/frontend/html/util/widgets.js index 33d6755ac..b8fc720c1 100644 --- a/src/zenserver/frontend/html/util/widgets.js +++ b/src/zenserver/frontend/html/util/widgets.js @@ -6,6 +6,14 @@ import { Component } from "./component.js" import { Friendly } from "../util/friendly.js" //////////////////////////////////////////////////////////////////////////////// +export function flash_highlight(element) +{ + if (!element) { return; } + element.classList.add("pager-search-highlight"); + setTimeout(() => { element.classList.remove("pager-search-highlight"); }, 1500); +} + +//////////////////////////////////////////////////////////////////////////////// class Widget extends Component { } @@ -404,12 +412,14 @@ export class ProgressBar extends Widget //////////////////////////////////////////////////////////////////////////////// export class Pager { - constructor(section, page_size, on_change) + constructor(section, page_size, on_change, search_fn) { this._page = 0; this._page_size = page_size; this._total = 0; this._on_change = on_change; + this._search_fn = search_fn || null; + this._search_input = null; const pager = section.tag().classify("module-pager").inner(); this._btn_prev = document.createElement("button"); @@ -422,6 +432,23 @@ export class Pager this._btn_next.className = "module-pager-btn"; this._btn_next.textContent = "Next \u2192"; this._btn_next.addEventListener("click", () => this._go_page(this._page + 1)); + + if (this._search_fn) + { + this._search_input = document.createElement("input"); + this._search_input.type = "text"; + this._search_input.className = "module-pager-search"; + this._search_input.placeholder = "Search\u2026"; + this._search_input.addEventListener("keydown", (e) => + { + if (e.key === "Enter") + { + this._do_search(this._search_input.value.trim()); + } + }); + pager.appendChild(this._search_input); + } + pager.appendChild(this._btn_prev); pager.appendChild(this._label); pager.appendChild(this._btn_next); @@ -432,7 +459,8 @@ export class Pager prepend(element) { - this._pager.insertBefore(element, this._btn_prev); + const ref = this._search_input || this._btn_prev; + this._pager.insertBefore(element, ref); } set_total(n) @@ -461,6 +489,23 @@ export class Pager this._on_change(); } + _do_search(term) + { + if (!term || !this._search_fn) + { + return; + } + const result = this._search_fn(term); + if (!result) + { + this._search_input.style.outline = "2px solid var(--theme_fail)"; + setTimeout(() => { this._search_input.style.outline = ""; }, 1000); + return; + } + this._go_page(Math.floor(result.index / this._page_size)); + flash_highlight(this._pager.parentNode.querySelector(`[zs_name="${CSS.escape(result.name)}"]`)); + } + _update_ui() { const total = this._total; @@ -474,6 +519,21 @@ export class Pager ? "No items" : `${start}\u2013${end} of ${total}`; } + + static make_search_fn(get_data, get_key) + { + return (term) => { + const t = term.toLowerCase(); + const data = get_data(); + const i = data.findIndex(item => get_key(item).toLowerCase().includes(t)); + return i < 0 ? null : { index: i, name: get_key(data[i]) }; + }; + } + + static loading(section) + { + return section.tag().classify("pager-loading").text("Loading\u2026").inner(); + } } diff --git a/src/zenserver/frontend/html/zen.css b/src/zenserver/frontend/html/zen.css index ca577675b..8d4e60472 100644 --- a/src/zenserver/frontend/html/zen.css +++ b/src/zenserver/frontend/html/zen.css @@ -1749,6 +1749,53 @@ tr:last-child td { text-align: center; } +.module-pager-search { + font-size: 12px; + padding: 4px 8px; + width: 14em; + border: 1px solid var(--theme_g2); + border-radius: 4px; + background: var(--theme_g4); + color: var(--theme_g0); + outline: none; + transition: border-color 0.15s, outline 0.3s; +} + +.module-pager-search:focus { + border-color: var(--theme_p0); +} + +.module-pager-search::placeholder { + color: var(--theme_g1); +} + +@keyframes pager-search-flash { + from { box-shadow: inset 0 0 0 100px var(--theme_p2); } + to { box-shadow: inset 0 0 0 100px transparent; } +} + +.zen_table > .pager-search-highlight > div { + animation: pager-search-flash 1s linear forwards; +} + +.module-table .pager-search-highlight td { + animation: pager-search-flash 1s linear forwards; +} + +@keyframes pager-loading-pulse { + 0%, 100% { opacity: 0.6; } + 50% { opacity: 0.2; } +} + +.pager-loading { + color: var(--theme_g1); + font-style: italic; + font-size: 14px; + font-weight: 600; + padding: 12px 0; + animation: pager-loading-pulse 1.5s ease-in-out infinite; +} + .module-table td, .module-table th { padding-top: 4px; padding-bottom: 4px; |