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,
})}
)}
{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()
: "-"}
|
))}
)}
);
}