Make the network device table sortable by column
Click a header to sort by it (ascending), click again to reverse. Status defaults to known-first (its display string sorts that way naturally, no special-casing needed). IP sorts numerically by octet, not lexically. Ties fall back to IP order so the table doesn't reshuffle mid-poll for devices sharing a sort value (e.g. many unnamed unknowns). Added a Playwright test verifying IP asc/desc and Name asc against real rendered data. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,26 +1,69 @@
|
||||
import { useState } from "react";
|
||||
import type { Device } from "../api";
|
||||
|
||||
function ipSortKey(ip: string): number[] {
|
||||
return ip.split(".").map(Number);
|
||||
}
|
||||
|
||||
function compareIp(a: Device, b: Device): number {
|
||||
const [ak, bk] = [ipSortKey(a.ip), ipSortKey(b.ip)];
|
||||
for (let i = 0; i < 4; i++) if (ak[i] !== bk[i]) return ak[i] - bk[i];
|
||||
return 0;
|
||||
}
|
||||
|
||||
type SortKey = "status" | "ip" | "mac" | "name" | "lastSeen";
|
||||
|
||||
// "known" sorts before "unknown" lexically already, so treating status as its
|
||||
// display string keeps the original known-first default with no special-casing.
|
||||
const COMPARATORS: Record<SortKey, (a: Device, b: Device) => number> = {
|
||||
status: (a, b) => (a.known === b.known ? 0 : a.known ? -1 : 1),
|
||||
ip: compareIp,
|
||||
mac: (a, b) => a.mac.localeCompare(b.mac),
|
||||
name: (a, b) => (a.name ?? "").localeCompare(b.name ?? ""),
|
||||
lastSeen: (a, b) => a.lastSeen.localeCompare(b.lastSeen),
|
||||
};
|
||||
|
||||
const COLUMNS: { key: SortKey; label: string }[] = [
|
||||
{ key: "status", label: "Status" },
|
||||
{ key: "ip", label: "IP" },
|
||||
{ key: "mac", label: "MAC" },
|
||||
{ key: "name", label: "Name" },
|
||||
{ key: "lastSeen", label: "Last seen" },
|
||||
];
|
||||
|
||||
export function DeviceTable({ devices }: { devices: Device[] }) {
|
||||
const [sortKey, setSortKey] = useState<SortKey>("status");
|
||||
const [sortDir, setSortDir] = useState<"asc" | "desc">("asc");
|
||||
|
||||
function handleSort(key: SortKey) {
|
||||
if (key === sortKey) {
|
||||
setSortDir((d) => (d === "asc" ? "desc" : "asc"));
|
||||
} else {
|
||||
setSortKey(key);
|
||||
setSortDir("asc");
|
||||
}
|
||||
}
|
||||
|
||||
const sorted = [...devices].sort((a, b) => {
|
||||
if (a.known !== b.known) return a.known ? -1 : 1;
|
||||
const [ak, bk] = [ipSortKey(a.ip), ipSortKey(b.ip)];
|
||||
for (let i = 0; i < 4; i++) if (ak[i] !== bk[i]) return ak[i] - bk[i];
|
||||
return 0;
|
||||
const primary = COMPARATORS[sortKey](a, b);
|
||||
const signed = sortDir === "asc" ? primary : -primary;
|
||||
// Stable secondary key so ties (e.g. many devices with no name) don't
|
||||
// reshuffle on every poll instead of just sitting still.
|
||||
return signed !== 0 ? signed : compareIp(a, b);
|
||||
});
|
||||
|
||||
return (
|
||||
<table className="device-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<th>IP</th>
|
||||
<th>MAC</th>
|
||||
<th>Name</th>
|
||||
<th>Last seen</th>
|
||||
{COLUMNS.map(({ key, label }) => (
|
||||
<th key={key}>
|
||||
<button className="sort-header" onClick={() => handleSort(key)}>
|
||||
{label}
|
||||
{sortKey === key && <span className="sort-arrow">{sortDir === "asc" ? " ▲" : " ▼"}</span>}
|
||||
</button>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
+20
-1
@@ -197,10 +197,29 @@ body {
|
||||
text-align: left;
|
||||
color: #8b949e;
|
||||
font-weight: 500;
|
||||
padding: 0.4rem 0.6rem;
|
||||
padding: 0;
|
||||
border-bottom: 1px solid #30363d;
|
||||
}
|
||||
|
||||
.sort-header {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding: 0.4rem 0.6rem;
|
||||
background: none;
|
||||
border: none;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sort-header:hover {
|
||||
color: #e6edf3;
|
||||
}
|
||||
|
||||
.sort-arrow {
|
||||
color: #58a6ff;
|
||||
}
|
||||
|
||||
.device-table td {
|
||||
padding: 0.4rem 0.6rem;
|
||||
border-bottom: 1px solid #21262d;
|
||||
|
||||
Reference in New Issue
Block a user