feat(customers): sortable table columns + default ascending ID order

- Customer list now defaults to ascending ID order instead of newest-first,
  so the table starts at customer #1 instead of the highest ID
- Add sort_by/sort_order query params to GET /customers (whitelisted column
  map to prevent SQL injection via arbitrary column names)
- Make ID/Name/Subdomain/Status/Devices/Created column headers clickable,
  toggling asc/desc with a visual arrow indicator

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
2026-07-23 15:16:31 +02:00
co-authored by Claude Sonnet 5
parent a5988af6a3
commit 0e2b292408
5 changed files with 74 additions and 10 deletions
+28 -1
View File
@@ -12,6 +12,8 @@ let currentPage = 'dashboard';
let currentCustomerId = null;
let currentCustomerData = null;
let customersPage = 1;
let customersSortBy = 'id';
let customersSortOrder = 'asc';
let brandingData = { branding_name: 'NetBird MSP Appliance', branding_logo_path: null, version: 'alpha-1.1' };
let azureConfig = { azure_enabled: false };
@@ -458,7 +460,7 @@ async function loadStats() {
async function loadCustomers() {
const search = document.getElementById('search-input').value;
const status = document.getElementById('status-filter').value;
let url = `/customers?page=${customersPage}&per_page=25`;
let url = `/customers?page=${customersPage}&per_page=25&sort_by=${customersSortBy}&sort_order=${customersSortOrder}`;
if (search) url += `&search=${encodeURIComponent(search)}`;
if (status) url += `&status=${encodeURIComponent(status)}`;
@@ -470,7 +472,32 @@ async function loadCustomers() {
}
}
function setCustomerSort(column) {
if (customersSortBy === column) {
customersSortOrder = customersSortOrder === 'asc' ? 'desc' : 'asc';
} else {
customersSortBy = column;
customersSortOrder = 'asc';
}
customersPage = 1;
loadCustomers();
}
function updateSortHeaders() {
document.querySelectorAll('.sortable-th').forEach(th => {
const col = th.getAttribute('data-sort-col');
const icon = th.querySelector('.sort-icon');
th.classList.remove('sort-asc', 'sort-desc');
if (icon) icon.className = 'bi bi-arrow-down-up sort-icon ms-1';
if (col === customersSortBy) {
th.classList.add(customersSortOrder === 'asc' ? 'sort-asc' : 'sort-desc');
if (icon) icon.className = `bi bi-arrow-${customersSortOrder === 'asc' ? 'up' : 'down'} sort-icon ms-1`;
}
});
}
function renderCustomersTable(data) {
updateSortHeaders();
const tbody = document.getElementById('customers-table-body');
if (!data.items || data.items.length === 0) {
tbody.innerHTML = `<tr><td colspan="8" class="text-center text-muted py-4">${t('dashboard.noCustomers')}</td></tr>`;