v2.5.0: select key traffic by row click

This commit is contained in:
Виталий Литвинов
2026-04-25 14:56:52 +03:00
parent 817ea9ab9f
commit bd3fc1af18
3 changed files with 79 additions and 36 deletions

View File

@@ -978,6 +978,26 @@ function ensureUserTrafficSelection() {
state.userTrafficUser = state.users[0]?.name || ""; 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() { function userTrafficRows() {
return state.userTraffic?.history || []; return state.userTraffic?.history || [];
} }
@@ -1114,11 +1134,12 @@ function renderUsers() {
} }
tbody.innerHTML = state.users.map((user) => { tbody.innerHTML = state.users.map((user) => {
const pending = state.pendingUsers.has(user.name); const pending = state.pendingUsers.has(user.name);
const selected = user.name === state.userTrafficUser;
const traffic = user.traffic || {}; const traffic = user.traffic || {};
const trafficTotal = Number(traffic.total_octets) ? fmtBytes(traffic.total_octets) : "--"; const trafficTotal = Number(traffic.total_octets) ? fmtBytes(traffic.total_octets) : "--";
const activeIps = Number(traffic.active_unique_ips) || 0; const activeIps = Number(traffic.active_unique_ips) || 0;
return ` return `
<tr class="${user.enabled ? "" : "disabled-row"} ${pending ? "pending-row" : ""}"> <tr class="${user.enabled ? "" : "disabled-row"} ${pending ? "pending-row" : ""} ${selected ? "selected-row" : ""}" data-select-user-traffic="${escapeAttr(user.name)}" aria-selected="${selected ? "true" : "false"}">
<td data-label="${escapeAttr(t("tableUser"))}"> <td data-label="${escapeAttr(t("tableUser"))}">
<strong>${escapeHtml(user.name)}</strong>${user.main ? ` <small>${escapeHtml(t("main"))}</small>` : ""} <strong>${escapeHtml(user.name)}</strong>${user.main ? ` <small>${escapeHtml(t("main"))}</small>` : ""}
</td> </td>
@@ -1227,6 +1248,7 @@ async function refreshAll() {
state.backupSchedule = state.overview.backup_schedule || state.backupSchedule; state.backupSchedule = state.overview.backup_schedule || state.backupSchedule;
updateLanguageFromOverview(state.overview); updateLanguageFromOverview(state.overview);
state.users = await api("/api/users"); state.users = await api("/api/users");
ensureUserTrafficSelection();
if (!state.stats) { if (!state.stats) {
state.stats = { state.stats = {
current: state.overview.stats_current || {}, current: state.overview.stats_current || {},
@@ -1532,41 +1554,45 @@ document.addEventListener("click", async (eventObj) => {
} }
const button = eventObj.target.closest("button"); const button = eventObj.target.closest("button");
if (!button) return; if (button) {
if (button.id === "themeToggle") {
if (button.id === "themeToggle") { setTheme(state.theme === "dark" ? "light" : "dark");
setTheme(state.theme === "dark" ? "light" : "dark"); } else if (button.id === "menuBtn") {
} else if (button.id === "menuBtn") { $("#sidebar").classList.toggle("open");
$("#sidebar").classList.toggle("open"); } else if (button.dataset.trafficRange) {
} else if (button.dataset.trafficRange) { changeTrafficRange(button.dataset.trafficRange);
changeTrafficRange(button.dataset.trafficRange); } else if (button.dataset.trafficView) {
} else if (button.dataset.trafficView) { state.trafficView = button.dataset.trafficView === "table" ? "table" : "chart";
state.trafficView = button.dataset.trafficView === "table" ? "table" : "chart"; renderStats();
renderStats(); } else if (button.dataset.userTraffic) {
} else if (button.dataset.userTraffic) { selectUserTraffic(button.dataset.userTraffic, { scroll: true });
state.userTrafficUser = button.dataset.userTraffic; } else if (button.dataset.userQr) {
refreshUserTraffic({ showLoading: true }).catch((err) => toast(err.message)); showUserQr(button.dataset.userQr);
} else if (button.dataset.userQr) { } else if (button.dataset.userTrafficRange) {
showUserQr(button.dataset.userQr); changeUserTrafficRange(button.dataset.userTrafficRange);
} else if (button.dataset.userTrafficRange) { } else if (button.dataset.userTrafficView) {
changeUserTrafficRange(button.dataset.userTrafficRange); state.userTrafficView = button.dataset.userTrafficView === "table" ? "table" : "chart";
} else if (button.dataset.userTrafficView) { renderUserTraffic();
state.userTrafficView = button.dataset.userTrafficView === "table" ? "table" : "chart"; } else if (button.dataset.backupSchedule) {
renderUserTraffic(); setBackupSchedule(button.dataset.backupSchedule);
} else if (button.dataset.backupSchedule) { } else if (button.dataset.restoreBackup) {
setBackupSchedule(button.dataset.backupSchedule); const name = button.dataset.restoreBackup;
} else if (button.dataset.restoreBackup) { if (confirm(`${t("confirmRestoreBackup")} ${name}?`)) restoreBackup(name).catch((err) => toast(err.message));
const name = button.dataset.restoreBackup; } else if (button.dataset.copy) {
if (confirm(`${t("confirmRestoreBackup")} ${name}?`)) restoreBackup(name).catch((err) => toast(err.message)); await copyText(button.dataset.copy);
} else if (button.dataset.copy) { } else if (button.dataset.delete) {
await copyText(button.dataset.copy); const name = button.dataset.delete;
} else if (button.dataset.delete) { if (confirm(`${t("confirmDelete")} ${name}?`)) deleteUser(name).catch((err) => toast(err.message));
const name = button.dataset.delete; } else if (button.dataset.restart) {
if (confirm(`${t("confirmDelete")} ${name}?`)) deleteUser(name).catch((err) => toast(err.message)); const name = button.dataset.restart;
} else if (button.dataset.restart) { if (confirm(`${t("confirmRestart")} ${name}?`)) restartService(name).catch((err) => toast(err.message));
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) => { document.addEventListener("change", (eventObj) => {

View File

@@ -400,6 +400,6 @@
</div> </div>
</div> </div>
</div> </div>
<script src="/app.js?v=2.5.0-admin10" type="module"></script> <script src="/app.js?v=2.5.0-admin11" type="module"></script>
</body> </body>
</html> </html>

View File

@@ -808,6 +808,23 @@ tr:last-child td {
border-bottom: 0; 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 { .disabled-row {
opacity: .72; opacity: .72;
} }