add columns filter for systems table

This commit is contained in:
Henry Dollman
2024-11-02 13:35:14 -04:00
parent e4f22ebb01
commit 44747e75b0
2 changed files with 131 additions and 96 deletions

View File

@@ -1,25 +1,22 @@
import { Suspense, lazy, useEffect, useMemo, useState } from "react" import { Suspense, lazy, useEffect, useMemo } from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card" import { Card, CardContent, CardHeader, CardTitle } from "../ui/card"
import { $alerts, $hubVersion, $systems, pb } from "@/lib/stores" import { $alerts, $hubVersion, $systems, pb } from "@/lib/stores"
import { useStore } from "@nanostores/react" import { useStore } from "@nanostores/react"
import { GithubIcon } from "lucide-react" import { GithubIcon } from "lucide-react"
import { Separator } from "../ui/separator" import { Separator } from "../ui/separator"
import { alertInfo, updateRecordList, updateSystemList } from "@/lib/utils" import { alertInfo, updateRecordList, updateSystemList } from "@/lib/utils"
import { AlertRecord, SystemRecord } from "@/types" import { AlertRecord, SystemRecord } from "@/types"
import { Input } from "../ui/input"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { Link } from "../router" import { Link } from "../router"
import { Plural, t, Trans } from "@lingui/macro" import { Plural, t, Trans } from "@lingui/macro"
import { useLingui } from "@lingui/react"
const SystemsTable = lazy(() => import("../systems-table/systems-table")) const SystemsTable = lazy(() => import("../systems-table/systems-table"))
export default function Home() { export default function Home() {
const hubVersion = useStore($hubVersion) const hubVersion = useStore($hubVersion)
const [filter, setFilter] = useState<string>()
const alerts = useStore($alerts) const alerts = useStore($alerts)
const systems = useStore($systems) const systems = useStore($systems)
const { _ } = useLingui()
// todo: maybe remove active alert if changed // todo: maybe remove active alert if changed
const activeAlerts = useMemo(() => { const activeAlerts = useMemo(() => {
@@ -99,30 +96,9 @@ export default function Home() {
</CardContent> </CardContent>
</Card> </Card>
)} )}
<Card>
<CardHeader className="pb-5 px-2 sm:px-6 max-sm:pt-5 max-sm:pb-1">
<div className="grid md:flex gap-5 w-full items-end">
<div className="px-2 sm:px-1">
<CardTitle className="mb-2.5">
<Trans>All Systems</Trans>
</CardTitle>
<CardDescription>
<Trans>Updated in real time. Click on a system to view information.</Trans>
</CardDescription>
</div>
<Input
placeholder={_(t`Filter...`)}
onChange={(e) => setFilter(e.target.value)}
className="w-full md:w-56 lg:w-72 ms-auto px-4"
/>
</div>
</CardHeader>
<CardContent className="max-sm:p-2">
<Suspense> <Suspense>
<SystemsTable filter={filter} /> <SystemsTable />
</Suspense> </Suspense>
</CardContent>
</Card>
{hubVersion && ( {hubVersion && (
<div className="flex gap-1.5 justify-end items-center pe-3 sm:pe-6 mt-3.5 text-xs opacity-80"> <div className="flex gap-1.5 justify-end items-center pe-3 sm:pe-6 mt-3.5 text-xs opacity-80">

View File

@@ -6,6 +6,7 @@ import {
SortingState, SortingState,
getSortedRowModel, getSortedRowModel,
flexRender, flexRender,
VisibilityState,
getCoreRowModel, getCoreRowModel,
useReactTable, useReactTable,
Column, Column,
@@ -17,6 +18,7 @@ import { Button, buttonVariants } from "@/components/ui/button"
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuSeparator, DropdownMenuSeparator,
@@ -48,15 +50,19 @@ import {
HardDriveIcon, HardDriveIcon,
ServerIcon, ServerIcon,
CpuIcon, CpuIcon,
ChevronDownIcon,
} from "lucide-react" } from "lucide-react"
import { useEffect, useMemo, useState } from "react" import { useEffect, useMemo, useState } from "react"
import { $hubVersion, $systems, pb } from "@/lib/stores" import { $hubVersion, $systems, pb } from "@/lib/stores"
import { useStore } from "@nanostores/react" import { useStore } from "@nanostores/react"
import { cn, copyToClipboard, decimalString, isReadOnlyUser } from "@/lib/utils" import { cn, copyToClipboard, decimalString, isReadOnlyUser, useLocalStorage } from "@/lib/utils"
import AlertsButton from "../alerts/alert-button" import AlertsButton from "../alerts/alert-button"
import { navigate } from "../router" import { navigate } from "../router"
import { EthernetIcon } from "../ui/icons" import { EthernetIcon } from "../ui/icons"
import { Trans, t } from "@lingui/macro" import { Trans, t } from "@lingui/macro"
import { useLingui } from "@lingui/react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"
import { Input } from "../ui/input"
function CellFormatter(info: CellContext<SystemRecord, unknown>) { function CellFormatter(info: CellContext<SystemRecord, unknown>) {
const val = info.getValue() as number const val = info.getValue() as number
@@ -76,7 +82,7 @@ function CellFormatter(info: CellContext<SystemRecord, unknown>) {
) )
} }
function sortableHeader(column: Column<SystemRecord, unknown>, name: React.ReactNode, Icon: any, hideSortIcon = false) { function sortableHeader(column: Column<SystemRecord, unknown>, Icon: any, hideSortIcon = false) {
return ( return (
<Button <Button
variant="ghost" variant="ghost"
@@ -84,21 +90,24 @@ function sortableHeader(column: Column<SystemRecord, unknown>, name: React.React
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")} onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
> >
<Icon className="me-2 h-4 w-4" /> <Icon className="me-2 h-4 w-4" />
{name} {column.id}
{!hideSortIcon && <ArrowUpDownIcon className="ms-2 h-4 w-4" />} {!hideSortIcon && <ArrowUpDownIcon className="ms-2 h-4 w-4" />}
</Button> </Button>
) )
} }
export default function SystemsTable({ filter }: { filter?: string }) { export default function SystemsTable() {
const data = useStore($systems) const data = useStore($systems)
const hubVersion = useStore($hubVersion) const hubVersion = useStore($hubVersion)
const [filter, setFilter] = useState<string>()
const [sorting, setSorting] = useState<SortingState>([]) const [sorting, setSorting] = useState<SortingState>([])
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]) const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
const [columnVisibility, setColumnVisibility] = useLocalStorage<VisibilityState>("cols", {})
const { i18n } = useLingui()
useEffect(() => { useEffect(() => {
if (filter !== undefined) { if (filter !== undefined) {
table.getColumn("name")?.setFilterValue(filter) table.getColumn(t`System`)?.setFilterValue(filter)
} }
}, [filter]) }, [filter])
@@ -109,6 +118,8 @@ export default function SystemsTable({ filter }: { filter?: string }) {
size: 200, size: 200,
minSize: 0, minSize: 0,
accessorKey: "name", accessorKey: "name",
id: t`System`,
enableHiding: false,
cell: (info) => { cell: (info) => {
const { status } = info.row.original const { status } = info.row.original
return ( return (
@@ -134,32 +145,35 @@ export default function SystemsTable({ filter }: { filter?: string }) {
</span> </span>
) )
}, },
header: ({ column }) => sortableHeader(column, t`System`, ServerIcon), header: ({ column }) => sortableHeader(column, ServerIcon),
}, },
{ {
accessorKey: "info.cpu", accessorKey: "info.cpu",
id: t`CPU`,
invertSorting: true, invertSorting: true,
cell: CellFormatter, cell: CellFormatter,
header: ({ column }) => sortableHeader(column, t`CPU`, CpuIcon), header: ({ column }) => sortableHeader(column, CpuIcon),
}, },
{ {
accessorKey: "info.mp", accessorKey: "info.mp",
id: t`Memory`,
invertSorting: true, invertSorting: true,
cell: CellFormatter, cell: CellFormatter,
header: ({ column }) => sortableHeader(column, t`Memory`, MemoryStickIcon), header: ({ column }) => sortableHeader(column, MemoryStickIcon),
}, },
{ {
accessorKey: "info.dp", accessorKey: "info.dp",
id: t`Disk`,
invertSorting: true, invertSorting: true,
cell: CellFormatter, cell: CellFormatter,
header: ({ column }) => sortableHeader(column, t`Disk`, HardDriveIcon), header: ({ column }) => sortableHeader(column, HardDriveIcon),
}, },
{ {
accessorFn: (originalRow) => originalRow.info.b || 0, accessorFn: (originalRow) => originalRow.info.b || 0,
id: "n", id: t`Net`,
invertSorting: true, invertSorting: true,
size: 115, size: 115,
header: ({ column }) => sortableHeader(column, t`Net`, EthernetIcon), header: ({ column }) => sortableHeader(column, EthernetIcon),
cell: (info) => { cell: (info) => {
const val = info.getValue() as number const val = info.getValue() as number
return ( return (
@@ -169,9 +183,10 @@ export default function SystemsTable({ filter }: { filter?: string }) {
}, },
{ {
accessorKey: "info.v", accessorKey: "info.v",
id: t`Agent`,
invertSorting: true, invertSorting: true,
size: 50, size: 50,
header: ({ column }) => sortableHeader(column, t`Agent`, WifiIcon, true), header: ({ column }) => sortableHeader(column, WifiIcon, true),
cell: (info) => { cell: (info) => {
const version = info.getValue() as string const version = info.getValue() as string
if (!version || !hubVersion) { if (!version || !hubVersion) {
@@ -189,9 +204,8 @@ export default function SystemsTable({ filter }: { filter?: string }) {
}, },
}, },
{ {
id: "actions", id: t({ message: "Actions", comment: "Table column" }),
size: 120, size: 120,
// minSize: 0,
cell: ({ row }) => { cell: ({ row }) => {
const { id, name, status, host } = row.original const { id, name, status, host } = row.original
return ( return (
@@ -271,7 +285,7 @@ export default function SystemsTable({ filter }: { filter?: string }) {
}, },
}, },
] as ColumnDef<SystemRecord>[] ] as ColumnDef<SystemRecord>[]
}, [hubVersion]) }, [hubVersion, i18n.locale])
const table = useReactTable({ const table = useReactTable({
data, data,
@@ -281,9 +295,11 @@ export default function SystemsTable({ filter }: { filter?: string }) {
getSortedRowModel: getSortedRowModel(), getSortedRowModel: getSortedRowModel(),
onColumnFiltersChange: setColumnFilters, onColumnFiltersChange: setColumnFilters,
getFilteredRowModel: getFilteredRowModel(), getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
state: { state: {
sorting, sorting,
columnFilters, columnFilters,
columnVisibility,
}, },
defaultColumn: { defaultColumn: {
minSize: 0, minSize: 0,
@@ -293,6 +309,47 @@ export default function SystemsTable({ filter }: { filter?: string }) {
}) })
return ( return (
<Card>
<CardHeader className="pb-5 px-2 sm:px-6 max-sm:pt-5 max-sm:pb-1">
<div className="grid md:flex gap-5 w-full items-end">
<div className="px-2 sm:px-1">
<CardTitle className="mb-2.5">
<Trans>All Systems</Trans>
</CardTitle>
<CardDescription>
<Trans>Updated in real time. Click on a system to view information.</Trans>
</CardDescription>
</div>
<div className="flex gap-2 ms-auto w-full md:w-80">
<Input placeholder={t`Filter...`} onChange={(e) => setFilter(e.target.value)} className="px-4" />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">
<Trans comment="Context: table columns">Columns</Trans>{" "}
<ChevronDownIcon className="ms-1.5 h-4 w-4 opacity-90" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{table
.getAllColumns()
.filter((column) => column.getCanHide())
.map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
checked={column.getIsVisible()}
onCheckedChange={(value) => column.toggleVisibility(!!value)}
>
{column.id}
</DropdownMenuCheckboxItem>
)
})}
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</CardHeader>
<CardContent className="max-sm:p-2">
<div className="rounded-md border overflow-hidden"> <div className="rounded-md border overflow-hidden">
<Table> <Table>
<TableHeader className="bg-muted/40"> <TableHeader className="bg-muted/40">
@@ -347,5 +404,7 @@ export default function SystemsTable({ filter }: { filter?: string }) {
</TableBody> </TableBody>
</Table> </Table>
</div> </div>
</CardContent>
</Card>
) )
} }