systems table updates

- component refactoring
- style updates for "view" menu and grid
This commit is contained in:
Henry Dollman
2024-12-08 18:08:54 -05:00
parent 5110eaf10f
commit e70de6a59e

View File

@@ -21,6 +21,9 @@ import {
DropdownMenuCheckboxItem, DropdownMenuCheckboxItem,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu" } from "@/components/ui/dropdown-menu"
@@ -50,41 +53,41 @@ import {
HardDriveIcon, HardDriveIcon,
ServerIcon, ServerIcon,
CpuIcon, CpuIcon,
ChevronDownIcon,
LayoutGridIcon, LayoutGridIcon,
LayoutListIcon, LayoutListIcon,
XCircle,
ArrowDownIcon, ArrowDownIcon,
ArrowUpIcon, ArrowUpIcon,
Settings2Icon,
EyeIcon,
} 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, useLocalStorage } 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 { Link, 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 { useLingui } from "@lingui/react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"
import { Input } from "../ui/input" import { Input } from "../ui/input"
import { ClassValue } from "clsx"
type ViewMode = 'table' | 'grid' type ViewMode = "table" | "grid"
function CellFormatter(info: CellContext<SystemRecord, unknown>) { function CellFormatter(info: CellContext<SystemRecord, unknown>) {
const val = info.getValue() as number const val = info.getValue() as number
return ( return (
<div className="flex gap-3 items-center tabular-nums tracking-tight"> <div className="flex gap-2 items-center tabular-nums tracking-tight">
<span className="min-w-[3.5em]">{decimalString(val, 1)}%</span> <span className="min-w-[3.5em]">{decimalString(val, 1)}%</span>
<span className="grow min-w-10 block bg-muted h-[1em] relative rounded-sm overflow-hidden"> <span className="grow min-w-10 block bg-muted h-[1em] relative rounded-sm overflow-hidden">
<span <span
className={cn( className={cn(
"absolute inset-0 w-full h-full origin-left transition-all duration-500", "absolute inset-0 w-full h-full origin-left",
(val < 65 && "bg-green-500") || (val < 90 && "bg-yellow-500") || "bg-red-600" (val < 65 && "bg-green-500") || (val < 90 && "bg-yellow-500") || "bg-red-600"
)} )}
style={{ style={{
transform: `scalex(${val / 100})`, transform: `scalex(${val / 100})`,
transition: "transform 500ms, background-color 500ms"
}} }}
></span> ></span>
</span> </span>
@@ -92,16 +95,17 @@ function CellFormatter(info: CellContext<SystemRecord, unknown>) {
) )
} }
function sortableHeader(column: Column<SystemRecord, unknown>, Icon: any, hideSortIcon = false) { function sortableHeader(column: Column<SystemRecord, unknown>, hideSortIcon = false) {
return ( return (
<Button <Button
variant="ghost" variant="ghost"
className="h-9 px-3 flex" className="h-9 px-3 flex"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")} onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
> >
<Icon className="me-2 h-4 w-4" /> {/* @ts-ignore */}
{column.columnDef?.icon && <column.columnDef.icon className="me-2 size-4" />}
{column.id} {column.id}
{!hideSortIcon && <ArrowUpDownIcon className="ms-2 h-4 w-4" />} {!hideSortIcon && <ArrowUpDownIcon className="ms-2 size-4" />}
</Button> </Button>
) )
} }
@@ -110,10 +114,10 @@ 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 [filter, setFilter] = useState<string>()
const [sorting, setSorting] = useState<SortingState>([]) const [sorting, setSorting] = useState<SortingState>([{ id: t`System`, desc: false }])
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]) const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
const [columnVisibility, setColumnVisibility] = useLocalStorage<VisibilityState>("cols", {}) const [columnVisibility, setColumnVisibility] = useLocalStorage<VisibilityState>("cols", {})
const [viewMode, setViewMode] = useLocalStorage<ViewMode>('viewMode', 'table') const [viewMode, setViewMode] = useLocalStorage<ViewMode>("viewMode", window.innerWidth > 1024 ? "table" : "grid")
const { i18n } = useLingui() const { i18n } = useLingui()
useEffect(() => { useEffect(() => {
@@ -131,19 +135,10 @@ export default function SystemsTable() {
accessorKey: "name", accessorKey: "name",
id: t`System`, id: t`System`,
enableHiding: false, enableHiding: false,
cell: (info) => { icon: ServerIcon,
const { status } = info.row.original cell: (info) => (
return (
<span className="flex gap-0.5 items-center text-base md:pe-5"> <span className="flex gap-0.5 items-center text-base md:pe-5">
<span <IndicatorDot system={info.row.original} />
className={cn("w-2 h-2 left-0 rounded-full", {
"bg-green-500": status === "up",
"bg-red-500": status === "down",
"bg-primary/40": status === "paused",
"bg-yellow-500": status === "pending",
})}
style={{ marginBottom: "-2px" }}
></span>
<Button <Button
data-nolink data-nolink
variant={"ghost"} variant={"ghost"}
@@ -154,43 +149,50 @@ export default function SystemsTable() {
<CopyIcon className="h-2.5 w-2.5" /> <CopyIcon className="h-2.5 w-2.5" />
</Button> </Button>
</span> </span>
) ),
}, header: ({ column }) => sortableHeader(column),
header: ({ column }) => sortableHeader(column, ServerIcon),
}, },
{ {
accessorKey: "info.cpu", accessorKey: "info.cpu",
id: t`CPU`, id: t`CPU`,
invertSorting: true, invertSorting: true,
cell: CellFormatter, cell: CellFormatter,
header: ({ column }) => sortableHeader(column, CpuIcon), icon: CpuIcon,
header: ({ column }) => sortableHeader(column),
}, },
{ {
accessorKey: "info.mp", accessorKey: "info.mp",
id: t`Memory`, id: t`Memory`,
invertSorting: true, invertSorting: true,
cell: CellFormatter, cell: CellFormatter,
header: ({ column }) => sortableHeader(column, MemoryStickIcon), icon: MemoryStickIcon,
header: ({ column }) => sortableHeader(column),
}, },
{ {
accessorKey: "info.dp", accessorKey: "info.dp",
id: t`Disk`, id: t`Disk`,
invertSorting: true, invertSorting: true,
cell: CellFormatter, cell: CellFormatter,
header: ({ column }) => sortableHeader(column, HardDriveIcon), icon: HardDriveIcon,
header: ({ column }) => sortableHeader(column),
}, },
{ {
accessorFn: (originalRow) => originalRow.info.b || 0, accessorFn: (originalRow) => originalRow.info.b || 0,
id: t`Net`, id: t`Net`,
invertSorting: true, invertSorting: true,
size: 115, size: 115,
header: ({ column }) => sortableHeader(column, EthernetIcon), icon: EthernetIcon,
cell: (info) => { header: ({ column }) => sortableHeader(column),
cell(info) {
const val = info.getValue() as number const val = info.getValue() as number
return ( return (
<span className={cn("tabular-nums whitespace-nowrap", { <span
"ps-1": viewMode === 'table', className={cn("tabular-nums whitespace-nowrap", {
})}>{decimalString(val, val >= 100 ? 1 : 2)} MB/s</span> "ps-1": viewMode === "table",
})}
>
{decimalString(val, val >= 100 ? 1 : 2)} MB/s
</span>
) )
}, },
}, },
@@ -199,20 +201,23 @@ export default function SystemsTable() {
id: t`Agent`, id: t`Agent`,
invertSorting: true, invertSorting: true,
size: 50, size: 50,
header: ({ column }) => sortableHeader(column, WifiIcon, true), icon: WifiIcon,
cell: (info) => { header: ({ column }) => sortableHeader(column, true),
cell(info) {
const version = info.getValue() as string const version = info.getValue() as string
if (!version || !hubVersion) { if (!version || !hubVersion) {
return null return null
} }
return ( return (
<span className={cn("flex gap-2 items-center md:pe-5 tabular-nums", {
"ps-1": viewMode === 'table',
})}>
<span <span
className={cn("w-2 h-2 left-0 rounded-full", version === hubVersion ? "bg-green-500" : "bg-yellow-500")} className={cn("flex gap-2 items-center md:pe-5 tabular-nums", {
style={{ marginBottom: "-1px" }} "ps-1": viewMode === "table",
></span> })}
>
<IndicatorDot
system={info.row.original}
className={version === hubVersion ? "bg-green-500" : "bg-yellow-500"}
/>
<span>{info.getValue() as string}</span> <span>{info.getValue() as string}</span>
</span> </span>
) )
@@ -221,83 +226,12 @@ export default function SystemsTable() {
{ {
id: t({ message: "Actions", comment: "Table column" }), id: t({ message: "Actions", comment: "Table column" }),
size: 120, size: 120,
cell: ({ row }) => { cell: ({ row }) => (
const { id, name, status, host } = row.original <div className="flex justify-end items-center gap-1">
return (
<div className={"flex justify-end items-center gap-1"}>
<AlertsButton system={row.original} /> <AlertsButton system={row.original} />
<AlertDialog> <ActionsButton system={row.original} />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size={"icon"} data-nolink>
<span className="sr-only">
<Trans>Open menu</Trans>
</span>
<MoreHorizontalIcon className="w-5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
className={cn(isReadOnlyUser() && "hidden")}
onClick={() => {
pb.collection("systems").update(id, {
status: status === "paused" ? "pending" : "paused",
})
}}
>
{status === "paused" ? (
<>
<PlayCircleIcon className="me-2.5 h-4 w-4" />
<Trans>Resume</Trans>
</>
) : (
<>
<PauseCircleIcon className="me-2.5 h-4 w-4" />
<Trans>Pause</Trans>
</>
)}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => copyToClipboard(host)}>
<CopyIcon className="me-2.5 h-4 w-4" />
<Trans>Copy host</Trans>
</DropdownMenuItem>
<DropdownMenuSeparator className={cn(isReadOnlyUser() && "hidden")} />
<AlertDialogTrigger asChild>
<DropdownMenuItem className={cn(isReadOnlyUser() && "hidden")}>
<Trash2Icon className="me-2.5 h-4 w-4" />
<Trans>Delete</Trans>
</DropdownMenuItem>
</AlertDialogTrigger>
</DropdownMenuContent>
</DropdownMenu>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
<Trans>Are you sure you want to delete {name}?</Trans>
</AlertDialogTitle>
<AlertDialogDescription>
<Trans>
This action cannot be undone. This will permanently delete all current records for {name} from
the database.
</Trans>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>
<Trans>Cancel</Trans>
</AlertDialogCancel>
<AlertDialogAction
className={cn(buttonVariants({ variant: "destructive" }))}
onClick={() => pb.collection("systems").delete(id)}
>
<Trans>Continue</Trans>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div> </div>
) ),
},
}, },
] as ColumnDef<SystemRecord>[] ] as ColumnDef<SystemRecord>[]
}, [hubVersion, i18n.locale]) }, [hubVersion, i18n.locale])
@@ -335,74 +269,80 @@ export default function SystemsTable() {
<Trans>Updated in real time. Click on a system to view information.</Trans> <Trans>Updated in real time. Click on a system to view information.</Trans>
</CardDescription> </CardDescription>
</div> </div>
<div className="flex gap-2 ms-auto w-full md:w-[500px]"> <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" /> <Input placeholder={t`Filter...`} onChange={(e) => setFilter(e.target.value)} className="px-4" />
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="outline"> <Button variant="outline">
<Trans>Options</Trans> <Settings2Icon className="me-1.5 size-4 opacity-80" />
<ChevronDownIcon className="ms-1.5 h-4 w-4 opacity-90" /> <Trans>View</Trans>
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[600px]"> <DropdownMenuContent align="end" className="w-[260px] md:w-[32em] h-72 md:h-auto overflow-y-auto">
<div className="grid grid-cols-3 divide-x"> <div className="grid grid-cols-1 md:grid-cols-3 divide-y md:divide-s md:divide-y-0">
<div className="p-2"> <div>
<DropdownMenuItem className="font-medium" disabled> <DropdownMenuLabel className="pt-2 px-3.5 flex items-center gap-2">
<Trans>View Type</Trans> <LayoutGridIcon className="size-4" />
</DropdownMenuItem> <Trans>Layout</Trans>
<DropdownMenuCheckboxItem </DropdownMenuLabel>
checked={viewMode === 'table'} <DropdownMenuSeparator />
onCheckedChange={() => setViewMode('table')} <DropdownMenuRadioGroup
className="gap-2" className="px-1 pb-1"
value={viewMode}
onValueChange={(view) => setViewMode(view as ViewMode)}
> >
<LayoutListIcon className="h-4 w-4" /> <DropdownMenuRadioItem value="table" onSelect={(e) => e.preventDefault()} className="gap-2">
<LayoutListIcon className="size-4" />
<span>Table</span> <span>Table</span>
</DropdownMenuCheckboxItem> </DropdownMenuRadioItem>
<DropdownMenuCheckboxItem <DropdownMenuRadioItem value="grid" onSelect={(e) => e.preventDefault()} className="gap-2">
checked={viewMode === 'grid'} <LayoutGridIcon className="size-4" />
onCheckedChange={() => setViewMode('grid')}
className="gap-2"
>
<LayoutGridIcon className="h-4 w-4" />
<span>Grid</span> <span>Grid</span>
</DropdownMenuCheckboxItem> </DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</div> </div>
<div className="p-2"> <div>
<DropdownMenuItem className="font-medium" disabled> <DropdownMenuLabel className="pt-2 px-3.5 flex items-center gap-2">
<Trans>Sort by</Trans> <ArrowUpDownIcon className="size-4" />
</DropdownMenuItem> <Trans>Sort By</Trans>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<div className="px-1 pb-1">
{table.getAllColumns().map((column) => { {table.getAllColumns().map((column) => {
if (column.id === t`Actions` || !column.getCanSort()) return null if (column.id === t`Actions` || !column.getCanSort()) return null
let Icon = <span className="w-6"></span>
const isCurrentSort = sorting[0]?.id === column.id // if current sort column, show sort direction
const sortDirection = sorting[0]?.desc ? <ArrowDownIcon className="me-2.5 h-4 w-4" /> : <ArrowUpIcon className="me-2.5 h-4 w-4" /> if (sorting[0]?.id === column.id) {
if (sorting[0]?.desc) {
Icon = <ArrowDownIcon className="me-2 size-4" />
} else {
Icon = <ArrowUpIcon className="me-2 size-4" />
}
}
return ( return (
<DropdownMenuItem <DropdownMenuItem
key={column.id} onSelect={(e) => {
onClick={() => { e.preventDefault()
const isDesc = sorting[0]?.id === column.id && !sorting[0]?.desc setSorting([{ id: column.id, desc: sorting[0]?.id === column.id && !sorting[0]?.desc }])
setSorting([{ id: column.id, desc: isDesc }])
}} }}
key={column.id}
> >
{isCurrentSort && sortDirection} {Icon}
{column.id} {column.id}
</DropdownMenuItem> </DropdownMenuItem>
) )
})} })}
{sorting.length > 0 && ( </div>
<DropdownMenuItem onClick={() => setSorting([])}>
<XCircle className="me-2.5 h-4 w-4" />
<Trans>Clear Sort</Trans>
</DropdownMenuItem>
)}
</div> </div>
<div className="p-2"> <div>
<DropdownMenuItem className="font-medium" disabled> <DropdownMenuLabel className="pt-2 px-3.5 flex items-center gap-2">
<EyeIcon className="size-4" />
<Trans>Visible Fields</Trans> <Trans>Visible Fields</Trans>
</DropdownMenuItem> </DropdownMenuLabel>
<DropdownMenuSeparator />
<div className="px-1.5 pb-1">
{table {table
.getAllColumns() .getAllColumns()
.filter((column) => column.getCanHide()) .filter((column) => column.getCanHide())
@@ -410,6 +350,7 @@ export default function SystemsTable() {
return ( return (
<DropdownMenuCheckboxItem <DropdownMenuCheckboxItem
key={column.id} key={column.id}
onSelect={(e) => e.preventDefault()}
checked={column.getIsVisible()} checked={column.getIsVisible()}
onCheckedChange={(value) => column.toggleVisibility(!!value)} onCheckedChange={(value) => column.toggleVisibility(!!value)}
> >
@@ -419,13 +360,15 @@ export default function SystemsTable() {
})} })}
</div> </div>
</div> </div>
</div>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </div>
</div> </div>
</CardHeader> </CardHeader>
<div className="pb-5 px-2 sm:px-6 max-sm:pt-5 max-sm:pb-1"> <div className="p-6 pt-0 max-sm:py-3 max-sm:px-2">
{viewMode === 'table' ? ( {viewMode === "table" ? (
// table layout
<div className="rounded-md border overflow-hidden"> <div className="rounded-md border overflow-hidden">
<Table> <Table>
<TableHeader> <TableHeader>
@@ -434,7 +377,9 @@ export default function SystemsTable() {
{headerGroup.headers.map((header) => { {headerGroup.headers.map((header) => {
return ( return (
<TableHead className="px-2" key={header.id}> <TableHead className="px-2" key={header.id}>
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} {header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
</TableHead> </TableHead>
) )
})} })}
@@ -481,40 +426,100 @@ export default function SystemsTable() {
</Table> </Table>
</div> </div>
) : ( ) : (
// grid layout
<div className="grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3"> <div className="grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
{table.getRowModel().rows?.length ? ( {table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => ( table.getRowModel().rows.map((row) => {
const system = row.original
const { status } = system
return (
<Card <Card
key={row.original.id} key={system.id}
className={cn("cursor-pointer hover:shadow-md transition-all w-full", { className={cn(
"opacity-50": row.original.status === "paused", "cursor-pointer hover:shadow-md transition-all bg-transparent w-full dark:border-border duration-200 relative",
})} {
onClick={(e) => { "opacity-50": status === "paused",
const target = e.target as HTMLElement
if (!target.closest("[data-nolink]") && e.currentTarget.contains(target)) {
navigate(`/system/${encodeURIComponent(row.original.name)}`)
} }
}} )}
> >
<CardHeader className="pt-4 pb-2"> <CardHeader className="py-1 ps-5 pe-3 bg-muted/30 border-b border-border/60">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2 min-w-0"> <CardTitle className="text-base tracking-normal shrink-1 text-primary/90 flex items-center min-h-10 gap-2.5 min-w-0">
<span <div className="flex items-center gap-2.5 min-w-0">
className={cn("flex-shrink-0 w-2 h-2 rounded-full", { <IndicatorDot system={system} />
"bg-green-500": row.original.status === "up", <CardTitle className="text-[.95em]/normal tracking-normal truncate text-primary/90">
"bg-red-500": row.original.status === "down", {system.name}
"bg-primary/40": row.original.status === "paused",
"bg-yellow-500": row.original.status === "pending",
})}
style={{ marginBottom: "-1px" }}
/>
<CardTitle className="text-base truncate">
{row.original.name}
</CardTitle> </CardTitle>
</div> </div>
</CardTitle>
{table.getColumn(t`Actions`)?.getIsVisible() && ( {table.getColumn(t`Actions`)?.getIsVisible() && (
<div className="flex gap-1 flex-shrink-0"> <div className="flex gap-1 flex-shrink-0 relative z-10">
<AlertsButton system={row.original} /> <AlertsButton system={system} />
<ActionsButton system={system} />
</div>
)}
</div>
</CardHeader>
<CardContent className="space-y-2.5 text-sm px-5 pt-3.5 pb-4">
{table.getAllColumns().map((column) => {
if (!column.getIsVisible() || column.id === t`System` || column.id === t`Actions`) return null
const cell = row.getAllCells().find((cell) => cell.column.id === column.id)
if (!cell) return null
return (
<div key={column.id} className="flex items-center gap-3">
{/* @ts-ignore */}
{column.columnDef?.icon && (
// @ts-ignore
<column.columnDef.icon className="size-4 text-muted-foreground" />
)}
<div className="flex items-center gap-3 flex-1">
<span className="text-muted-foreground min-w-16">{column.id}:</span>
<div className="flex-1">{flexRender(cell.column.columnDef.cell, cell.getContext())}</div>
</div>
</div>
)
})}
</CardContent>
<Link
href={`/system/${encodeURIComponent(row.original.name)}`}
className="inset-0 absolute w-full h-full"
>
<span className="sr-only">{row.original.name}</span>
</Link>
</Card>
)
})
) : (
<div className="col-span-full text-center py-8">
<Trans>No systems found.</Trans>
</div>
)}
</div>
)}
</div>
</Card>
)
}
function IndicatorDot({ system, className }: { system: SystemRecord; className?: ClassValue }) {
className ||= {
"bg-green-500": system.status === "up",
"bg-red-500": system.status === "down",
"bg-primary/40": system.status === "paused",
"bg-yellow-500": system.status === "pending",
}
return (
<span
className={cn("flex-shrink-0 size-2 rounded-full", className)}
// style={{ marginBottom: "-1px" }}
/>
)
}
function ActionsButton({ system }: { system: SystemRecord }) {
// const [opened, setOpened] = useState(false)
const { id, status, host, name } = system
return (
<AlertDialog> <AlertDialog>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
@@ -529,31 +534,31 @@ export default function SystemsTable() {
<DropdownMenuItem <DropdownMenuItem
className={cn(isReadOnlyUser() && "hidden")} className={cn(isReadOnlyUser() && "hidden")}
onClick={() => { onClick={() => {
pb.collection("systems").update(row.original.id, { pb.collection("systems").update(id, {
status: row.original.status === "paused" ? "pending" : "paused", status: status === "paused" ? "pending" : "paused",
}) })
}} }}
> >
{row.original.status === "paused" ? ( {status === "paused" ? (
<> <>
<PlayCircleIcon className="me-2.5 h-4 w-4" /> <PlayCircleIcon className="me-2.5 size-4" />
<Trans>Resume</Trans> <Trans>Resume</Trans>
</> </>
) : ( ) : (
<> <>
<PauseCircleIcon className="me-2.5 h-4 w-4" /> <PauseCircleIcon className="me-2.5 size-4" />
<Trans>Pause</Trans> <Trans>Pause</Trans>
</> </>
)} )}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => copyToClipboard(row.original.host)}> <DropdownMenuItem onClick={() => copyToClipboard(host)}>
<CopyIcon className="me-2.5 h-4 w-4" /> <CopyIcon className="me-2.5 size-4" />
<Trans>Copy host</Trans> <Trans>Copy host</Trans>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator className={cn(isReadOnlyUser() && "hidden")} /> <DropdownMenuSeparator className={cn(isReadOnlyUser() && "hidden")} />
<AlertDialogTrigger asChild> <AlertDialogTrigger asChild>
<DropdownMenuItem className={cn(isReadOnlyUser() && "hidden")}> <DropdownMenuItem className={cn(isReadOnlyUser() && "hidden")}>
<Trash2Icon className="me-2.5 h-4 w-4" /> <Trash2Icon className="me-2.5 size-4" />
<Trans>Delete</Trans> <Trans>Delete</Trans>
</DropdownMenuItem> </DropdownMenuItem>
</AlertDialogTrigger> </AlertDialogTrigger>
@@ -562,12 +567,12 @@ export default function SystemsTable() {
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle> <AlertDialogTitle>
<Trans>Are you sure you want to delete {row.original.name}?</Trans> <Trans>Are you sure you want to delete {name}?</Trans>
</AlertDialogTitle> </AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
<Trans> <Trans>
This action cannot be undone. This will permanently delete all current records for {row.original.name} from This action cannot be undone. This will permanently delete all current records for {name} from the
the database. database.
</Trans> </Trans>
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
@@ -577,57 +582,12 @@ export default function SystemsTable() {
</AlertDialogCancel> </AlertDialogCancel>
<AlertDialogAction <AlertDialogAction
className={cn(buttonVariants({ variant: "destructive" }))} className={cn(buttonVariants({ variant: "destructive" }))}
onClick={() => pb.collection("systems").delete(row.original.id)} onClick={() => pb.collection("systems").delete(id)}
> >
<Trans>Continue</Trans> <Trans>Continue</Trans>
</AlertDialogAction> </AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
</div>
)}
</div>
</CardHeader>
<CardContent className="space-y-2.5 text-sm">
{table.getAllColumns().map((column) => {
if (!column.getIsVisible() || column.id === t`System` || column.id === t`Actions`) return null
const cell = row.getAllCells().find(cell => cell.column.id === column.id)
if (!cell) return null
const icon = (() => {
switch (column.id) {
case t`CPU`: return <CpuIcon className="h-4 w-4 text-muted-foreground" />
case t`Memory`: return <MemoryStickIcon className="h-4 w-4 text-muted-foreground" />
case t`Disk`: return <HardDriveIcon className="h-4 w-4 text-muted-foreground" />
case t`Net`: return <EthernetIcon className="h-4 w-4 text-muted-foreground" />
case t`Agent`: return <WifiIcon className="h-4 w-4 text-muted-foreground" />
default: return null
}
})()
return (
<div key={column.id} className="flex items-center gap-3">
{icon}
<div className="flex items-center gap-3 flex-1">
<span className="text-muted-foreground w-14">{column.id}:</span>
<div className="flex-1">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</div>
</div>
</div>
)
})}
</CardContent>
</Card>
))
) : (
<div className="col-span-full text-center py-8">
<Trans>No systems found.</Trans>
</div>
)}
</div>
)}
</div>
</Card>
) )
} }