diff --git a/internal/site/src/components/routes/system/smart-table.tsx b/internal/site/src/components/routes/system/smart-table.tsx index 04dab4c7..5126cccb 100644 --- a/internal/site/src/components/routes/system/smart-table.tsx +++ b/internal/site/src/components/routes/system/smart-table.tsx @@ -3,13 +3,16 @@ import { type ColumnDef, type ColumnFiltersState, type Column, + type Row, type SortingState, + type Table as TableType, flexRender, getCoreRowModel, getFilteredRowModel, getSortedRowModel, useReactTable, } from "@tanstack/react-table" +import { useVirtualizer, type VirtualItem } from "@tanstack/react-virtual" import { Activity, Box, @@ -58,7 +61,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" -import { useCallback, useMemo, useEffect, useState } from "react" +import { memo, useCallback, useMemo, useEffect, useRef, useState } from "react" import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" // Column definition for S.M.A.R.T. attributes table @@ -102,7 +105,11 @@ function formatCapacity(bytes: number): string { const SMART_DEVICE_FIELDS = "id,system,name,model,state,capacity,temp,type,hours,cycles,updated" -export const createColumns = (longestName: number): ColumnDef[] => [ +export const createColumns = ( + longestName: number, + longestModel: number, + longestDevice: number +): ColumnDef[] => [ { id: "system", accessorFn: (record) => record.system, @@ -127,7 +134,11 @@ export const createColumns = (longestName: number): ColumnDef sortingFn: (a, b) => a.original.name.localeCompare(b.original.name), header: ({ column }) => , cell: ({ getValue }) => ( -
+
{getValue() as string}
), @@ -137,7 +148,11 @@ export const createColumns = (longestName: number): ColumnDef sortingFn: (a, b) => a.original.model.localeCompare(b.original.model), header: ({ column }) => , cell: ({ getValue }) => ( -
+
{getValue() as string}
), @@ -146,7 +161,7 @@ export const createColumns = (longestName: number): ColumnDef accessorKey: "capacity", invertSorting: true, header: ({ column }) => , - cell: ({ getValue }) => {formatCapacity(getValue() as number)}, + cell: ({ getValue }) => {formatCapacity(getValue() as number)}, }, { accessorKey: "state", @@ -154,9 +169,9 @@ export const createColumns = (longestName: number): ColumnDef cell: ({ getValue }) => { const status = getValue() as string return ( -
- {status} -
+ + {status} + ) }, }, @@ -165,11 +180,9 @@ export const createColumns = (longestName: number): ColumnDef sortingFn: (a, b) => a.original.type.localeCompare(b.original.type), header: ({ column }) => , cell: ({ getValue }) => ( -
- - {getValue() as string} - -
+ + {getValue() as string} + ), }, { @@ -181,11 +194,11 @@ export const createColumns = (longestName: number): ColumnDef cell: ({ getValue }) => { const hours = getValue() as number | undefined if (hours == null) { - return
N/A
+ return
N/A
} const seconds = hours * 3600 return ( -
+
{secondsToString(seconds, "hour")}
{secondsToString(seconds, "day")}
@@ -201,9 +214,9 @@ export const createColumns = (longestName: number): ColumnDef cell: ({ getValue }) => { const cycles = getValue() as number | undefined if (cycles == null) { - return
N/A
+ return
N/A
} - return {cycles.toLocaleString()} + return {cycles.toLocaleString()} }, }, { @@ -213,10 +226,10 @@ export const createColumns = (longestName: number): ColumnDef cell: ({ getValue }) => { const temp = getValue() as number | null | undefined if (!temp) { - return
N/A
+ return
N/A
} const { value, unit } = formatTemperature(temp) - return {`${value} ${unit}`} + return {`${value} ${unit}`} }, }, // { @@ -241,7 +254,7 @@ export const createColumns = (longestName: number): ColumnDef // if today, use hourWithSeconds, otherwise use formatShortDate const formatter = new Date(timestamp).toDateString() === new Date().toDateString() ? hourWithSeconds : formatShortDate - return {formatter(timestamp)} + return {formatter(timestamp)} }, }, ] @@ -283,26 +296,32 @@ export default function DisksTable({ systemId }: { systemId?: string }) { const allSystems = useStore($allSystemsById) // duplicate the devices to test with more rows - // if (smartDevices?.length && smartDevices.length < 50) { + // if ( + // smartDevices?.length && + // smartDevices.length < 50 && + // typeof window !== "undefined" && + // window.location.hostname === "localhost" + // ) { // 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 + // Calculate the right width for the columns based on the longest strings among the displayed devices + const { longestName, longestModel, longestDevice } = useMemo(() => { + const result = { longestName: 0, longestModel: 0, longestDevice: 0 } + if (!smartDevices || Object.keys(allSystems).length === 0) { + return result } - let maxLen = 0 const seenSystems = new Set() for (const device of smartDevices) { - if (seenSystems.has(device.system)) { - continue + if (!systemId && !seenSystems.has(device.system)) { + seenSystems.add(device.system) + const name = allSystems[device.system]?.name ?? "" + result.longestName = Math.max(result.longestName, getVisualStringWidth(name)) } - seenSystems.add(device.system) - const name = allSystems[device.system]?.name ?? "" - maxLen = Math.max(maxLen, getVisualStringWidth(name)) + result.longestModel = Math.max(result.longestModel, getVisualStringWidth(device.model ?? "")) + result.longestDevice = Math.max(result.longestDevice, getVisualStringWidth(device.name ?? "")) } - return maxLen + return result }, [smartDevices, systemId, allSystems]) const openSheet = (disk: SmartDeviceRecord) => { @@ -469,10 +488,10 @@ export default function DisksTable({ systemId }: { systemId?: string }) { // Filter columns based on whether systemId is provided const tableColumns = useMemo(() => { - const columns = createColumns(longestName) + const columns = createColumns(longestName, longestModel, longestDevice) const baseColumns = systemId ? columns.filter((col) => col.id !== "system") : columns return [...baseColumns, actionColumn] - }, [systemId, actionColumn, longestName]) + }, [systemId, actionColumn, longestName, longestModel, longestDevice]) const table = useReactTable({ data: smartDevices || ([] as SmartDeviceRecord[]), @@ -504,6 +523,7 @@ export default function DisksTable({ systemId }: { systemId?: string }) { .every((term) => searchString.includes(term)) }, }) + const rows = table.getRowModel().rows // Hide the table on system pages if there's no data, but always show on global page if (systemId && !smartDevices?.length && !columnFilters.length) { @@ -543,57 +563,123 @@ export default function DisksTable({ systemId }: { systemId?: string }) {
-
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} - - ) - })} - - ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - openSheet(row.original)} - > - {row.getVisibleCells().map((cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} - - )) - ) : ( - - - {smartDevices ? ( - t`No results.` - ) : ( - - )} - - - )} - -
-
+
) } +const SmartDevicesTable = memo(function SmartDevicesTable({ + table, + rows, + colLength, + data, + openSheet, +}: { + table: TableType + rows: Row[] + colLength: number + data: SmartDeviceRecord[] | undefined + openSheet: (disk: SmartDeviceRecord) => void +}) { + const scrollRef = useRef(null) + + const virtualizer = useVirtualizer({ + count: rows.length, + estimateSize: () => 65, + getScrollElement: () => scrollRef.current, + overscan: 5, + }) + const virtualRows = virtualizer.getVirtualItems() + + const paddingTop = Math.max(0, virtualRows[0]?.start ?? 0 - virtualizer.options.scrollMargin) + const paddingBottom = Math.max(0, virtualizer.getTotalSize() - (virtualRows[virtualRows.length - 1]?.end ?? 0)) + + return ( +
2) && "min-h-50" + )} + ref={scrollRef} + > +
+ + + + {rows.length ? ( + virtualRows.map((virtualRow) => { + const row = rows[virtualRow.index] + return + }) + ) : ( + + + {data ? t`No results.` : } + + + )} + +
+
+
+ ) +}) + +function SmartTableHead({ table }: { table: TableType }) { + return ( + +
+ {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} + + ))} + + ))} +
+ ) +} + +const SmartDeviceTableRow = memo(function SmartDeviceTableRow({ + row, + virtualRow, + openSheet, +}: { + row: Row + virtualRow: VirtualItem + openSheet: (disk: SmartDeviceRecord) => void +}) { + return ( + openSheet(row.original)} + > + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + ) +}) + function DiskSheet({ diskId, open,