From c9bbbe91f2c89857ddc9a67d6020c40140d14ae7 Mon Sep 17 00:00:00 2001 From: henrygd Date: Sun, 15 Mar 2026 14:59:25 -0400 Subject: [PATCH] ui: improve table col widths and hide text showing above header --- .../containers-table-columns.tsx | 60 ++++++++++++++----- .../containers-table/containers-table.tsx | 31 ++++++++-- .../components/routes/system/smart-table.tsx | 38 ++++++++++-- .../systemd-table/systemd-table.tsx | 14 ++--- .../systems-table/systems-table.tsx | 1 + 5 files changed, 110 insertions(+), 34 deletions(-) diff --git a/internal/site/src/components/containers-table/containers-table-columns.tsx b/internal/site/src/components/containers-table/containers-table-columns.tsx index 17fae3cf..77f5cde5 100644 --- a/internal/site/src/components/containers-table/containers-table-columns.tsx +++ b/internal/site/src/components/containers-table/containers-table-columns.tsx @@ -15,9 +15,9 @@ import { import { EthernetIcon, HourglassIcon, SquareArrowRightEnterIcon } from "../ui/icons" import { Badge } from "../ui/badge" import { t } from "@lingui/core/macro" -import { $allSystemsById } from "@/lib/stores" +import { $allSystemsById, $longestSystemNameLen } from "@/lib/stores" import { useStore } from "@nanostores/react" -import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip" +import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip" // Unit names and their corresponding number of seconds for converting docker status strings const unitSeconds = [ @@ -63,7 +63,12 @@ export const containerChartCols: ColumnDef[] = [ header: ({ column }) => , cell: ({ getValue }) => { const allSystems = useStore($allSystemsById) - return {allSystems[getValue() as string]?.name ?? ""} + const longestName = useStore($longestSystemNameLen) + return ( +
+ {allSystems[getValue() as string]?.name ?? ""} +
+ ) }, }, // { @@ -82,7 +87,7 @@ export const containerChartCols: ColumnDef[] = [ header: ({ column }) => , cell: ({ getValue }) => { const val = getValue() as number - return {`${decimalString(val, val >= 10 ? 1 : 2)}%`} + return {`${decimalString(val, val >= 10 ? 1 : 2)}%`} }, }, { @@ -94,7 +99,7 @@ export const containerChartCols: ColumnDef[] = [ const val = getValue() as number const formatted = formatBytes(val, false, undefined, true) return ( - {`${decimalString(formatted.value, formatted.value >= 10 ? 1 : 2)} ${formatted.unit}`} + {`${decimalString(formatted.value, formatted.value >= 10 ? 1 : 2)} ${formatted.unit}`} ) }, }, @@ -103,11 +108,12 @@ export const containerChartCols: ColumnDef[] = [ 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}`} +
{`${decimalString(formatted.value, formatted.value >= 10 ? 1 : 2)} ${formatted.unit}`}
) }, }, @@ -116,6 +122,7 @@ export const containerChartCols: ColumnDef[] = [ invertSorting: true, accessorFn: (record) => record.health, header: ({ column }) => , + minSize: 121, cell: ({ getValue }) => { const healthValue = getValue() as number const healthStatus = ContainerHealthLabels[healthValue] || "Unknown" @@ -138,15 +145,21 @@ export const containerChartCols: ColumnDef[] = [ 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 + const val = getValue() as string | undefined if (!val) { - return - + return
-
} - const className = "ms-1.5 w-20 block truncate tabular-nums" - if (val.length > 9) { + const className = "ms-1 w-27 block truncate tabular-nums" + if (val.length > 14) { return ( {val} @@ -165,7 +178,12 @@ export const containerChartCols: ColumnDef[] = [ ), cell: ({ getValue }) => { - return {getValue() as string} + const val = getValue() as string + return ( +
+ {val} +
+ ) }, }, { @@ -175,7 +193,7 @@ export const containerChartCols: ColumnDef[] = [ sortingFn: (a, b) => getStatusValue(a.original.status) - getStatusValue(b.original.status), header: ({ column }) => , cell: ({ getValue }) => { - return {getValue() as string} + return {getValue() as string} }, }, { @@ -185,7 +203,7 @@ export const containerChartCols: ColumnDef[] = [ header: ({ column }) => , cell: ({ getValue }) => { const timestamp = getValue() as number - return {hourWithSeconds(new Date(timestamp).toISOString())} + return {hourWithSeconds(new Date(timestamp).toISOString())} }, }, ] @@ -215,3 +233,17 @@ function HeaderButton({ ) } + +/** + * 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 +} diff --git a/internal/site/src/components/containers-table/containers-table.tsx b/internal/site/src/components/containers-table/containers-table.tsx index 42906e25..13225a74 100644 --- a/internal/site/src/components/containers-table/containers-table.tsx +++ b/internal/site/src/components/containers-table/containers-table.tsx @@ -1,3 +1,4 @@ +/** biome-ignore-all lint/security/noDangerouslySetInnerHtml: html comes directly from docker via agent */ import { t } from "@lingui/core/macro" import { Trans } from "@lingui/react/macro" import { @@ -13,7 +14,7 @@ import { type VisibilityState, } from "@tanstack/react-table" import { useVirtualizer, type VirtualItem } from "@tanstack/react-virtual" -import { memo, RefObject, useEffect, useRef, useState } from "react" +import { memo, type RefObject, useEffect, useRef, useState } from "react" import { Input } from "@/components/ui/input" import { TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import { pb } from "@/lib/api" @@ -44,6 +45,20 @@ export default function ContainersTable({ systemId }: { systemId?: string }) { ) const [columnFilters, setColumnFilters] = useState([]) const [columnVisibility, setColumnVisibility] = useState({}) + + // Hide ports column if no ports are present + useEffect(() => { + if (data) { + const hasPorts = data.some((container) => container.ports) + setColumnVisibility((prev) => { + if (prev.ports === hasPorts) { + return prev + } + return { ...prev, ports: hasPorts } + }) + } + }, [data]) + const [rowSelection, setRowSelection] = useState({}) const [globalFilter, setGlobalFilter] = useState("") @@ -67,7 +82,7 @@ export default function ContainersTable({ systemId }: { systemId?: string }) { setData((curItems) => { const lastUpdated = Math.max(items[0].updated, items.at(-1)?.updated ?? 0) const containerIds = new Set() - const newItems = [] + const newItems: ContainerRecord[] = [] for (const item of items) { if (Math.abs(lastUpdated - item.updated) < 70_000) { containerIds.add(item.id) @@ -301,9 +316,6 @@ function ContainerSheet({ setSheetOpen: (open: boolean) => void activeContainer: RefObject }) { - const container = activeContainer.current - if (!container) return null - const [logsDisplay, setLogsDisplay] = useState("") const [infoDisplay, setInfoDisplay] = useState("") const [logsFullscreenOpen, setLogsFullscreenOpen] = useState(false) @@ -311,6 +323,8 @@ function ContainerSheet({ const [isRefreshingLogs, setIsRefreshingLogs] = useState(false) const logsContainerRef = useRef(null) + const container = activeContainer.current + function scrollLogsToBottom() { if (logsContainerRef.current) { logsContainerRef.current.scrollTo({ top: logsContainerRef.current.scrollHeight }) @@ -318,6 +332,7 @@ function ContainerSheet({ } const refreshLogs = async () => { + if (!container) return setIsRefreshingLogs(true) const startTime = Date.now() @@ -349,6 +364,8 @@ function ContainerSheet({ })() }, [container]) + if (!container) return null + return ( <> }) { return ( +
{table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => { return ( - + {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} ) @@ -481,6 +499,7 @@ const ContainerTableRow = memo(function ContainerTableRow({ className="py-0 ps-4.5" style={{ height: virtualRow.size, + width: cell.column.getSize(), }} > {flexRender(cell.column.columnDef.cell, cell.getContext())} diff --git a/internal/site/src/components/routes/system/smart-table.tsx b/internal/site/src/components/routes/system/smart-table.tsx index dad92bf5..04dab4c7 100644 --- a/internal/site/src/components/routes/system/smart-table.tsx +++ b/internal/site/src/components/routes/system/smart-table.tsx @@ -40,6 +40,7 @@ import { toFixedFloat, formatTemperature, cn, + getVisualStringWidth, secondsToString, hourWithSeconds, formatShortDate, @@ -101,7 +102,7 @@ function formatCapacity(bytes: number): string { const SMART_DEVICE_FIELDS = "id,system,name,model,state,capacity,temp,type,hours,cycles,updated" -export const columns: ColumnDef[] = [ +export const createColumns = (longestName: number): ColumnDef[] => [ { id: "system", accessorFn: (record) => record.system, @@ -114,7 +115,11 @@ export const columns: ColumnDef[] = [ header: ({ column }) => , cell: ({ getValue }) => { const allSystems = useStore($allSystemsById) - return {allSystems[getValue() as string]?.name ?? ""} + return ( +
+ {allSystems[getValue() as string]?.name ?? ""} +
+ ) }, }, { @@ -275,6 +280,30 @@ export default function DisksTable({ systemId }: { systemId?: string }) { const [sheetOpen, setSheetOpen] = useState(false) const [rowActionState, setRowActionState] = useState<{ type: "refresh" | "delete"; id: string } | null>(null) const [globalFilter, setGlobalFilter] = useState("") + const allSystems = useStore($allSystemsById) + + // duplicate the devices to test with more rows + // if (smartDevices?.length && smartDevices.length < 50) { + // setSmartDevices([...smartDevices, ...smartDevices, ...smartDevices]) + // } + + // Calculate the right width for the system column based on the longest system name among the displayed devices + const longestName = useMemo(() => { + if (systemId || !smartDevices || Object.keys(allSystems).length === 0) { + return 0 + } + let maxLen = 0 + const seenSystems = new Set() + for (const device of smartDevices) { + if (seenSystems.has(device.system)) { + continue + } + seenSystems.add(device.system) + const name = allSystems[device.system]?.name ?? "" + maxLen = Math.max(maxLen, getVisualStringWidth(name)) + } + return maxLen + }, [smartDevices, systemId, allSystems]) const openSheet = (disk: SmartDeviceRecord) => { setActiveDiskId(disk.id) @@ -440,9 +469,10 @@ export default function DisksTable({ systemId }: { systemId?: string }) { // Filter columns based on whether systemId is provided const tableColumns = useMemo(() => { + const columns = createColumns(longestName) const baseColumns = systemId ? columns.filter((col) => col.id !== "system") : columns return [...baseColumns, actionColumn] - }, [systemId, actionColumn]) + }, [systemId, actionColumn, longestName]) const table = useReactTable({ data: smartDevices || ([] as SmartDeviceRecord[]), @@ -513,7 +543,7 @@ export default function DisksTable({ systemId }: { systemId?: string }) { -
+
{table.getHeaderGroups().map((headerGroup) => ( diff --git a/internal/site/src/components/systemd-table/systemd-table.tsx b/internal/site/src/components/systemd-table/systemd-table.tsx index 9dbf03ed..91225f39 100644 --- a/internal/site/src/components/systemd-table/systemd-table.tsx +++ b/internal/site/src/components/systemd-table/systemd-table.tsx @@ -46,7 +46,6 @@ export default function SystemdTable({ systemId }: { systemId?: string }) { return setData([]) }, [systemId]) - useEffect(() => { const lastUpdated = data[0]?.updated ?? 0 @@ -360,15 +359,9 @@ function SystemdSheet({ return ( <> {hasCurrent ? current : notAvailable} - {hasMax && ( - - {`(${t`limit`}: ${max})`} - - )} + {hasMax && {`(${t`limit`}: ${max})`}} {max === null && ( - - {`(${t`limit`}: ${t`Unlimited`.toLowerCase()})`} - + {`(${t`limit`}: ${t`Unlimited`.toLowerCase()})`} )} ) @@ -435,7 +428,7 @@ function SystemdSheet({ ) } - + const capitalize = (str: string) => `${str.charAt(0).toUpperCase()}${str.slice(1).toLowerCase()}` return ( @@ -621,6 +614,7 @@ function SystemdSheet({ function SystemdTableHead({ table }: { table: TableType }) { return ( +
{table.getHeaderGroups().map((headerGroup) => (
{headerGroup.headers.map((header) => { diff --git a/internal/site/src/components/systems-table/systems-table.tsx b/internal/site/src/components/systems-table/systems-table.tsx index 78438c5a..64516ade 100644 --- a/internal/site/src/components/systems-table/systems-table.tsx +++ b/internal/site/src/components/systems-table/systems-table.tsx @@ -391,6 +391,7 @@ function SystemsTableHead({ table }: { table: TableType }) { const { t } = useLingui() return ( +
{table.getHeaderGroups().map((headerGroup) => (
{headerGroup.headers.map((header) => {