Add sorting to the smart table (#1333)

This commit is contained in:
Sven van Ginkel
2025-10-27 16:43:23 +01:00
committed by GitHub
parent 1452817423
commit 9df4d29236

View File

@@ -3,6 +3,7 @@ import { t } from "@lingui/core/macro"
import { import {
ColumnDef, ColumnDef,
ColumnFiltersState, ColumnFiltersState,
Column,
flexRender, flexRender,
getCoreRowModel, getCoreRowModel,
getFilteredRowModel, getFilteredRowModel,
@@ -10,7 +11,7 @@ import {
SortingState, SortingState,
useReactTable, useReactTable,
} from "@tanstack/react-table" } from "@tanstack/react-table"
import { Activity, Box, Clock, HardDrive, HashIcon, CpuIcon, BinaryIcon, RotateCwIcon, LoaderCircleIcon, CheckCircle2Icon, XCircleIcon, ArrowLeftRightIcon } from "lucide-react" import { Activity, Box, Clock, HardDrive, HashIcon, CpuIcon, BinaryIcon, RotateCwIcon, LoaderCircleIcon, CheckCircle2Icon, XCircleIcon, ArrowLeftRightIcon, ArrowUpDownIcon } from "lucide-react"
import { Card, CardHeader, CardTitle, CardDescription } from "@/components/ui/card" import { Card, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet" import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
@@ -23,9 +24,10 @@ import {
TableRow, TableRow,
} from "@/components/ui/table" } from "@/components/ui/table"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { pb } from "@/lib/api" import { pb } from "@/lib/api"
import { SmartData, SmartAttribute } from "@/types" import { SmartData, SmartAttribute } from "@/types"
import { formatBytes, toFixedFloat, formatTemperature } from "@/lib/utils" import { formatBytes, toFixedFloat, formatTemperature, cn } from "@/lib/utils"
import { Trans } from "@lingui/react/macro" import { Trans } from "@lingui/react/macro"
import { ThermometerIcon } from "@/components/ui/icons" import { ThermometerIcon } from "@/components/ui/icons"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
@@ -106,105 +108,82 @@ function convertSmartDataToDiskInfo(smartDataRecord: Record<string, SmartData>):
export const columns: ColumnDef<DiskInfo>[] = [ export const columns: ColumnDef<DiskInfo>[] = [
{ {
accessorKey: "device", accessorKey: "device",
header: () => ( sortingFn: (a, b) => a.original.device.localeCompare(b.original.device),
<div className="flex items-center gap-1.5"> header: ({ column }) => <HeaderButton column={column} name={t`Device`} Icon={HardDrive} />,
<HardDrive className="size-4" />
<Trans>Device</Trans>
</div>
),
cell: ({ row }) => ( cell: ({ row }) => (
<div className="font-medium">{row.getValue("device")}</div> <div className="font-medium ms-1.5">{row.getValue("device")}</div>
), ),
}, },
{ {
accessorKey: "model", accessorKey: "model",
header: () => ( sortingFn: (a, b) => a.original.model.localeCompare(b.original.model),
<div className="flex items-center gap-1.5"> header: ({ column }) => <HeaderButton column={column} name={t`Model`} Icon={Box} />,
<Box className="size-4" />
<Trans>Model</Trans>
</div>
),
cell: ({ row }) => ( cell: ({ row }) => (
<div className="max-w-50 truncate" title={row.getValue("model")}> <div className="max-w-50 truncate ms-1.5" title={row.getValue("model")}>
{row.getValue("model")} {row.getValue("model")}
</div> </div>
), ),
}, },
{ {
accessorKey: "capacity", accessorKey: "capacity",
header: () => ( header: ({ column }) => <HeaderButton column={column} name={t`Capacity`} Icon={BinaryIcon} />,
<div className="flex items-center gap-1.5"> cell: ({ getValue }) => (
<BinaryIcon className="size-4" /> <span className="ms-1.5">{getValue() as string}</span>
<Trans>Capacity</Trans>
</div>
), ),
}, },
{ {
accessorKey: "temperature", accessorKey: "temperature",
header: () => ( invertSorting: true,
<div className="flex items-center gap-2"> header: ({ column }) => <HeaderButton column={column} name={t`Temp`} Icon={ThermometerIcon} />,
<ThermometerIcon className="size-4" />
<Trans>Temp</Trans>
</div>
),
cell: ({ getValue }) => { cell: ({ getValue }) => {
const { value, unit } = formatTemperature(getValue() as number) const { value, unit } = formatTemperature(getValue() as number)
return `${value} ${unit}` return <span className="ms-1.5">{`${value} ${unit}`}</span>
}, },
}, },
{ {
accessorKey: "status", accessorKey: "status",
header: () => ( header: ({ column }) => <HeaderButton column={column} name={t`Status`} Icon={Activity} />,
<div className="flex items-center gap-2">
<Activity className="size-4" />
<Trans>Status</Trans>
</div>
),
cell: ({ getValue }) => { cell: ({ getValue }) => {
const status = getValue() as string const status = getValue() as string
return ( return (
<div className="ms-1.5">
<Badge <Badge
variant={status === "PASSED" ? "success" : status === "FAILED" ? "danger" : "warning"} variant={status === "PASSED" ? "success" : status === "FAILED" ? "danger" : "warning"}
> >
{status} {status}
</Badge> </Badge>
</div>
) )
}, },
}, },
{ {
accessorKey: "deviceType", accessorKey: "deviceType",
header: () => ( sortingFn: (a, b) => a.original.deviceType.localeCompare(b.original.deviceType),
<div className="flex items-center gap-1.5"> header: ({ column }) => <HeaderButton column={column} name={t`Type`} Icon={ArrowLeftRightIcon} />,
<ArrowLeftRightIcon className="size-4" />
<Trans>Type</Trans>
</div>
),
cell: ({ getValue }) => ( cell: ({ getValue }) => (
<div className="ms-1.5">
<Badge variant="outline" className="uppercase"> <Badge variant="outline" className="uppercase">
{getValue() as string} {getValue() as string}
</Badge> </Badge>
</div>
), ),
}, },
{ {
accessorKey: "powerOnHours", accessorKey: "powerOnHours",
header: () => ( invertSorting: true,
<div className="flex items-center gap-1.5"> header: ({ column }) => <HeaderButton column={column} name={t({ message: "Power On", comment: "Power On Time" })} Icon={Clock} />,
<Clock className="size-4" />
<Trans comment="Power On Time">Power On</Trans>
</div>
),
cell: ({ row }) => { cell: ({ row }) => {
const hours = row.getValue("powerOnHours") as number | undefined const hours = row.getValue("powerOnHours") as number | undefined
if (!hours && hours !== 0) { if (!hours && hours !== 0) {
return ( return (
<div className="text-sm text-muted-foreground"> <div className="text-sm text-muted-foreground ms-1.5">
N/A N/A
</div> </div>
) )
} }
const days = Math.floor(hours / 24) const days = Math.floor(hours / 24)
return ( return (
<div className="text-sm"> <div className="text-sm ms-1.5">
<div>{hours.toLocaleString()} hours</div> <div>{hours.toLocaleString()} hours</div>
<div className="text-muted-foreground text-xs">{days} days</div> <div className="text-muted-foreground text-xs">{days} days</div>
</div> </div>
@@ -213,46 +192,55 @@ export const columns: ColumnDef<DiskInfo>[] = [
}, },
{ {
accessorKey: "powerCycles", accessorKey: "powerCycles",
header: () => ( invertSorting: true,
<div className="flex items-center gap-1.5"> header: ({ column }) => <HeaderButton column={column} name={t({ message: "Cycles", comment: "Power Cycles" })} Icon={RotateCwIcon} />,
<RotateCwIcon className="size-4" />
<Trans comment="Power Cycles">Cycles</Trans>
</div>
),
cell: ({ getValue }) => { cell: ({ getValue }) => {
const cycles = getValue() as number | undefined const cycles = getValue() as number | undefined
if (!cycles && cycles !== 0) { if (!cycles && cycles !== 0) {
return ( return (
<div className="text-muted-foreground"> <div className="text-muted-foreground ms-1.5">
N/A N/A
</div> </div>
) )
} }
return cycles return <span className="ms-1.5">{cycles}</span>
}, },
}, },
{ {
accessorKey: "serialNumber", accessorKey: "serialNumber",
header: () => ( sortingFn: (a, b) => a.original.serialNumber.localeCompare(b.original.serialNumber),
<div className="flex items-center gap-1.5"> header: ({ column }) => <HeaderButton column={column} name={t`Serial Number`} Icon={HashIcon} />,
<HashIcon className="size-4" /> cell: ({ getValue }) => (
<Trans>Serial Number</Trans> <span className="ms-1.5">{getValue() as string}</span>
</div>
), ),
}, },
{ {
accessorKey: "firmwareVersion", accessorKey: "firmwareVersion",
header: () => ( sortingFn: (a, b) => a.original.firmwareVersion.localeCompare(b.original.firmwareVersion),
<div className="flex items-center gap-1.5"> header: ({ column }) => <HeaderButton column={column} name={t`Firmware`} Icon={CpuIcon} />,
<CpuIcon className="size-4" /> cell: ({ getValue }) => (
<Trans>Firmware</Trans> <span className="ms-1.5">{getValue() as string}</span>
</div>
), ),
}, },
] ]
function HeaderButton({ column, name, Icon }: { column: Column<DiskInfo>; name: string; Icon: React.ElementType }) {
const isSorted = column.getIsSorted()
return (
<Button
className={cn("h-9 px-3 flex items-center gap-2 duration-50", isSorted && "bg-accent/70 light:bg-accent text-accent-foreground/90")}
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
{Icon && <Icon className="size-4" />}
{name}
<ArrowUpDownIcon className="size-4" />
</Button>
)
}
export default function DisksTable({ systemId }: { systemId: string }) { export default function DisksTable({ systemId }: { systemId: string }) {
// const [sorting, setSorting] = React.useState<SortingState>([{ id: "device", desc: false }]) const [sorting, setSorting] = React.useState<SortingState>([{ id: "device", desc: false }])
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]) const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([])
const [rowSelection, setRowSelection] = React.useState({}) const [rowSelection, setRowSelection] = React.useState({})
const [smartData, setSmartData] = React.useState<Record<string, SmartData> | undefined>(undefined) const [smartData, setSmartData] = React.useState<Record<string, SmartData> | undefined>(undefined)
@@ -284,14 +272,14 @@ export default function DisksTable({ systemId }: { systemId: string }) {
const table = useReactTable({ const table = useReactTable({
data: diskData, data: diskData,
columns: columns, columns: columns,
// onSortingChange: setSorting, onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters, onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(), getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(), getFilteredRowModel: getFilteredRowModel(),
onRowSelectionChange: setRowSelection, onRowSelectionChange: setRowSelection,
state: { state: {
// sorting, sorting,
columnFilters, columnFilters,
rowSelection, rowSelection,
}, },
@@ -331,7 +319,7 @@ export default function DisksTable({ systemId }: { systemId: string }) {
<TableRow key={headerGroup.id}> <TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => { {headerGroup.headers.map((header) => {
return ( return (
<TableHead key={header.id}> <TableHead key={header.id} className="px-2">
{header.isPlaceholder {header.isPlaceholder
? null ? null
: flexRender( : flexRender(