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:
1 parent
e35da87886
commit
f4274c1ad8
16 files changed
+651
-119
No files matched your search
@@ -7,6 +7,9 @@ import {
|
||||
type DockhandContainersResponse,
|
||||
type IntegrationSummary,
|
||||
} from "../api/client";
|
||||
import { useSortable } from "../hooks/useSortable";
|
||||
import SortableTh from "../components/SortableTh";
|
||||
import { downloadCsv } from "../utils/csv";
|
||||
|
||||
function containerStateBadge(state: string) {
|
||||
switch (state) {
|
||||
@@ -73,6 +76,17 @@ export default function Docker({ user }: { user: CurrentUser }) {
|
||||
const hosts = [...new Set((data?.containers ?? []).map((c) => c.environmentName))].sort();
|
||||
const filteredContainers = (data?.containers ?? []).filter((c) => !hostFilter || c.environmentName === hostFilter);
|
||||
|
||||
const { sorted, sortKey, sortDir, requestSort } = useSortable(filteredContainers);
|
||||
|
||||
function exportCsv() {
|
||||
if (!sorted) return;
|
||||
downloadCsv(
|
||||
"docker-containers.csv",
|
||||
["Container", "Host", "Image", "State", "Status"],
|
||||
sorted.map((c) => [c.name, c.environmentName, c.image, c.state, c.status]),
|
||||
);
|
||||
}
|
||||
|
||||
async function containerAction(c: DockhandContainer, action: "start" | "stop" | "restart") {
|
||||
if (!selectedId) return;
|
||||
if (action === "stop" && !confirm(`Stop container "${c.name}"?`)) return;
|
||||
@@ -153,6 +167,9 @@ export default function Docker({ user }: { user: CurrentUser }) {
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
<button className="btn btn-sm btn-outline-secondary" onClick={exportCsv} disabled={!sorted || sorted.length === 0}>
|
||||
Export CSV
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm btn-outline-secondary"
|
||||
onClick={() => selected && loadContainers(selected.id)}
|
||||
@@ -166,15 +183,21 @@ export default function Docker({ user }: { user: CurrentUser }) {
|
||||
<table className="table table-vcenter card-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Container</th>
|
||||
<th>Host</th>
|
||||
<th>Image</th>
|
||||
<th>Status</th>
|
||||
<SortableTh<DockhandContainer> label="Container" sortKeyName="name" activeKey={sortKey} direction={sortDir} onSort={requestSort} />
|
||||
<SortableTh<DockhandContainer>
|
||||
label="Host"
|
||||
sortKeyName="environmentName"
|
||||
activeKey={sortKey}
|
||||
direction={sortDir}
|
||||
onSort={requestSort}
|
||||
/>
|
||||
<SortableTh<DockhandContainer> label="Image" sortKeyName="image" activeKey={sortKey} direction={sortDir} onSort={requestSort} />
|
||||
<SortableTh<DockhandContainer> label="Status" sortKeyName="state" activeKey={sortKey} direction={sortDir} onSort={requestSort} />
|
||||
{canEdit && <th className="w-1">Actions</th>}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredContainers.map((c) => (
|
||||
{sorted?.map((c) => (
|
||||
<tr key={`${c.environmentId}:${c.id}`}>
|
||||
<td>{c.name}</td>
|
||||
<td className="text-secondary">{c.environmentName}</td>
|
||||
@@ -217,7 +240,7 @@ export default function Docker({ user }: { user: CurrentUser }) {
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
{data && filteredContainers.length === 0 && (
|
||||
{data && sorted?.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={canEdit ? 5 : 4} className="text-secondary text-center">
|
||||
{hostFilter ? `No containers found on "${hostFilter}".` : "No containers found across any environment."}
|
||||
|
||||
Reference in new issue
Block a user