import { Trans, useLingui } from "@lingui/react/macro" import { useStore } from "@nanostores/react" import { subscribeKeys } from "nanostores" import { getPagePath } from "@nanostores/router" import { type ColumnDef, type ColumnFiltersState, flexRender, getCoreRowModel, getFilteredRowModel, getSortedRowModel, type Row, type SortingState, type Table as TableType, useReactTable, type VisibilityState, } from "@tanstack/react-table" import { useVirtualizer, type VirtualItem } from "@tanstack/react-virtual" import { ArrowDownIcon, ArrowUpDownIcon, ArrowUpIcon, EyeIcon, FilterIcon, LayoutGridIcon, LayoutListIcon, Settings2Icon, XIcon, } from "lucide-react" import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react" import { Button } from "@/components/ui/button" import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { Input } from "@/components/ui/input" import { TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import { SystemStatus } from "@/lib/enums" import { queueUserSettings } from "@/lib/api" import { $downSystems, $pausedSystems, $systems, $upSystems, $userSettings } from "@/lib/stores" import { cn, runOnce } from "@/lib/utils" import type { SystemRecord } from "@/types" import AlertButton from "../alerts/alert-button" import { $router, Link } from "../router" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card" import { SystemsTableColumns, ActionsButton, IndicatorDot } from "./systems-table-columns" type ViewMode = "table" | "grid" type StatusFilter = "all" | SystemRecord["status"] const preloadSystemDetail = runOnce(() => import("@/components/routes/system.tsx")) export default function SystemsTable() { const data = useStore($systems) const downSystems = $downSystems.get() const upSystems = $upSystems.get() const pausedSystems = $pausedSystems.get() const { i18n, t } = useLingui() const [filter, setFilter] = useState("") const [statusFilter, setStatusFilter] = useState( () => $userSettings.get().statusFilter ?? (JSON.parse(localStorage.getItem("besz-statusFilter") || "null") as StatusFilter | null) ?? "all" ) const [sorting, setSorting] = useState( () => $userSettings.get().sortMode ?? JSON.parse(sessionStorage.getItem("besz-sortMode") || "null") ?? [{ id: "system", desc: false }] ) const [columnFilters, setColumnFilters] = useState([]) const [columnVisibility, setColumnVisibility] = useState( () => $userSettings.get().cols ?? JSON.parse(localStorage.getItem("besz-cols") || "{}") ) // Apply settings from server once they load (handles incognito / new devices) const applied = useRef(new Set()) useEffect(() => { return subscribeKeys($userSettings, ["cols", "statusFilter", "viewMode", "sortMode"], (vals) => { if (!applied.current.has("cols") && vals.cols !== undefined) { applied.current.add("cols") setColumnVisibility(vals.cols) } if (!applied.current.has("statusFilter") && vals.statusFilter !== undefined) { applied.current.add("statusFilter") setStatusFilter(vals.statusFilter) } if (!applied.current.has("viewMode") && vals.viewMode !== undefined) { applied.current.add("viewMode") setViewMode(vals.viewMode) } if (!applied.current.has("sortMode") && vals.sortMode !== undefined) { applied.current.add("sortMode") setSorting(vals.sortMode) } }) }, []) const handleColumnVisibilityChange = useCallback( (updater: VisibilityState | ((prev: VisibilityState) => VisibilityState)) => { setColumnVisibility((prev) => { const next = typeof updater === "function" ? updater(prev) : updater localStorage.setItem("besz-cols", JSON.stringify(next)) $userSettings.setKey("cols", next) queueUserSettings({ cols: next }) return next }) }, [] ) const handleStatusFilterChange = useCallback((value: string) => { const next = value as StatusFilter setStatusFilter(next) localStorage.setItem("besz-statusFilter", JSON.stringify(next)) $userSettings.setKey("statusFilter", next) queueUserSettings({ statusFilter: next }) }, []) const handleViewModeChange = useCallback((view: string) => { const next = view as ViewMode setViewMode(next) localStorage.setItem("besz-viewMode", JSON.stringify(next)) $userSettings.setKey("viewMode", next) queueUserSettings({ viewMode: next }) }, []) const handleSortingChange = useCallback((updater: SortingState | ((prev: SortingState) => SortingState)) => { setSorting((prev) => { const next = typeof updater === "function" ? updater(prev) : updater sessionStorage.setItem("besz-sortMode", JSON.stringify(next)) $userSettings.setKey("sortMode", next) queueUserSettings({ sortMode: next }) return next }) }, []) const locale = i18n.locale // Filter data based on status filter const filteredData = useMemo(() => { if (statusFilter === "all") { return data } if (statusFilter === SystemStatus.Up) { return Object.values(upSystems) ?? [] } if (statusFilter === SystemStatus.Down) { return Object.values(downSystems) ?? [] } return Object.values(pausedSystems) ?? [] }, [data, statusFilter]) const [viewMode, setViewMode] = useState( () => $userSettings.get().viewMode ?? (JSON.parse(localStorage.getItem("besz-viewMode") || "null") as ViewMode | null) ?? // show grid view on mobile if there are less than 200 systems (looks better but table is more efficient) (window.innerWidth < 1024 && filteredData.length < 200 ? "grid" : "table") ) useEffect(() => { if (filter !== undefined) { table.getColumn("system")?.setFilterValue(filter) } }, [filter]) const columnDefs = useMemo(() => SystemsTableColumns(viewMode), [viewMode]) const table = useReactTable({ data: filteredData, columns: columnDefs, getCoreRowModel: getCoreRowModel(), onSortingChange: handleSortingChange, getSortedRowModel: getSortedRowModel(), onColumnFiltersChange: setColumnFilters, getFilteredRowModel: getFilteredRowModel(), onColumnVisibilityChange: handleColumnVisibilityChange, state: { sorting, columnFilters, columnVisibility, }, defaultColumn: { invertSorting: true, sortUndefined: "last", minSize: 0, size: 900, maxSize: 900, }, }) const rows = table.getRowModel().rows const columns = table.getAllColumns() const visibleColumns = table.getVisibleLeafColumns() const [upSystemsLength, downSystemsLength, pausedSystemsLength] = useMemo(() => { return [Object.values(upSystems).length, Object.values(downSystems).length, Object.values(pausedSystems).length] }, [upSystems, downSystems, pausedSystems]) const CardHead = useMemo(() => { return (
All Systems Click on a system to view more information.
setFilter(e.target.value)} value={filter} className="ps-4 pe-10 w-full" /> {filter && ( )}
Layout e.preventDefault()} className="gap-2"> Table e.preventDefault()} className="gap-2"> Grid
Status e.preventDefault()}> All Systems e.preventDefault()}> Up ({upSystemsLength}) e.preventDefault()}> Down ({downSystemsLength}) e.preventDefault()}> Paused ({pausedSystemsLength})
Sort By
{columns.map((column) => { if (!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() handleSortingChange([ { id: column.id, desc: sorting[0]?.id === column.id && !sorting[0]?.desc }, ]) }} key={column.id} > {Icon} {/* @ts-ignore */} {column.columnDef.name()} ) })}
Visible Fields
{columns .filter((column) => column.getCanHide()) .map((column) => { return ( e.preventDefault()} checked={column.getIsVisible()} onCheckedChange={(value) => column.toggleVisibility(!!value)} > {/* @ts-ignore */} {column.columnDef.name()} ) })}
) }, [ visibleColumns.length, sorting, viewMode, locale, statusFilter, upSystemsLength, downSystemsLength, pausedSystemsLength, filter, ]) return ( {CardHead} {viewMode === "table" ? ( // table layout
) : ( // grid layout
{rows?.length ? ( rows.map((row) => { return }) ) : (
No systems found.
)}
)}
) } const AllSystemsTable = memo( ({ table, rows, colLength }: { table: TableType; rows: Row[]; colLength: number }) => { // The virtualizer will need a reference to the scrollable container element const scrollRef = useRef(null) const virtualizer = useVirtualizer({ count: rows.length, estimateSize: () => (rows.length > 10 ? 56 : 60), getScrollElement: () => scrollRef.current, overscan: 5, }) const virtualRows = virtualizer.getVirtualItems() const paddingTop = Math.max(0, virtualRows[0]?.start ?? 0 - virtualizer.options.scrollMargin) const paddingBottom = Math.max(0, virtualizer.getTotalSize() - (virtualRows[virtualRows.length - 1]?.end ?? 0)) return (
2) && "min-h-50" )} ref={scrollRef} > {/* add header height to table size */}
{rows.length ? ( virtualRows.map((virtualRow) => { const row = rows[virtualRow.index] as Row return ( ) }) ) : ( No systems found. )}
) } ) function SystemsTableHead({ table }: { table: TableType }) { const { t } = useLingui() return ( {table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => { return ( {flexRender(header.column.columnDef.header, header.getContext())} ) })} ))} ) } const SystemTableRow = memo( ({ row, virtualRow, colLength, }: { row: Row virtualRow: VirtualItem length: number colLength: number }) => { const system = row.original const { t } = useLingui() return useMemo(() => { return ( {row.getVisibleCells().map((cell) => ( {flexRender(cell.column.columnDef.cell, cell.getContext())} ))} ) }, [system, system.status, colLength, t]) } ) const SystemCard = memo( ({ row, table, colLength }: { row: Row; table: TableType; colLength: number }) => { const system = row.original const { t } = useLingui() return useMemo(() => { return (

{system.name}

{table.getColumn("actions")?.getIsVisible() && (
)}
{table.getAllColumns().map((column) => { if (!column.getIsVisible() || column.id === "system" || column.id === "actions") return null const cell = row.getAllCells().find((cell) => cell.column.id === column.id) if (!cell) return null // @ts-expect-error const { Icon, name } = column.columnDef as ColumnDef return ( <>
{column.id === "lastSeen" ? ( ) : ( Icon && )}
{name()}:
{flexRender(cell.column.columnDef.cell, cell.getContext())}
) })}
{row.original.name}
) }, [system, colLength, t]) } )