import type { Column, ColumnDef } from "@tanstack/react-table" import { Button } from "@/components/ui/button" import { cn, decimalString, formatBytes, hourWithSeconds } from "@/lib/utils" import type { ContainerRecord } from "@/types" import { ContainerHealth, ContainerHealthLabels } from "@/lib/enums" import { ClockIcon, ContainerIcon, CpuIcon, LayersIcon, MemoryStickIcon, ServerIcon, ShieldCheckIcon, } from "lucide-react" import { EthernetIcon, HourglassIcon, SquareArrowRightEnterIcon } from "../ui/icons" import { Badge } from "../ui/badge" import { t } from "@lingui/core/macro" import { $allSystemsById, $longestSystemNameLen } from "@/lib/stores" import { useStore } from "@nanostores/react" import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip" // Unit names and their corresponding number of seconds for converting docker status strings const unitSeconds = [ ["s", 1], ["mi", 60], ["h", 3600], ["d", 86400], ["w", 604800], ["mo", 2592000], ] as const // Convert docker status string to number of seconds ("Up X minutes", "Up X hours", etc.) function getStatusValue(status: string): number { const [_, num, unit] = status.split(" ") // Docker uses "a" or "an" instead of "1" for singular units (e.g., "Up a minute", "Up an hour") const numValue = num === "a" || num === "an" ? 1 : Number(num) for (const [unitName, value] of unitSeconds) { if (unit.startsWith(unitName)) { return numValue * value } } return 0 } export const containerChartCols: ColumnDef[] = [ { id: "name", sortingFn: (a, b) => a.original.name.localeCompare(b.original.name), accessorFn: (record) => record.name, header: ({ column }) => , cell: ({ getValue }) => { return {getValue() as string} }, }, { id: "system", accessorFn: (record) => record.system, sortingFn: (a, b) => { const allSystems = $allSystemsById.get() const systemNameA = allSystems[a.original.system]?.name ?? "" const systemNameB = allSystems[b.original.system]?.name ?? "" return systemNameA.localeCompare(systemNameB) }, header: ({ column }) => , cell: ({ getValue }) => { const allSystems = useStore($allSystemsById) const longestName = useStore($longestSystemNameLen) return (
{allSystems[getValue() as string]?.name ?? ""}
) }, }, // { // id: "id", // accessorFn: (record) => record.id, // sortingFn: (a, b) => a.original.id.localeCompare(b.original.id), // header: ({ column }) => , // cell: ({ getValue }) => { // return {getValue() as string} // }, // }, { id: "cpu", accessorFn: (record) => record.cpu, invertSorting: true, header: ({ column }) => , cell: ({ getValue }) => { const val = getValue() as number return {`${decimalString(val, val >= 10 ? 1 : 2)}%`} }, }, { id: "memory", accessorFn: (record) => record.memory, invertSorting: true, header: ({ column }) => , cell: ({ getValue }) => { const val = getValue() as number const formatted = formatBytes(val, false, undefined, true) return ( {`${decimalString(formatted.value, formatted.value >= 10 ? 1 : 2)} ${formatted.unit}`} ) }, }, { id: "net", accessorFn: (record) => record.net, invertSorting: true, header: ({ column }) => , minSize: 112, cell: ({ getValue }) => { const val = getValue() as number const formatted = formatBytes(val, true, undefined, false) return (
{`${decimalString(formatted.value, formatted.value >= 10 ? 1 : 2)} ${formatted.unit}`}
) }, }, { id: "health", invertSorting: true, accessorFn: (record) => record.health, header: ({ column }) => , minSize: 121, cell: ({ getValue }) => { const healthValue = getValue() as number const healthStatus = ContainerHealthLabels[healthValue] || "Unknown" return ( {healthStatus} ) }, }, { id: "ports", accessorFn: (record) => record.ports || undefined, header: ({ column }) => ( ), sortingFn: (a, b) => getPortValue(a.original.ports) - getPortValue(b.original.ports), minSize: 147, cell: ({ getValue }) => { const val = getValue() as string | undefined if (!val) { return
-
} const className = "ms-1 w-27 block truncate tabular-nums" if (val.length > 14) { return ( {val} {val} ) } return {val} }, }, { id: "image", sortingFn: (a, b) => a.original.image.localeCompare(b.original.image), accessorFn: (record) => record.image, header: ({ column }) => ( ), cell: ({ getValue }) => { const val = getValue() as string return (
{val}
) }, }, { id: "status", accessorFn: (record) => record.status, invertSorting: true, sortingFn: (a, b) => getStatusValue(a.original.status) - getStatusValue(b.original.status), header: ({ column }) => , cell: ({ getValue }) => { return {getValue() as string} }, }, { id: "updated", invertSorting: true, accessorFn: (record) => record.updated, header: ({ column }) => , cell: ({ getValue }) => { const timestamp = getValue() as number return {hourWithSeconds(new Date(timestamp).toISOString())} }, }, ] function HeaderButton({ column, name, Icon, }: { column: Column name: string Icon: React.ElementType }) { const isSorted = column.getIsSorted() return ( ) } /** * Convert port string to a number for sorting. * Handles formats like "80", "127.0.0.1:80", and "80, 443" (takes the first mapping). */ function getPortValue(ports: string | undefined): number { if (!ports) { return 0 } const first = ports.includes(",") ? ports.substring(0, ports.indexOf(",")) : ports const colonIndex = first.lastIndexOf(":") const portStr = colonIndex === -1 ? first : first.substring(colonIndex + 1) return Number(portStr) || 0 }