From e5f9da6c26104236af706089848aed8864ba9ff3 Mon Sep 17 00:00:00 2001 From: robbond Date: Fri, 6 Mar 2026 06:58:29 +0000 Subject: [PATCH] feat(ui): add sortable column headers to domains table --- components/dashboard/dashboard-controls.tsx | 256 +++++++++++++++++++- 1 file changed, 247 insertions(+), 9 deletions(-) diff --git a/components/dashboard/dashboard-controls.tsx b/components/dashboard/dashboard-controls.tsx index 9c2c104..6192686 100644 --- a/components/dashboard/dashboard-controls.tsx +++ b/components/dashboard/dashboard-controls.tsx @@ -55,6 +55,15 @@ type Domain = { updated_at: string; }; +type DomainSortKey = + | "domain" + | "status" + | "hosting" + | "created" + | "aliases" + | "last_seen" + | "actions"; + type Props = { instances: Instance[]; subscriptions: Subscription[]; @@ -198,6 +207,10 @@ export function DashboardControls({ ); const [domainSearch, setDomainSearch] = useState(""); const [domainStatus, setDomainStatus] = useState("all"); + const [domainSort, setDomainSort] = useState<{ + key: DomainSortKey; + direction: "asc" | "desc"; + }>({ key: "domain", direction: "asc" }); const [message, setMessage] = useState(null); const [loading, setLoading] = useState(false); const [isConnectPanelOpen, setIsConnectPanelOpen] = useState(!hasInstances); @@ -295,6 +308,112 @@ export function DashboardControls({ return statusMatches && searchMatches; }); + const sortedDomains = filteredDomains + .map((domain, index) => ({ domain, index })) + .sort((a, b) => { + function compareNullable( + left: T | null | undefined, + right: T | null | undefined, + compare: (leftValue: T, rightValue: T) => number, + ) { + const leftMissing = left == null; + const rightMissing = right == null; + if (leftMissing && rightMissing) return 0; + if (leftMissing) return 1; + if (rightMissing) return -1; + return compare(left, right); + } + + function compareStrings( + left: string | null | undefined, + right: string | null | undefined, + ) { + return compareNullable(left, right, (l, r) => + l.localeCompare(r, undefined, { sensitivity: "base" }), + ); + } + + function compareNumbers( + left: number | null | undefined, + right: number | null | undefined, + ) { + return compareNullable(left, right, (l, r) => l - r); + } + + function toTime(value: string | null | undefined) { + if (!value) return null; + const time = new Date(value).getTime(); + return Number.isNaN(time) ? null : time; + } + + const leftDomain = a.domain; + const rightDomain = b.domain; + + let comparison = 0; + + if (domainSort.key === "domain") { + comparison = compareStrings( + leftDomain.domain_name, + rightDomain.domain_name, + ); + } else if (domainSort.key === "status") { + comparison = compareStrings( + formatDomainStatusLabel(leftDomain.status), + formatDomainStatusLabel(rightDomain.status), + ); + } else if (domainSort.key === "hosting") { + comparison = compareStrings( + leftDomain.hosting_type, + rightDomain.hosting_type, + ); + } else if (domainSort.key === "created") { + comparison = compareNumbers( + toTime(leftDomain.source_created_at), + toTime(rightDomain.source_created_at), + ); + } else if (domainSort.key === "aliases") { + comparison = compareNumbers( + leftDomain.aliases_count, + rightDomain.aliases_count, + ); + } else if (domainSort.key === "last_seen") { + comparison = compareNumbers( + toTime(leftDomain.updated_at), + toTime(rightDomain.updated_at), + ); + } else if (domainSort.key === "actions") { + comparison = compareNumbers( + leftDomain.subscription_id ? 1 : 0, + rightDomain.subscription_id ? 1 : 0, + ); + } + + if (comparison === 0) { + comparison = a.index - b.index; + } + + return domainSort.direction === "asc" ? comparison : comparison * -1; + }) + .map((entry) => entry.domain); + + function toggleDomainSort(key: DomainSortKey) { + setDomainSort((current) => { + if (current.key === key) { + return { + key, + direction: current.direction === "asc" ? "desc" : "asc", + }; + } + + return { key, direction: "asc" }; + }); + } + + function getSortIndicator(key: DomainSortKey) { + if (domainSort.key !== key) return "↕"; + return domainSort.direction === "asc" ? "↑" : "↓"; + } + const instanceHealthSummary = instances.reduce( (acc, instance) => { const status = (instance.health_status ?? "unknown").toLowerCase(); @@ -1094,24 +1213,143 @@ export function DashboardControls({ - - - - - - - + + + + + + + - {filteredDomains.length === 0 ? ( + {sortedDomains.length === 0 ? ( ) : ( - filteredDomains.map((domain) => ( + sortedDomains.map((domain) => (
DomainSubscription StatusHostingCreatedAliasesLast seenActions + + + + + + + + + + + + + +
No domains found.