swarm-ui: sortable + filterable table columns
hyperhive#4020, mara. TableColumn gets two new optional extractors, each one the whole signal for its capability (mara: 'why separate selector and flag?' — dropped the sortable/filterable booleans that would've said the same thing twice and could disagree with the extractor's presence): - sortBy: (row) => string | number — column is click-to-sort iff present. Table owns the sort state (one active column, header click cycles none -> ascending -> descending), since a rendered cell often isn't the sortable value itself (e.g. AgentsPage's status column renders a Badge, not a plain string). - filterValue: (row) => string — column is filterable iff present. A text input row under the headers, substring match case-insensitive. Client-side only, no backend change - every consuming page already fetches its full row set. Wired into AgentsPage and HivesPage, the two pages with a real per-row Table. Left IssueReportPage alone (already has its own purpose-built sort + label/hide-blocked filters, predates this and covers its own domain better than a generic per-column text filter would) and JobsPage alone (renders an indented state tree via JobqGraph, no column table at all despite what my original scoping comment assumed). Verified: typecheck + build clean, nix fmt clean, static render of the actual built CSS confirms the new sort-header + filter-row markup doesn't break table layout.
This commit is contained in:
parent
9065898e08
commit
247ff4498d
4 changed files with 225 additions and 13 deletions
|
|
@ -207,8 +207,20 @@ export function AgentsPage() {
|
|||
}
|
||||
|
||||
const columns: TableColumn<AgentRow>[] = [
|
||||
{ key: "name", header: "name", render: (a) => a.name },
|
||||
{ key: "hive", header: "hive", render: (a) => a.hive ?? "—" },
|
||||
{
|
||||
key: "name",
|
||||
header: "name",
|
||||
render: (a) => a.name,
|
||||
sortBy: (a) => a.name,
|
||||
filterValue: (a) => a.name,
|
||||
},
|
||||
{
|
||||
key: "hive",
|
||||
header: "hive",
|
||||
render: (a) => a.hive ?? "—",
|
||||
sortBy: (a) => a.hive ?? "",
|
||||
filterValue: (a) => a.hive ?? "",
|
||||
},
|
||||
{
|
||||
key: "status",
|
||||
header: "status",
|
||||
|
|
@ -217,6 +229,8 @@ export function AgentsPage() {
|
|||
// stuffed a full sentence into a pill meant for a short discrete
|
||||
// label and blew the row out (mara: "looks messy"). That string
|
||||
// now lives in its own "message" column below.
|
||||
sortBy: (a) => FRESHNESS[a.freshness].label,
|
||||
filterValue: (a) => FRESHNESS[a.freshness].label,
|
||||
render: (a) => {
|
||||
const { tone, label } = FRESHNESS[a.freshness];
|
||||
return (
|
||||
|
|
@ -252,6 +266,7 @@ export function AgentsPage() {
|
|||
// `status_text: null` (the wire contract's own rule), so a stopped
|
||||
// agent just shows an em dash here rather than a stale message.
|
||||
render: (a) => a.snapshot?.status_text ?? "—",
|
||||
filterValue: (a) => a.snapshot?.status_text ?? "",
|
||||
},
|
||||
{
|
||||
key: "wanted",
|
||||
|
|
@ -261,6 +276,8 @@ export function AgentsPage() {
|
|||
// control" shape (see its header comment, which names pause/resume
|
||||
// as the exact motivating case), not a separate status chip next
|
||||
// to a separate button.
|
||||
sortBy: (a) => a.wanted ?? "",
|
||||
filterValue: (a) => a.wanted ?? "no declaration",
|
||||
render: (a) => {
|
||||
const pending = pendingAgents.has(a.name);
|
||||
const impliedCurrent =
|
||||
|
|
@ -295,6 +312,8 @@ export function AgentsPage() {
|
|||
{
|
||||
key: "config-pr",
|
||||
header: "config PR",
|
||||
sortBy: (a) => a.config_pr?.pr_number ?? 0,
|
||||
filterValue: (a) => (a.config_pr ? `#${a.config_pr.pr_number}` : ""),
|
||||
render: (a) =>
|
||||
a.config_pr ? (
|
||||
<Badge
|
||||
|
|
|
|||
|
|
@ -49,7 +49,13 @@ const FRESHNESS: Record<Freshness, { tone: BadgeTone; label: string }> = {
|
|||
};
|
||||
|
||||
const COLUMNS: TableColumn<HiveStatus>[] = [
|
||||
{ key: "name", header: "name", render: (h) => h.name },
|
||||
{
|
||||
key: "name",
|
||||
header: "name",
|
||||
render: (h) => h.name,
|
||||
sortBy: (h) => h.name,
|
||||
filterValue: (h) => h.name,
|
||||
},
|
||||
{
|
||||
key: "domain",
|
||||
header: "domain",
|
||||
|
|
@ -61,6 +67,8 @@ const COLUMNS: TableColumn<HiveStatus>[] = [
|
|||
) : (
|
||||
"—"
|
||||
),
|
||||
sortBy: (h) => h.domain ?? "",
|
||||
filterValue: (h) => h.domain ?? "",
|
||||
},
|
||||
{
|
||||
key: "status",
|
||||
|
|
@ -82,6 +90,8 @@ const COLUMNS: TableColumn<HiveStatus>[] = [
|
|||
/>
|
||||
);
|
||||
},
|
||||
sortBy: (h) => FRESHNESS[h.freshness].label,
|
||||
filterValue: (h) => FRESHNESS[h.freshness].label,
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue