"use client"; import { useMemo, useState } from "react"; import { AgentFeatures } from "@/components/AgentFeatures"; import { AgentLink } from "@/components/AgentLink"; import { ClientPager } from "@/components/ClientPager"; import { EmptyPanel, Td, Th } from "@/components/DataPanel"; import { LabelChips } from "@/components/Labels"; import { ListToolbar } from "@/components/ListToolbar"; import { SortHeaderButton, type SortDir } from "@/components/SortHeaderButton"; import { DateTime } from "@/components/TimeZoneControls"; import type { components } from "@/lib/api-types"; import { statusBadgeClass } from "@/lib/format"; import { DEFAULT_PAGE_SIZE, clampPage, pageCountOf, paginate, type PageSize, } from "@/lib/pagination"; import { usePolling } from "@/lib/usePolling"; import type { InventoryData } from "@/lib/view-types"; type Agent = components["schemas"]["Agent"]; type CheckState = components["schemas"]["CheckState"]; type CheckStatus = CheckState["status"]; type CheckCounts = Record; type SortField = "host" | "status" | "last_seen_at"; export type AgentRow = { agent: Agent; checks: CheckCounts }; const checkStatuses: CheckStatus[] = ["ok", "warning", "critical", "unknown"]; const statusRank: Record = { alive: 0, stale: 1, dead: 2 }; const checkSortStatuses: CheckStatus[] = ["critical", "warning", "unknown", "ok"]; export function AgentsBrowser({ initialData }: { initialData: InventoryData }) { const { data } = usePolling(initialData, "/api/poll/inventory"); const [query, setQuery] = useState(""); const [sort, setSort] = useState("status"); const [dir, setDir] = useState("desc"); const initialRows = useMemo(() => rowsFromInventory(data), [data]); const rows = useMemo( () => sortAgents(filterAgents(initialRows, query), sort, dir), [dir, initialRows, query, sort], ); const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE); const [page, setPage] = useState(1); const pageCount = pageCountOf(rows.length, pageSize); const currentPage = clampPage(page, pageCount); const visible = paginate(rows, currentPage, pageSize); const onQueryChange = (value: string) => { setQuery(value); setPage(1); }; const onPageSizeChange = (next: PageSize) => { setPageSize(next); setPage(1); }; const chooseSort = (field: SortField) => { if (field === sort) { setDir((current) => (current === "asc" ? "desc" : "asc")); } else { setSort(field); setDir(fieldDefaultDir(field)); } setPage(1); }; return ( <>

Agents

{rows.length} of {initialRows.length} items
{rows.length === 0 ? ( ) : ( {visible.map(({ agent: a, checks }) => ( ))}
HOST STATUS LAST_SEEN_AT features labels
)} ); } function filterAgents(rows: AgentRow[], query: string): AgentRow[] { const needle = query.trim().toLowerCase(); if (!needle) return rows; return rows.filter(({ agent }) => `${agent.hostname} ${agent.agent_id}`.toLowerCase().includes(needle), ); } function fieldDefaultDir(field: SortField): SortDir { return field === "host" ? "asc" : "desc"; } function sortAgents(rows: AgentRow[], sort: SortField, dir: SortDir): AgentRow[] { return [...rows].sort((a, b) => { const primary = compareByField(a, b, sort); if (primary !== 0) return dir === "asc" ? primary : -primary; return hostName(a.agent).localeCompare(hostName(b.agent)) || a.agent.agent_id.localeCompare(b.agent.agent_id); }); } function compareByField(a: AgentRow, b: AgentRow, sort: SortField): number { switch (sort) { case "host": return hostName(a.agent).localeCompare(hostName(b.agent)) || a.agent.agent_id.localeCompare(b.agent.agent_id); case "last_seen_at": return Date.parse(a.agent.last_seen_at) - Date.parse(b.agent.last_seen_at); case "status": return compareStatusImportance(a, b); } } function compareStatusImportance(a: AgentRow, b: AgentRow): number { const agentStatus = statusRank[a.agent.status] - statusRank[b.agent.status]; if (agentStatus !== 0) return agentStatus; for (const status of checkSortStatuses) { const checkStatus = a.checks[status] - b.checks[status]; if (checkStatus !== 0) return checkStatus; } return 0; } function hostName(agent: Agent): string { return agent.hostname || agent.agent_id; } function AgentStatus({ status, checks, }: { status: "alive" | "stale" | "dead"; checks?: CheckCounts; }) { const counts = checks ?? emptyCheckCounts(); const total = checkStatuses.reduce((sum, s) => sum + counts[s], 0); return (
{status} checks {total} {checkStatuses.map((s, i) => ( {i > 0 ? / : null} {shortStatus(s)} {counts[s]} ))}
); } function emptyCheckCounts(): CheckCounts { return { ok: 0, warning: 0, critical: 0, unknown: 0 }; } function rowsFromInventory(data: InventoryData): AgentRow[] { const checksByAgent = new Map(); for (const c of data.checks) { const counts = checksByAgent.get(c.agent_id) ?? emptyCheckCounts(); counts[c.status] += 1; checksByAgent.set(c.agent_id, counts); } return data.agents.map((agent) => ({ agent, checks: checksByAgent.get(agent.agent_id) ?? emptyCheckCounts(), })); } function shortStatus(status: CheckStatus): string { switch (status) { case "ok": return "ok"; case "warning": return "warn"; case "critical": return "crit"; case "unknown": return "unknown"; } }