From bd3fc1af18f038a94cd13f1e15e0c96ab4f80f64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D1=82=D0=B0=D0=BB=D0=B8=D0=B9=20=D0=9B=D0=B8?= =?UTF-8?q?=D1=82=D0=B2=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Sat, 25 Apr 2026 14:56:52 +0300 Subject: [PATCH] v2.5.0: select key traffic by row click --- admin-web/static/app.js | 96 +++++++++++++++++++++++-------------- admin-web/static/index.html | 2 +- admin-web/static/styles.css | 17 +++++++ 3 files changed, 79 insertions(+), 36 deletions(-) diff --git a/admin-web/static/app.js b/admin-web/static/app.js index f45be2d..0b1e79d 100644 --- a/admin-web/static/app.js +++ b/admin-web/static/app.js @@ -978,6 +978,26 @@ function ensureUserTrafficSelection() { state.userTrafficUser = state.users[0]?.name || ""; } +async function selectUserTraffic(name, options = {}) { + const next = String(name || ""); + if (!next || !state.users.some((user) => user.name === next)) return; + const changed = state.userTrafficUser !== next; + state.userTrafficUser = next; + if (changed) { + state.userTraffic = null; + } + renderUsers(); + renderUserTraffic(); + if (options.scroll) { + $("#userTrafficPanel")?.scrollIntoView({ behavior: "smooth", block: "start" }); + } + try { + await refreshUserTraffic({ showLoading: true }); + } catch (err) { + toast(err.message); + } +} + function userTrafficRows() { return state.userTraffic?.history || []; } @@ -1114,11 +1134,12 @@ function renderUsers() { } tbody.innerHTML = state.users.map((user) => { const pending = state.pendingUsers.has(user.name); + const selected = user.name === state.userTrafficUser; const traffic = user.traffic || {}; const trafficTotal = Number(traffic.total_octets) ? fmtBytes(traffic.total_octets) : "--"; const activeIps = Number(traffic.active_unique_ips) || 0; return ` - + ${escapeHtml(user.name)}${user.main ? ` ${escapeHtml(t("main"))}` : ""} @@ -1227,6 +1248,7 @@ async function refreshAll() { state.backupSchedule = state.overview.backup_schedule || state.backupSchedule; updateLanguageFromOverview(state.overview); state.users = await api("/api/users"); + ensureUserTrafficSelection(); if (!state.stats) { state.stats = { current: state.overview.stats_current || {}, @@ -1532,41 +1554,45 @@ document.addEventListener("click", async (eventObj) => { } const button = eventObj.target.closest("button"); - if (!button) return; - - if (button.id === "themeToggle") { - setTheme(state.theme === "dark" ? "light" : "dark"); - } else if (button.id === "menuBtn") { - $("#sidebar").classList.toggle("open"); - } else if (button.dataset.trafficRange) { - changeTrafficRange(button.dataset.trafficRange); - } else if (button.dataset.trafficView) { - state.trafficView = button.dataset.trafficView === "table" ? "table" : "chart"; - renderStats(); - } else if (button.dataset.userTraffic) { - state.userTrafficUser = button.dataset.userTraffic; - refreshUserTraffic({ showLoading: true }).catch((err) => toast(err.message)); - } else if (button.dataset.userQr) { - showUserQr(button.dataset.userQr); - } else if (button.dataset.userTrafficRange) { - changeUserTrafficRange(button.dataset.userTrafficRange); - } else if (button.dataset.userTrafficView) { - state.userTrafficView = button.dataset.userTrafficView === "table" ? "table" : "chart"; - renderUserTraffic(); - } else if (button.dataset.backupSchedule) { - setBackupSchedule(button.dataset.backupSchedule); - } else if (button.dataset.restoreBackup) { - const name = button.dataset.restoreBackup; - if (confirm(`${t("confirmRestoreBackup")} ${name}?`)) restoreBackup(name).catch((err) => toast(err.message)); - } else if (button.dataset.copy) { - await copyText(button.dataset.copy); - } else if (button.dataset.delete) { - const name = button.dataset.delete; - if (confirm(`${t("confirmDelete")} ${name}?`)) deleteUser(name).catch((err) => toast(err.message)); - } else if (button.dataset.restart) { - const name = button.dataset.restart; - if (confirm(`${t("confirmRestart")} ${name}?`)) restartService(name).catch((err) => toast(err.message)); + if (button) { + if (button.id === "themeToggle") { + setTheme(state.theme === "dark" ? "light" : "dark"); + } else if (button.id === "menuBtn") { + $("#sidebar").classList.toggle("open"); + } else if (button.dataset.trafficRange) { + changeTrafficRange(button.dataset.trafficRange); + } else if (button.dataset.trafficView) { + state.trafficView = button.dataset.trafficView === "table" ? "table" : "chart"; + renderStats(); + } else if (button.dataset.userTraffic) { + selectUserTraffic(button.dataset.userTraffic, { scroll: true }); + } else if (button.dataset.userQr) { + showUserQr(button.dataset.userQr); + } else if (button.dataset.userTrafficRange) { + changeUserTrafficRange(button.dataset.userTrafficRange); + } else if (button.dataset.userTrafficView) { + state.userTrafficView = button.dataset.userTrafficView === "table" ? "table" : "chart"; + renderUserTraffic(); + } else if (button.dataset.backupSchedule) { + setBackupSchedule(button.dataset.backupSchedule); + } else if (button.dataset.restoreBackup) { + const name = button.dataset.restoreBackup; + if (confirm(`${t("confirmRestoreBackup")} ${name}?`)) restoreBackup(name).catch((err) => toast(err.message)); + } else if (button.dataset.copy) { + await copyText(button.dataset.copy); + } else if (button.dataset.delete) { + const name = button.dataset.delete; + if (confirm(`${t("confirmDelete")} ${name}?`)) deleteUser(name).catch((err) => toast(err.message)); + } else if (button.dataset.restart) { + const name = button.dataset.restart; + if (confirm(`${t("confirmRestart")} ${name}?`)) restartService(name).catch((err) => toast(err.message)); + } + return; } + + const row = eventObj.target.closest("[data-select-user-traffic]"); + if (!row) return; + selectUserTraffic(row.dataset.selectUserTraffic, { scroll: true }); }); document.addEventListener("change", (eventObj) => { diff --git a/admin-web/static/index.html b/admin-web/static/index.html index 12f9e5c..02ed21b 100644 --- a/admin-web/static/index.html +++ b/admin-web/static/index.html @@ -400,6 +400,6 @@ - + diff --git a/admin-web/static/styles.css b/admin-web/static/styles.css index d70c0f2..26c76c9 100644 --- a/admin-web/static/styles.css +++ b/admin-web/static/styles.css @@ -808,6 +808,23 @@ tr:last-child td { border-bottom: 0; } +tr[data-select-user-traffic] { + cursor: pointer; + transition: background .16s ease, box-shadow .16s ease; +} + +tr[data-select-user-traffic]:hover td { + background: color-mix(in srgb, var(--blue) 7%, transparent); +} + +tr.selected-row td { + background: color-mix(in srgb, var(--blue) 10%, var(--panel)); +} + +tr.selected-row td:first-child { + box-shadow: inset 4px 0 0 var(--blue); +} + .disabled-row { opacity: .72; }