diff --git a/frontend/packages/swarm-ui/src/pages/AgentsPage.tsx b/frontend/packages/swarm-ui/src/pages/AgentsPage.tsx index 4010af2b..5a4d23b6 100644 --- a/frontend/packages/swarm-ui/src/pages/AgentsPage.tsx +++ b/frontend/packages/swarm-ui/src/pages/AgentsPage.tsx @@ -310,7 +310,11 @@ export function AgentsPage() { header: "hive", render: (a) => a.hive ?? "—", sortBy: (a) => a.hive ?? "", - filterValue: (a) => a.hive ?? "", + // "—" (not "") so the missing-hive option in the filter dropdown + // reads the same as the cell itself, rather than showing a blank + // choice. + filterValue: (a) => a.hive ?? "—", + filterMode: "select", }, { key: "status", @@ -322,6 +326,7 @@ export function AgentsPage() { // now lives in its own "message" column below. sortBy: (a) => FRESHNESS[a.freshness].label, filterValue: (a) => FRESHNESS[a.freshness].label, + filterMode: "select", render: (a) => { const { tone, label } = FRESHNESS[a.freshness]; return ( @@ -367,6 +372,7 @@ export function AgentsPage() { // callbacks to the page's own state/handlers. sortBy: (a) => a.wanted ?? "", filterValue: (a) => a.wanted ?? "no declaration", + filterMode: "select", render: (a) => { const err = actionErrors.get(a.name); return ( diff --git a/frontend/packages/swarm-ui/src/pages/HivesPage.tsx b/frontend/packages/swarm-ui/src/pages/HivesPage.tsx index c89f3446..1ee81b32 100644 --- a/frontend/packages/swarm-ui/src/pages/HivesPage.tsx +++ b/frontend/packages/swarm-ui/src/pages/HivesPage.tsx @@ -92,6 +92,7 @@ const COLUMNS: TableColumn[] = [ }, sortBy: (h) => FRESHNESS[h.freshness].label, filterValue: (h) => FRESHNESS[h.freshness].label, + filterMode: "select", }, ]; diff --git a/frontend/packages/swarm-ui/src/ui/table/Table.css b/frontend/packages/swarm-ui/src/ui/table/Table.css index 2ec46dae..30fb1f81 100644 --- a/frontend/packages/swarm-ui/src/ui/table/Table.css +++ b/frontend/packages/swarm-ui/src/ui/table/Table.css @@ -70,7 +70,8 @@ padding-bottom: 0.5em; font-weight: 400; } -.ui-table-filter-input { +.ui-table-filter-input, +.ui-table-filter-select { width: 100%; box-sizing: border-box; background: var(--bg); diff --git a/frontend/packages/swarm-ui/src/ui/table/Table.tsx b/frontend/packages/swarm-ui/src/ui/table/Table.tsx index bf982941..9a1c6887 100644 --- a/frontend/packages/swarm-ui/src/ui/table/Table.tsx +++ b/frontend/packages/swarm-ui/src/ui/table/Table.tsx @@ -51,13 +51,28 @@ export interface TableColumn { */ sortBy?: (row: T) => string | number; /** - * Column is filterable (a text input in its own row under the - * headers) when present — extracts the substring to match a case- - * insensitive filter query against. Same one-signal reasoning as - * `sortBy`: no separate `filterable` flag, the extractor's presence - * is the flag. + * Column is filterable (a control in its own row under the headers) + * when present — extracts the string to filter this column against. + * Same one-signal reasoning as `sortBy`: no separate `filterable` + * flag, the extractor's presence is the flag. How the extracted + * string is matched against the operator's input is `filterMode`. */ filterValue?: (row: T) => string; + /** + * `"text"` (default): free substring, case-insensitive `` — + * today's only behavior, unaffected if you don't set this. + * `"select"`: a ` { + const v = (e.target as HTMLSelectElement).value; + setFilters((prev) => ({ ...prev, [c.key]: v })); + }} + > + + {selectOptionsFor(c).map((v) => ( + + ))} + + ) : c.filterValue ? (