Make every table sortable and exportable to CSV

Applies the useSortable/SortableTh infrastructure introduced in the
Diagnostic Log commit to the rest of the app's tables: Audit Log,
Users, Servers (manage table), server task tables (grouped by
schedule type, on both the all-servers and per-server views), DNS
providers/zones/records, Docker containers, Gitea repos, Integrations
(manage table), IP Addresses, Proxmox guests, Secrets (already had
CSV, gained sorting), Semaphore templates, Synology volumes/disks, and
Tailscale devices.

Every column header is now click-to-sort (again on the raw field, not
its formatted display -- a byte count sorts numerically even though
the cell shows "1.2 GB", a date sorts chronologically even though the
cell shows "5d 15h 2m"), and every table got an "Export CSV" button
next to its Refresh button, exporting whatever's currently
sorted/filtered via the existing downloadCsv util.

Grouped tables (ServerTaskTable renders one sub-table per schedule
type) can't call the useSortable hook per group without breaking the
Rules of Hooks, so extracted its comparison core as a standalone
sortItems() function, driven by one shared sort-state pair at the
component's top level and applied per group.

Deliberately left two small (1-6 row) tables embedded inside stat
cards unsorted -- Proxmox's per-node storage list and ServerDetail's
agent-reported disk list -- since they're secondary detail inside an
already-scannable card, not primary list content; happy to add if
useful in practice.

Verified the shared sort core (sortItems) directly: numeric-aware
string compare (so "item2" sorts before "item10"), numbers, booleans,
and that null/undefined always sort to the end regardless of
direction.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
bobbanandClaude Sonnet 5 committed 2026-09-17 21:54:36 +02:00
1 parent e35da87886
commit f4274c1ad8
16 files changed
+651 -119

No files matched your search

+40 -7
View File
@@ -8,6 +8,9 @@ import {
type ProxmoxGuestsResponse,
type ProxmoxNodeStats,
} from "../api/client";
import { useSortable } from "../hooks/useSortable";
import SortableTh from "../components/SortableTh";
import { downloadCsv } from "../utils/csv";
function proxmoxStatusBadge(status: string) {
return status === "running" ? (
@@ -124,6 +127,17 @@ export default function Proxmox({ user }: { user: CurrentUser }) {
}
}
const { sorted: sortedGuests, sortKey: guestSortKey, sortDir: guestSortDir, requestSort: requestGuestSort } = useSortable(data?.guests);
function exportGuestsCsv() {
if (!sortedGuests) return;
downloadCsv(
"proxmox-guests.csv",
["Name", "VMID", "Node", "Type", "Uptime", "Status"],
sortedGuests.map((g) => [g.name, g.vmid, g.node, g.type === "qemu" ? "VM" : "LXC", formatUptime(g.uptime), g.status]),
);
}
return (
<>
<h2 className="page-title mb-3">Proxmox</h2>
@@ -274,6 +288,13 @@ export default function Proxmox({ user }: { user: CurrentUser }) {
)}
</h3>
<div className="card-actions">
<button
className="btn btn-sm btn-outline-secondary"
onClick={exportGuestsCsv}
disabled={!sortedGuests || sortedGuests.length === 0}
>
Export CSV
</button>
<button
className="btn btn-sm btn-outline-secondary"
onClick={() => selected && loadGuests(selected.id)}
@@ -287,16 +308,28 @@ export default function Proxmox({ user }: { user: CurrentUser }) {
<table className="table table-vcenter card-table">
<thead>
<tr>
<th>Name</th>
<th>Node</th>
<th>Type</th>
<th>Uptime</th>
<th>Status</th>
<SortableTh<ProxmoxGuest> label="Name" sortKeyName="name" activeKey={guestSortKey} direction={guestSortDir} onSort={requestGuestSort} />
<SortableTh<ProxmoxGuest> label="Node" sortKeyName="node" activeKey={guestSortKey} direction={guestSortDir} onSort={requestGuestSort} />
<SortableTh<ProxmoxGuest> label="Type" sortKeyName="type" activeKey={guestSortKey} direction={guestSortDir} onSort={requestGuestSort} />
<SortableTh<ProxmoxGuest>
label="Uptime"
sortKeyName="uptime"
activeKey={guestSortKey}
direction={guestSortDir}
onSort={requestGuestSort}
/>
<SortableTh<ProxmoxGuest>
label="Status"
sortKeyName="status"
activeKey={guestSortKey}
direction={guestSortDir}
onSort={requestGuestSort}
/>
{canEdit && <th className="w-1">Actions</th>}
</tr>
</thead>
<tbody>
{data?.guests.map((g) => (
{sortedGuests?.map((g) => (
<tr key={`${g.node}:${g.type}:${g.vmid}`}>
<td>
{g.name} <span className="text-secondary">#{g.vmid}</span>
@@ -348,7 +381,7 @@ export default function Proxmox({ user }: { user: CurrentUser }) {
)}
</tr>
))}
{data?.guests.length === 0 && (
{sortedGuests?.length === 0 && (
<tr>
<td colSpan={canEdit ? 6 : 5} className="text-secondary text-center">
No VMs or containers found across any node.