From 0234682720b25276cecef854b5c3cfe388ea98e0 Mon Sep 17 00:00:00 2001 From: 0xMMMMMM <4688216+0xMMMMMM@users.noreply.github.com> Date: Fri, 6 Dec 2024 15:09:37 +0700 Subject: [PATCH 1/3] feat: add grid view option for systems table - Add toggle button to switch between table and grid layouts - Implement card-based grid view with system metrics - Add sort dropdown menu for grid view - Display system status, metrics with icons and labels - Improve handling of long system names - Maintain consistent sorting and filtering between views - Persist view preference in localStorage --- .../systems-table/systems-table.tsx | 343 ++++++++++++++---- 1 file changed, 280 insertions(+), 63 deletions(-) diff --git a/beszel/site/src/components/systems-table/systems-table.tsx b/beszel/site/src/components/systems-table/systems-table.tsx index f449ad9..6c8d6eb 100644 --- a/beszel/site/src/components/systems-table/systems-table.tsx +++ b/beszel/site/src/components/systems-table/systems-table.tsx @@ -51,6 +51,8 @@ import { ServerIcon, CpuIcon, ChevronDownIcon, + LayoutGridIcon, + LayoutListIcon, } from "lucide-react" import { useEffect, useMemo, useState } from "react" import { $hubVersion, $systems, pb } from "@/lib/stores" @@ -64,18 +66,23 @@ import { useLingui } from "@lingui/react" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card" import { Input } from "../ui/input" +type ViewMode = 'table' | 'grid' + function CellFormatter(info: CellContext) { const val = info.getValue() as number return ( -
+
{decimalString(val, 1)}%
@@ -103,6 +110,7 @@ export default function SystemsTable() { const [sorting, setSorting] = useState([]) const [columnFilters, setColumnFilters] = useState([]) const [columnVisibility, setColumnVisibility] = useLocalStorage("cols", {}) + const [viewMode, setViewMode] = useLocalStorage('viewMode', 'table') const { i18n } = useLingui() useEffect(() => { @@ -131,7 +139,7 @@ export default function SystemsTable() { "bg-primary/40": status === "paused", "bg-yellow-500": status === "pending", })} - style={{ marginBottom: "-1px" }} + style={{ marginBottom: "-2px" }} >
-
+
setFilter(e.target.value)} className="px-4" /> + + {viewMode === 'grid' && ( + + + + + + {table.getAllColumns().map((column) => { + if (column.id === t`Actions` || !column.getCanSort()) return null + + const isCurrentSort = sorting[0]?.id === column.id + const sortDirection = sorting[0]?.desc ? '↓' : '↑' + + return ( + { + const isDesc = sorting[0]?.id === column.id && !sorting[0]?.desc + setSorting([{ id: column.id, desc: isDesc }]) + }} + > + {column.id} {isCurrentSort && sortDirection} + + ) + })} + {sorting.length > 0 && ( + <> + + setSorting([])}> + Clear Sort + + + )} + + + )} @@ -349,62 +418,210 @@ export default function SystemsTable() {
- -
- - - {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) => ( - + {viewMode === 'table' ? ( +
+
+ + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} + + ) })} - onClick={(e) => { - const target = e.target as HTMLElement - if (!target.closest("[data-nolink]") && e.currentTarget.contains(target)) { - navigate(`/system/${encodeURIComponent(row.original.name)}`) - } - }} - > - {row.getVisibleCells().map((cell) => ( - 10 ? "py-2" : "py-2.5")} - > - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} - )) - ) : ( - - - No systems found. - - - )} - -
-
-
+ ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + { + const target = e.target as HTMLElement + if (!target.closest("[data-nolink]") && e.currentTarget.contains(target)) { + navigate(`/system/${encodeURIComponent(row.original.name)}`) + } + }} + > + {row.getVisibleCells().map((cell) => ( + 10 ? "py-2" : "py-2.5")} + > + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + No systems found. + + + )} + + + + ) : ( +
+ {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + { + const target = e.target as HTMLElement + if (!target.closest("[data-nolink]") && e.currentTarget.contains(target)) { + navigate(`/system/${encodeURIComponent(row.original.name)}`) + } + }} + > + +
+
+ + + {row.original.name} + +
+ {table.getColumn(t`Actions`)?.getIsVisible() && ( +
+ + + + + + + + { + pb.collection("systems").update(row.original.id, { + status: row.original.status === "paused" ? "pending" : "paused", + }) + }} + > + {row.original.status === "paused" ? ( + <> + + Resume + + ) : ( + <> + + Pause + + )} + + copyToClipboard(row.original.host)}> + + Copy host + + + + + + Delete + + + + + + + + Are you sure you want to delete {row.original.name}? + + + + This action cannot be undone. This will permanently delete all current records for {row.original.name} from + the database. + + + + + + Cancel + + pb.collection("systems").delete(row.original.id)} + > + Continue + + + + +
+ )} +
+
+ + {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 + case t`Memory`: return + case t`Disk`: return + case t`Net`: return + case t`Agent`: return + default: return null + } + })() + + return ( +
+ {icon} +
+ {column.id}: +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+
+
+ ) + })} +
+
+ )) + ) : ( +
+ No systems found. +
+ )} +
+ )} + ) } From 5110eaf10f0fa9ead06cf052a00f400396374b6c Mon Sep 17 00:00:00 2001 From: 0xMMMMMM <4688216+0xMMMMMM@users.noreply.github.com> Date: Sun, 8 Dec 2024 06:26:12 +0700 Subject: [PATCH 2/3] refactor: reorganize systems table options into single dropdown Combines view type, sort by and visible fields into a single dropdown menu for better organization. --- .../systems-table/systems-table.tsx | 154 +++++++++--------- 1 file changed, 80 insertions(+), 74 deletions(-) diff --git a/beszel/site/src/components/systems-table/systems-table.tsx b/beszel/site/src/components/systems-table/systems-table.tsx index 6c8d6eb..66d306d 100644 --- a/beszel/site/src/components/systems-table/systems-table.tsx +++ b/beszel/site/src/components/systems-table/systems-table.tsx @@ -53,6 +53,9 @@ import { ChevronDownIcon, LayoutGridIcon, LayoutListIcon, + XCircle, + ArrowDownIcon, + ArrowUpIcon, } from "lucide-react" import { useEffect, useMemo, useState } from "react" import { $hubVersion, $systems, pb } from "@/lib/stores" @@ -334,85 +337,88 @@ export default function SystemsTable() {
setFilter(e.target.value)} className="px-4" /> - - {viewMode === 'grid' && ( - - - - - - {table.getAllColumns().map((column) => { - if (column.id === t`Actions` || !column.getCanSort()) return null - - const isCurrentSort = sorting[0]?.id === column.id - const sortDirection = sorting[0]?.desc ? '↓' : '↑' - - return ( - { - const isDesc = sorting[0]?.id === column.id && !sorting[0]?.desc - setSorting([{ id: column.id, desc: isDesc }]) - }} - > - {column.id} {isCurrentSort && sortDirection} - - ) - })} - {sorting.length > 0 && ( - <> - - setSorting([])}> - Clear Sort - - - )} - - - )} - - {table - .getAllColumns() - .filter((column) => column.getCanHide()) - .map((column) => { - return ( - column.toggleVisibility(!!value)} - > - {column.id} - - ) - })} + +
+
+ + View Type + + setViewMode('table')} + className="gap-2" + > + + Table + + setViewMode('grid')} + className="gap-2" + > + + Grid + +
+ +
+ + Sort by + + {table.getAllColumns().map((column) => { + if (column.id === t`Actions` || !column.getCanSort()) return null + + const isCurrentSort = sorting[0]?.id === column.id + const sortDirection = sorting[0]?.desc ? : + + return ( + { + const isDesc = sorting[0]?.id === column.id && !sorting[0]?.desc + setSorting([{ id: column.id, desc: isDesc }]) + }} + > + {isCurrentSort && sortDirection} + {column.id} + + ) + })} + {sorting.length > 0 && ( + setSorting([])}> + + Clear Sort + + )} +
+ +
+ + Visible Fields + + {table + .getAllColumns() + .filter((column) => column.getCanHide()) + .map((column) => { + return ( + column.toggleVisibility(!!value)} + > + {column.id} + + ) + })} +
+
From e70de6a59e9195961f59b1243a5835ee5a227123 Mon Sep 17 00:00:00 2001 From: Henry Dollman Date: Sun, 8 Dec 2024 18:08:54 -0500 Subject: [PATCH 3/3] systems table updates - component refactoring - style updates for "view" menu and grid --- .../systems-table/systems-table.tsx | 630 ++++++++---------- 1 file changed, 295 insertions(+), 335 deletions(-) diff --git a/beszel/site/src/components/systems-table/systems-table.tsx b/beszel/site/src/components/systems-table/systems-table.tsx index 66d306d..d5a994e 100644 --- a/beszel/site/src/components/systems-table/systems-table.tsx +++ b/beszel/site/src/components/systems-table/systems-table.tsx @@ -21,6 +21,9 @@ import { DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" @@ -50,41 +53,41 @@ import { HardDriveIcon, ServerIcon, CpuIcon, - ChevronDownIcon, LayoutGridIcon, LayoutListIcon, - XCircle, ArrowDownIcon, ArrowUpIcon, + Settings2Icon, + EyeIcon, } from "lucide-react" import { useEffect, useMemo, useState } from "react" import { $hubVersion, $systems, pb } from "@/lib/stores" import { useStore } from "@nanostores/react" import { cn, copyToClipboard, decimalString, isReadOnlyUser, useLocalStorage } from "@/lib/utils" import AlertsButton from "../alerts/alert-button" -import { navigate } from "../router" +import { Link, navigate } from "../router" import { EthernetIcon } from "../ui/icons" 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" +import { ClassValue } from "clsx" -type ViewMode = 'table' | 'grid' +type ViewMode = "table" | "grid" function CellFormatter(info: CellContext) { const val = info.getValue() as number return ( -
+
{decimalString(val, 1)}% @@ -92,16 +95,17 @@ function CellFormatter(info: CellContext) { ) } -function sortableHeader(column: Column, Icon: any, hideSortIcon = false) { +function sortableHeader(column: Column, hideSortIcon = false) { return ( ) } @@ -110,10 +114,10 @@ export default function SystemsTable() { const data = useStore($systems) const hubVersion = useStore($hubVersion) const [filter, setFilter] = useState() - const [sorting, setSorting] = useState([]) + const [sorting, setSorting] = useState([{ id: t`System`, desc: false }]) const [columnFilters, setColumnFilters] = useState([]) const [columnVisibility, setColumnVisibility] = useLocalStorage("cols", {}) - const [viewMode, setViewMode] = useLocalStorage('viewMode', 'table') + const [viewMode, setViewMode] = useLocalStorage("viewMode", window.innerWidth > 1024 ? "table" : "grid") const { i18n } = useLingui() useEffect(() => { @@ -131,66 +135,64 @@ export default function SystemsTable() { accessorKey: "name", id: t`System`, enableHiding: false, - cell: (info) => { - const { status } = info.row.original - return ( - - - - - ) - }, - header: ({ column }) => sortableHeader(column, ServerIcon), + icon: ServerIcon, + cell: (info) => ( + + + + + ), + header: ({ column }) => sortableHeader(column), }, { accessorKey: "info.cpu", id: t`CPU`, invertSorting: true, cell: CellFormatter, - header: ({ column }) => sortableHeader(column, CpuIcon), + icon: CpuIcon, + header: ({ column }) => sortableHeader(column), }, { accessorKey: "info.mp", id: t`Memory`, invertSorting: true, cell: CellFormatter, - header: ({ column }) => sortableHeader(column, MemoryStickIcon), + icon: MemoryStickIcon, + header: ({ column }) => sortableHeader(column), }, { accessorKey: "info.dp", id: t`Disk`, invertSorting: true, cell: CellFormatter, - header: ({ column }) => sortableHeader(column, HardDriveIcon), + icon: HardDriveIcon, + header: ({ column }) => sortableHeader(column), }, { accessorFn: (originalRow) => originalRow.info.b || 0, id: t`Net`, invertSorting: true, size: 115, - header: ({ column }) => sortableHeader(column, EthernetIcon), - cell: (info) => { + icon: EthernetIcon, + header: ({ column }) => sortableHeader(column), + cell(info) { const val = info.getValue() as number return ( - {decimalString(val, val >= 100 ? 1 : 2)} MB/s + + {decimalString(val, val >= 100 ? 1 : 2)} MB/s + ) }, }, @@ -199,20 +201,23 @@ export default function SystemsTable() { id: t`Agent`, invertSorting: true, size: 50, - header: ({ column }) => sortableHeader(column, WifiIcon, true), - cell: (info) => { + icon: WifiIcon, + header: ({ column }) => sortableHeader(column, true), + cell(info) { const version = info.getValue() as string if (!version || !hubVersion) { return null } return ( - - + + {info.getValue() as string} ) @@ -221,83 +226,12 @@ export default function SystemsTable() { { id: t({ message: "Actions", comment: "Table column" }), size: 120, - cell: ({ row }) => { - const { id, name, status, host } = row.original - return ( -
- - - - - - - - { - pb.collection("systems").update(id, { - status: status === "paused" ? "pending" : "paused", - }) - }} - > - {status === "paused" ? ( - <> - - Resume - - ) : ( - <> - - Pause - - )} - - copyToClipboard(host)}> - - Copy host - - - - - - Delete - - - - - - - - Are you sure you want to delete {name}? - - - - This action cannot be undone. This will permanently delete all current records for {name} from - the database. - - - - - - Cancel - - pb.collection("systems").delete(id)} - > - Continue - - - - -
- ) - }, + cell: ({ row }) => ( +
+ + +
+ ), }, ] as ColumnDef[] }, [hubVersion, i18n.locale]) @@ -335,88 +269,96 @@ export default function SystemsTable() { Updated in real time. Click on a system to view information.
-
+
setFilter(e.target.value)} className="px-4" /> - -
-
- - View Type - - setViewMode('table')} - className="gap-2" + +
+
+ + + Layout + + + setViewMode(view as ViewMode)} > - - Table - - setViewMode('grid')} - className="gap-2" - > - - Grid - + e.preventDefault()} className="gap-2"> + + Table + + e.preventDefault()} className="gap-2"> + + Grid + +
-
- - Sort by - - {table.getAllColumns().map((column) => { - if (column.id === t`Actions` || !column.getCanSort()) return null - - const isCurrentSort = sorting[0]?.id === column.id - const sortDirection = sorting[0]?.desc ? : - - return ( - { - const isDesc = sorting[0]?.id === column.id && !sorting[0]?.desc - setSorting([{ id: column.id, desc: isDesc }]) - }} - > - {isCurrentSort && sortDirection} - {column.id} - - ) - })} - {sorting.length > 0 && ( - setSorting([])}> - - Clear Sort - - )} -
- -
- - Visible Fields - - {table - .getAllColumns() - .filter((column) => column.getCanHide()) - .map((column) => { +
+ + + Sort By + + +
+ {table.getAllColumns().map((column) => { + if (column.id === t`Actions` || !column.getCanSort()) return null + let Icon = + // if current sort column, show sort direction + if (sorting[0]?.id === column.id) { + if (sorting[0]?.desc) { + Icon = + } else { + Icon = + } + } return ( - { + e.preventDefault() + setSorting([{ id: column.id, desc: sorting[0]?.id === column.id && !sorting[0]?.desc }]) + }} key={column.id} - checked={column.getIsVisible()} - onCheckedChange={(value) => column.toggleVisibility(!!value)} > + {Icon} {column.id} - + ) })} +
+
+ +
+ + + Visible Fields + + +
+ {table + .getAllColumns() + .filter((column) => column.getCanHide()) + .map((column) => { + return ( + e.preventDefault()} + checked={column.getIsVisible()} + onCheckedChange={(value) => column.toggleVisibility(!!value)} + > + {column.id} + + ) + })} +
@@ -424,8 +366,9 @@ export default function SystemsTable() {
-
- {viewMode === 'table' ? ( +
+ {viewMode === "table" ? ( + // table layout
@@ -434,7 +377,9 @@ export default function SystemsTable() { {headerGroup.headers.map((header) => { return ( - {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} + {header.isPlaceholder + ? null + : flexRender(header.column.columnDef.header, header.getContext())} ) })} @@ -481,145 +426,69 @@ export default function SystemsTable() {
) : ( + // grid layout
{table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - { - const target = e.target as HTMLElement - if (!target.closest("[data-nolink]") && e.currentTarget.contains(target)) { - navigate(`/system/${encodeURIComponent(row.original.name)}`) - } - }} - > - -
-
- - - {row.original.name} + table.getRowModel().rows.map((row) => { + const system = row.original + const { status } = system + return ( + + +
+ +
+ + + {system.name} + +
+ {table.getColumn(t`Actions`)?.getIsVisible() && ( +
+ + +
+ )}
- {table.getColumn(t`Actions`)?.getIsVisible() && ( -
- - - - - - - - { - pb.collection("systems").update(row.original.id, { - status: row.original.status === "paused" ? "pending" : "paused", - }) - }} - > - {row.original.status === "paused" ? ( - <> - - Resume - - ) : ( - <> - - Pause - - )} - - copyToClipboard(row.original.host)}> - - Copy host - - - - - - Delete - - - - - - - - Are you sure you want to delete {row.original.name}? - - - - This action cannot be undone. This will permanently delete all current records for {row.original.name} from - the database. - - - - - - Cancel - - pb.collection("systems").delete(row.original.id)} - > - Continue - - - - -
- )} -
- - - {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 - case t`Memory`: return - case t`Disk`: return - case t`Net`: return - case t`Agent`: return - default: return null - } - })() - - return ( -
- {icon} -
- {column.id}: -
- {flexRender(cell.column.columnDef.cell, cell.getContext())} + + + {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 ( +
+ {/* @ts-ignore */} + {column.columnDef?.icon && ( + // @ts-ignore + + )} +
+ {column.id}: +
{flexRender(cell.column.columnDef.cell, cell.getContext())}
-
- ) - })} - - - )) + ) + })} + + + {row.original.name} + + + ) + }) ) : (
No systems found. @@ -631,3 +500,94 @@ export default function SystemsTable() { ) } + +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 ( + + ) +} + +function ActionsButton({ system }: { system: SystemRecord }) { + // const [opened, setOpened] = useState(false) + const { id, status, host, name } = system + return ( + + + + + + + { + pb.collection("systems").update(id, { + status: status === "paused" ? "pending" : "paused", + }) + }} + > + {status === "paused" ? ( + <> + + Resume + + ) : ( + <> + + Pause + + )} + + copyToClipboard(host)}> + + Copy host + + + + + + Delete + + + + + + + + Are you sure you want to delete {name}? + + + + This action cannot be undone. This will permanently delete all current records for {name} from the + database. + + + + + + Cancel + + pb.collection("systems").delete(id)} + > + Continue + + + + + ) +}