import { getErrorMessage } from "@/lib/error-message.js"; import { useEffect, useMemo, useState, useCallback } from "react"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { ArrowDown, ArrowUp, ArrowUpDown, Loader2, RefreshCw, Search, Server, } from "lucide-react"; import { Input } from "@/components/input"; import { Button } from "@/components/button"; import { cn } from "@/lib/utils"; import { listFleets, getFleetInventory, refreshFleetInventory, type FleetRow, type FleetInventoryEntry, } from "@/api/fleets-api"; type SortKey = | "hostName" | "osPrettyName" | "kernel" | "architecture" | "uptimeSeconds" | "packageManager" | "collectedAt"; type SortDirection = "asc" | "desc"; function formatUptime(seconds: number | null): string { if (seconds === null) return "-"; const d = Math.floor(seconds / 86400); const h = Math.floor((seconds % 86400) / 3600); const m = Math.floor((seconds % 3600) / 60); if (d > 0) return `${d}d ${h}h`; if (h > 0) return `${h}h ${m}m`; return `${m}m`; } function compareValues( a: string | number | null, b: string | number | null, ): number { if (a === null && b === null) return 0; if (a === null) return 1; if (b === null) return -1; if (typeof a === "number" && typeof b === "number") return a - b; return String(a).localeCompare(String(b)); } function SortHeader({ label, column, sortKey, sortDirection, onSort, }: { label: string; column: SortKey; sortKey: SortKey; sortDirection: SortDirection; onSort: (column: SortKey) => void; }) { const active = sortKey === column; return ( ); } export function FleetInventoryTab({ fleetId, isVisible = true, }: { fleetId?: number; isVisible?: boolean; }) { const { t } = useTranslation(); const [fleets, setFleets] = useState([]); const [selectedFleetId, setSelectedFleetId] = useState( fleetId, ); const [entries, setEntries] = useState([]); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const [search, setSearch] = useState(""); const [sortKey, setSortKey] = useState("hostName"); const [sortDirection, setSortDirection] = useState("asc"); useEffect(() => { if (fleetId !== undefined) setSelectedFleetId(fleetId); }, [fleetId]); useEffect(() => { listFleets() .then(setFleets) .catch(() => {}); }, []); useEffect(() => { if (!fleets.length) return; if (selectedFleetId === undefined) { setSelectedFleetId(fleets[0].id); } }, [fleets, selectedFleetId]); const loadInventory = useCallback(async () => { if (selectedFleetId === undefined) return; setLoading(true); try { const data = await getFleetInventory(selectedFleetId); setEntries(data); } catch (error) { const message = getErrorMessage(error, ""); toast.error(message || t("newUi.sidebar.fleets.inventoryLoadFailed")); } finally { setLoading(false); } }, [selectedFleetId, t]); useEffect(() => { if (!isVisible) return; loadInventory(); }, [loadInventory, isVisible]); async function handleRefresh() { if (selectedFleetId === undefined) return; setRefreshing(true); try { const { results } = await refreshFleetInventory(selectedFleetId); const failed = results.filter((r) => !r.success).length; if (failed > 0) { toast.warning( t("newUi.sidebar.fleets.commandPartialFailure", { failed, total: results.length, }), ); } else { toast.success(t("newUi.sidebar.fleets.inventoryRefreshed")); } await loadInventory(); } catch (error) { const message = getErrorMessage(error, ""); toast.error(message || t("newUi.sidebar.fleets.inventoryRefreshFailed")); } finally { setRefreshing(false); } } function handleSort(column: SortKey) { if (sortKey === column) { setSortDirection((prev) => (prev === "asc" ? "desc" : "asc")); } else { setSortKey(column); setSortDirection("asc"); } } const selectedFleet = fleets.find((f) => f.id === selectedFleetId); const filteredSorted = useMemo(() => { const q = search.trim().toLowerCase(); const filtered = entries.filter( (e) => !q || e.hostName.toLowerCase().includes(q) || (e.inventory?.osPrettyName ?? "").toLowerCase().includes(q) || (e.inventory?.hostname ?? "").toLowerCase().includes(q), ); const dir = sortDirection === "asc" ? 1 : -1; return [...filtered].sort((a, b) => { let cmp = 0; switch (sortKey) { case "hostName": cmp = compareValues(a.hostName, b.hostName); break; case "osPrettyName": cmp = compareValues( a.inventory?.osPrettyName ?? null, b.inventory?.osPrettyName ?? null, ); break; case "kernel": cmp = compareValues( a.inventory?.kernel ?? null, b.inventory?.kernel ?? null, ); break; case "architecture": cmp = compareValues( a.inventory?.architecture ?? null, b.inventory?.architecture ?? null, ); break; case "uptimeSeconds": cmp = compareValues( a.inventory?.uptimeSeconds ?? null, b.inventory?.uptimeSeconds ?? null, ); break; case "packageManager": cmp = compareValues( a.inventory?.packageManager ?? null, b.inventory?.packageManager ?? null, ); break; case "collectedAt": cmp = compareValues( a.inventory?.collectedAt ?? null, b.inventory?.collectedAt ?? null, ); break; } return cmp * dir; }); }, [entries, search, sortKey, sortDirection]); const collectedCount = entries.filter((e) => e.inventory !== null).length; return (
{t("newUi.sidebar.fleets.tabInventory")} {selectedFleet && ( {t("newUi.sidebar.fleets.memberCount", { count: entries.length, })}{" "} ·{" "} {t("newUi.sidebar.fleets.inventoryCollectedCount", { count: collectedCount, total: entries.length, })} )}
setSearch(e.target.value)} placeholder={t("newUi.sidebar.fleets.searchHosts")} className="h-7 w-48 pl-6 text-xs" />
{loading ? (
) : fleets.length === 0 ? (
{t("newUi.sidebar.fleets.noFleets")}
) : filteredSorted.length === 0 ? (
{t("newUi.sidebar.fleets.noMembers")}
) : ( {filteredSorted.map((entry) => ( ))}
{entry.hostName}
{entry.inventory?.osPrettyName ?? "-"} {entry.inventory?.kernel ?? "-"} {entry.inventory?.architecture ?? "-"} {formatUptime(entry.inventory?.uptimeSeconds ?? null)} {entry.inventory?.packageManager ?? "-"} {entry.inventory?.collectedAt ? new Date(entry.inventory.collectedAt).toLocaleString() : "-"}
)}
); }