fix user list
This commit is contained in:
@@ -750,3 +750,10 @@ select:disabled {
|
||||
@media(max-width:1180px){.bot-overview-grid{grid-template-columns:repeat(2,minmax(0,1fr));}.bot-section-nav{grid-template-columns:repeat(3,minmax(0,1fr));}.bot-master-detail{grid-template-columns:1fr;}.bot-editor-card{position:static;}.bot-nav-shell{top:78px;}}
|
||||
@media(max-width:760px){.bot-hero{padding:20px;border-radius:22px;}.bot-hero h2{font-size:1.55rem;}.bot-hero-actions{position:relative;right:auto;top:auto;max-width:none;justify-content:flex-start;margin-top:16px;}.bot-overview-grid{grid-template-columns:1fr 1fr;margin-top:18px;}.bot-section-nav{display:none;}.bot-section-select{display:block;}.bot-nav-shell{top:76px;}.bot-config-grid,.bot-message-grid{grid-template-columns:1fr;}.bot-section-heading{align-items:flex-start;flex-direction:column;}.bot-section-heading>.card-actions{width:100%;justify-content:flex-start;}.bot-master-detail{display:block;}.bot-master-detail>.card+.card{margin-top:14px!important;}.bot-save-row{align-items:flex-start;flex-direction:column;}}
|
||||
@media(max-width:460px){.bot-overview-grid{grid-template-columns:1fr;}.bot-overview-card{padding:11px 12px;}.bot-hero-actions .btn{width:100%;}.bot-section-heading .btn{width:100%;}.bot-copy-row{align-items:stretch;flex-direction:column;}.bot-copy-row .btn{width:100%;}}
|
||||
|
||||
/* Sortable SSH user table headers */
|
||||
th[data-sort-key]{cursor:pointer;user-select:none;white-space:nowrap;transition:color .12s ease;}
|
||||
th[data-sort-key]:hover{color:var(--accent);}
|
||||
th[data-sort-key]::after{content:"";display:inline-block;width:.9em;font-size:.72em;opacity:.85;}
|
||||
th[data-sort-key].sort-asc::after{content:" \25B2";}
|
||||
th[data-sort-key].sort-desc::after{content:" \25BC";}
|
||||
|
||||
@@ -57,7 +57,77 @@ async function loadUsersSilent() {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Column sorting (click a header to sort) ----
|
||||
// Value extractor per sortable column. Numbers sort numerically, strings
|
||||
// alphabetically; online counts as 1 so "status" groups online users together.
|
||||
const USER_SORT_EXTRACT = {
|
||||
username: u => String(u.username || "").toLowerCase(),
|
||||
status: u => (u.active_conns || 0) > 0 ? 1 : 0,
|
||||
auth: u => u.use_pam ? "pam" : (u.totp_enabled ? (u.allow_static_password ? "totp+pw" : "totp") : "password"),
|
||||
conn: u => u.active_conns || 0,
|
||||
max: u => u.max_connections || 0,
|
||||
up: u => u.limit_mbps_up || 0,
|
||||
down: u => u.limit_mbps_down || 0,
|
||||
expires: u => u.expires_at ? new Date(u.expires_at).getTime() : Infinity,
|
||||
owner: u => String(u.owner_username || "").toLowerCase(),
|
||||
};
|
||||
// Columns that default to descending on first click (most/online first).
|
||||
const USER_SORT_DEFAULT_DESC = new Set(["status", "conn", "max", "up", "down"]);
|
||||
|
||||
let userSort = { key: "username", dir: "asc" };
|
||||
let lastUsersData = [];
|
||||
|
||||
function sortUsers(list) {
|
||||
const ext = USER_SORT_EXTRACT[userSort.key] || USER_SORT_EXTRACT.username;
|
||||
const dir = userSort.dir === "desc" ? -1 : 1;
|
||||
return list.slice().sort((a, b) => {
|
||||
const va = ext(a), vb = ext(b);
|
||||
let cmp;
|
||||
if (typeof va === "number" && typeof vb === "number") cmp = va - vb;
|
||||
else cmp = String(va).localeCompare(String(vb));
|
||||
// Stable tie-break by username so equal rows never shuffle between polls.
|
||||
if (cmp === 0) cmp = String(a.username || "").localeCompare(String(b.username || ""));
|
||||
return cmp * dir;
|
||||
});
|
||||
}
|
||||
|
||||
function updateSortIndicators() {
|
||||
const table = usersBody && usersBody.closest("table");
|
||||
if (!table) return;
|
||||
table.querySelectorAll("th[data-sort-key]").forEach(th => {
|
||||
th.classList.remove("sort-asc", "sort-desc");
|
||||
if (th.getAttribute("data-sort-key") === userSort.key) {
|
||||
th.classList.add(userSort.dir === "asc" ? "sort-asc" : "sort-desc");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setUserSort(key) {
|
||||
if (!USER_SORT_EXTRACT[key]) return;
|
||||
if (userSort.key === key) {
|
||||
userSort.dir = userSort.dir === "asc" ? "desc" : "asc";
|
||||
} else {
|
||||
userSort.key = key;
|
||||
userSort.dir = USER_SORT_DEFAULT_DESC.has(key) ? "desc" : "asc";
|
||||
}
|
||||
updateSortIndicators();
|
||||
renderUsers(lastUsersData);
|
||||
}
|
||||
|
||||
(function initUserSortHeaders() {
|
||||
const table = usersBody && usersBody.closest("table");
|
||||
if (!table) return;
|
||||
table.querySelectorAll("th[data-sort-key]").forEach(th => {
|
||||
th.addEventListener("click", () => setUserSort(th.getAttribute("data-sort-key")));
|
||||
});
|
||||
updateSortIndicators();
|
||||
})();
|
||||
|
||||
function renderUsers(users) {
|
||||
// Cache the raw list so a header click can re-sort without refetching, and
|
||||
// order by the active column so rows don't shuffle on each live poll.
|
||||
lastUsersData = users || [];
|
||||
users = sortUsers(lastUsersData);
|
||||
updateDashboardFromUsers(users);
|
||||
const isSA = currentRole === "superadmin";
|
||||
userCountChip.textContent = users.length;
|
||||
|
||||
+3
-3
@@ -272,9 +272,9 @@
|
||||
<div class="tbl-wrap">
|
||||
<table>
|
||||
<thead><tr>
|
||||
<th>User</th><th>Status</th><th>Auth</th>
|
||||
<th>Conn</th><th>Max</th><th>Up</th><th>Dn</th><th>Expires</th>
|
||||
<th id="ownerColHead" class="superadmin-only hidden">Owner</th>
|
||||
<th data-sort-key="username">User</th><th data-sort-key="status">Status</th><th data-sort-key="auth">Auth</th>
|
||||
<th data-sort-key="conn">Conn</th><th data-sort-key="max">Max</th><th data-sort-key="up">Up</th><th data-sort-key="down">Dn</th><th data-sort-key="expires">Expires</th>
|
||||
<th id="ownerColHead" data-sort-key="owner" class="superadmin-only hidden">Owner</th>
|
||||
<th>Actions</th>
|
||||
</tr></thead>
|
||||
<tbody id="usersBody"></tbody>
|
||||
|
||||
Reference in New Issue
Block a user