mirror of
https://github.com/henrygd/beszel.git
synced 2025-12-17 02:36:17 +01:00
Add sorting to the smart table (#1333)
This commit is contained in:
@@ -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 (
|
||||||
<Badge
|
<div className="ms-1.5">
|
||||||
variant={status === "PASSED" ? "success" : status === "FAILED" ? "danger" : "warning"}
|
<Badge
|
||||||
>
|
variant={status === "PASSED" ? "success" : status === "FAILED" ? "danger" : "warning"}
|
||||||
{status}
|
>
|
||||||
</Badge>
|
{status}
|
||||||
|
</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 }) => (
|
||||||
<Badge variant="outline" className="uppercase">
|
<div className="ms-1.5">
|
||||||
{getValue() as string}
|
<Badge variant="outline" className="uppercase">
|
||||||
</Badge>
|
{getValue() as string}
|
||||||
|
</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(
|
||||||
|
|||||||
Reference in New Issue
Block a user