mirror of
https://github.com/henrygd/beszel.git
synced 2026-09-24 02:17:47 +02:00
feat(ui): Add view settings to network monitors table (#2396)
This commit is contained in:
@@ -23,7 +23,7 @@ import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { ChevronDownIcon, ListIcon, SearchIcon, ServerIcon } from "lucide-react"
|
||||
import { ChevronDownIcon, ListIcon, PlusIcon, SearchIcon, ServerIcon } from "lucide-react"
|
||||
import { useToast } from "@/components/ui/use-toast"
|
||||
import { $systems } from "@/lib/stores"
|
||||
import { cn, supportsNetworkMonitors } from "@/lib/utils"
|
||||
@@ -446,8 +446,13 @@ export function AddMonitorDialog({ systemId, monitors }: { systemId?: string; mo
|
||||
<>
|
||||
<div className="flex gap-0 rounded-lg">
|
||||
<Button variant="outline" onClick={openAdd} className="rounded-e-none grow" disabled={!hasEligibleSystems}>
|
||||
{/* <PlusIcon className="size-4 me-1" /> */}
|
||||
<Trans>Add {{ foo: t`Monitor` }}</Trans>
|
||||
<PlusIcon className="size-4 me-1" />
|
||||
<span className="sm:hidden">
|
||||
<Trans>Add</Trans>
|
||||
</span>
|
||||
<span className="hidden sm:inline">
|
||||
<Trans>Add {{ foo: t`Monitor` }}</Trans>
|
||||
</span>
|
||||
</Button>
|
||||
<div className="w-px h-full bg-muted"></div>
|
||||
<DropdownMenu>
|
||||
|
||||
@@ -40,6 +40,12 @@ import { Badge } from "../ui/badge"
|
||||
import { getMonitorTarget } from "@/lib/network-monitor-utils"
|
||||
import { pb } from "@/lib/api"
|
||||
|
||||
declare module "@tanstack/react-table" {
|
||||
interface ColumnMeta<TData, TValue> {
|
||||
label?: string
|
||||
}
|
||||
}
|
||||
|
||||
const protocolColors: Record<string, string> = {
|
||||
icmp: "bg-blue-500/15! text-blue-600 dark:text-blue-400",
|
||||
tcp: "bg-purple-500/15! text-purple-600 dark:text-purple-400",
|
||||
@@ -98,6 +104,7 @@ export function getMonitorColumns(
|
||||
},
|
||||
{
|
||||
id: "system",
|
||||
meta: { label: t`System` },
|
||||
accessorFn: (record) => record.system,
|
||||
sortingFn: (a, b) => {
|
||||
const allSystems = $allSystemsById.get()
|
||||
@@ -134,6 +141,7 @@ export function getMonitorColumns(
|
||||
},
|
||||
{
|
||||
id: "target",
|
||||
meta: { label: t`Target` },
|
||||
sortingFn: (a, b) => a.original.target.localeCompare(b.original.target),
|
||||
accessorFn: (record) => getMonitorTarget(record),
|
||||
header: ({ column }) => <HeaderButton column={column} name={t`Target`} Icon={GlobeIcon} />,
|
||||
@@ -164,6 +172,7 @@ export function getMonitorColumns(
|
||||
},
|
||||
{
|
||||
id: "protocol",
|
||||
meta: { label: t`Protocol` },
|
||||
accessorFn: (record) => record.protocol,
|
||||
header: ({ column }) => <HeaderButton column={column} name={t`Protocol`} Icon={ArrowLeftRightIcon} />,
|
||||
cell: ({ getValue }) => {
|
||||
@@ -173,6 +182,7 @@ export function getMonitorColumns(
|
||||
},
|
||||
{
|
||||
id: "interval",
|
||||
meta: { label: t`Interval` },
|
||||
accessorFn: (record) => record.interval,
|
||||
invertSorting: true,
|
||||
header: ({ column }) => <HeaderButton column={column} name={t`Interval`} Icon={RefreshCwIcon} />,
|
||||
@@ -180,6 +190,7 @@ export function getMonitorColumns(
|
||||
},
|
||||
{
|
||||
id: "res",
|
||||
meta: { label: t`Response` },
|
||||
accessorFn: (record) => record.res,
|
||||
invertSorting: true,
|
||||
header: ({ column }) => <HeaderButton column={column} name={t`Response`} Icon={TimerIcon} />,
|
||||
@@ -187,6 +198,7 @@ export function getMonitorColumns(
|
||||
},
|
||||
{
|
||||
id: "res1h",
|
||||
meta: { label: t`Avg 1h` },
|
||||
accessorFn: (record) => record.resAvg1h,
|
||||
invertSorting: true,
|
||||
header: ({ column }) => <HeaderButton column={column} name={t`Avg 1h`} Icon={TimerIcon} />,
|
||||
@@ -194,6 +206,7 @@ export function getMonitorColumns(
|
||||
},
|
||||
{
|
||||
id: "max1h",
|
||||
meta: { label: t`Max 1h` },
|
||||
accessorFn: (record) => record.resMax1h,
|
||||
invertSorting: true,
|
||||
header: ({ column }) => <HeaderButton column={column} name={t`Max 1h`} Icon={TimerIcon} />,
|
||||
@@ -201,6 +214,7 @@ export function getMonitorColumns(
|
||||
},
|
||||
{
|
||||
id: "min1h",
|
||||
meta: { label: t`Min 1h` },
|
||||
accessorFn: (record) => record.resMin1h,
|
||||
invertSorting: true,
|
||||
header: ({ column }) => <HeaderButton column={column} name={t`Min 1h`} Icon={TimerIcon} />,
|
||||
@@ -208,6 +222,7 @@ export function getMonitorColumns(
|
||||
},
|
||||
{
|
||||
id: "loss",
|
||||
meta: { label: t`Loss 1h` },
|
||||
accessorFn: (record) => record.loss1h,
|
||||
invertSorting: true,
|
||||
header: ({ column }) => <HeaderButton column={column} name={t`Loss 1h`} Icon={WifiOffIcon} />,
|
||||
@@ -236,6 +251,7 @@ export function getMonitorColumns(
|
||||
},
|
||||
{
|
||||
id: "updated",
|
||||
meta: { label: t`Updated` },
|
||||
invertSorting: true,
|
||||
accessorFn: (record) => record.updated,
|
||||
header: ({ column }) => <HeaderButton column={column} name={t`Updated`} Icon={ClockIcon} />,
|
||||
|
||||
@@ -26,27 +26,47 @@ import {
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog"
|
||||
import { Button, buttonVariants } from "@/components/ui/button"
|
||||
import { memo, useCallback, useMemo, useRef, useState } from "react"
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { subscribeKeys } from "nanostores"
|
||||
import { getMonitorColumns } from "@/components/network-monitors-table/network-monitors-columns"
|
||||
import { Card, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { useToast } from "@/components/ui/use-toast"
|
||||
import { isReadOnlyUser } from "@/lib/api"
|
||||
import { isReadOnlyUser, queueUserSettings } from "@/lib/api"
|
||||
import { pb } from "@/lib/api"
|
||||
import { SystemStatus } from "@/lib/enums"
|
||||
import { $allSystemsById, $direction, getUserChartTime } from "@/lib/stores"
|
||||
import { $allSystemsById, $direction, $userSettings, getUserChartTime } from "@/lib/stores"
|
||||
import {
|
||||
cn,
|
||||
isVisuallyLonger,
|
||||
matchesFilterGroups,
|
||||
parseFilterGroups,
|
||||
parseSemVer,
|
||||
useBrowserStorage,
|
||||
} from "@/lib/utils"
|
||||
import type { ChartData, NetworkMonitorRecord } from "@/types"
|
||||
import { AddMonitorDialog, EditMonitorDialog } from "./monitor-dialog"
|
||||
import { ArrowLeftRightIcon, EthernetPortIcon, LoaderCircleIcon, ServerIcon, XIcon } from "lucide-react"
|
||||
import {
|
||||
ArrowDownIcon,
|
||||
ArrowLeftRightIcon,
|
||||
ArrowUpDownIcon,
|
||||
ArrowUpIcon,
|
||||
EthernetPortIcon,
|
||||
EyeIcon,
|
||||
LoaderCircleIcon,
|
||||
ServerIcon,
|
||||
Settings2Icon,
|
||||
XIcon,
|
||||
} from "lucide-react"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet"
|
||||
import ChartTimeSelect from "@/components/charts/chart-time-select"
|
||||
import { LossChart, AvgMinMaxResponseChart } from "@/components/routes/system/charts/monitors-charts"
|
||||
@@ -66,13 +86,19 @@ export default function NetworkMonitorsTableNew({
|
||||
monitors: NetworkMonitorRecord[]
|
||||
isLoading: boolean
|
||||
}) {
|
||||
const [sorting, setSorting] = useBrowserStorage<SortingState>(
|
||||
`sort-np-target-${systemId ? 1 : 0}`,
|
||||
[{ id: systemId ? "target" : "system", desc: false }],
|
||||
sessionStorage
|
||||
const sortSettingsKey = systemId ? "monitorSortModeSystem" : "monitorSortMode"
|
||||
const sortStorageKey = `besz-sort-np-target-${systemId ? 1 : 0}`
|
||||
const [sorting, setSorting] = useState<SortingState>(
|
||||
() =>
|
||||
$userSettings.get()[sortSettingsKey] ??
|
||||
JSON.parse(sessionStorage.getItem(sortStorageKey) || "null") ?? [
|
||||
{ id: systemId ? "target" : "system", desc: false },
|
||||
]
|
||||
)
|
||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
|
||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({})
|
||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>(
|
||||
() => $userSettings.get().monitorCols ?? JSON.parse(localStorage.getItem("besz-monitor-cols") || "{}")
|
||||
)
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||
const [globalFilter, setGlobalFilter] = useState("")
|
||||
const [deleteOpen, setDeleteOpen] = useState(false)
|
||||
@@ -82,6 +108,47 @@ export default function NetworkMonitorsTableNew({
|
||||
const { toast } = useToast()
|
||||
const canManageMonitors = !isReadOnlyUser()
|
||||
|
||||
// Apply settings from server once they load (handles incognito / new devices)
|
||||
const appliedSettings = useRef(new Set<string>())
|
||||
useEffect(() => {
|
||||
return subscribeKeys($userSettings, ["monitorCols", sortSettingsKey], (vals) => {
|
||||
if (!appliedSettings.current.has("monitorCols") && vals.monitorCols !== undefined) {
|
||||
appliedSettings.current.add("monitorCols")
|
||||
setColumnVisibility(vals.monitorCols)
|
||||
}
|
||||
if (!appliedSettings.current.has(sortSettingsKey) && vals[sortSettingsKey] !== undefined) {
|
||||
appliedSettings.current.add(sortSettingsKey)
|
||||
setSorting(vals[sortSettingsKey] as SortingState)
|
||||
}
|
||||
})
|
||||
}, [sortSettingsKey])
|
||||
|
||||
const handleColumnVisibilityChange = useCallback(
|
||||
(updater: VisibilityState | ((prev: VisibilityState) => VisibilityState)) => {
|
||||
setColumnVisibility((prev) => {
|
||||
const next = typeof updater === "function" ? updater(prev) : updater
|
||||
localStorage.setItem("besz-monitor-cols", JSON.stringify(next))
|
||||
$userSettings.setKey("monitorCols", next)
|
||||
queueUserSettings({ monitorCols: next })
|
||||
return next
|
||||
})
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const handleSortingChange = useCallback(
|
||||
(updater: SortingState | ((prev: SortingState) => SortingState)) => {
|
||||
setSorting((prev) => {
|
||||
const next = typeof updater === "function" ? updater(prev) : updater
|
||||
sessionStorage.setItem(sortStorageKey, JSON.stringify(next))
|
||||
$userSettings.setKey(sortSettingsKey, next)
|
||||
queueUserSettings({ [sortSettingsKey]: next })
|
||||
return next
|
||||
})
|
||||
},
|
||||
[sortSettingsKey, sortStorageKey]
|
||||
)
|
||||
|
||||
const longestTarget = useMemo(() => {
|
||||
let longestTarget = ""
|
||||
for (const p of monitors) {
|
||||
@@ -208,9 +275,9 @@ export default function NetworkMonitorsTableNew({
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
onSortingChange: setSorting,
|
||||
onSortingChange: handleSortingChange,
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
onColumnVisibilityChange: setColumnVisibility,
|
||||
onColumnVisibilityChange: handleColumnVisibilityChange,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
defaultColumn: {
|
||||
sortUndefined: "last",
|
||||
@@ -237,11 +304,12 @@ export default function NetworkMonitorsTableNew({
|
||||
|
||||
const rows = table.getRowModel().rows
|
||||
const visibleColumns = table.getVisibleLeafColumns()
|
||||
const visibleColumnsKey = visibleColumns.map((column) => column.id).join(",")
|
||||
|
||||
return (
|
||||
<Card className="@container w-full px-3 py-5 sm:py-6 sm:px-6">
|
||||
<CardHeader className="p-0 mb-3 sm:mb-4">
|
||||
<div className="grid md:flex gap-x-5 gap-y-3 w-full items-end">
|
||||
<div className="grid md-lg:flex gap-x-5 gap-y-3 w-full items-end">
|
||||
<div className="px-2 sm:px-1">
|
||||
<CardTitle className="mb-2">
|
||||
<Trans>Network Monitors</Trans>
|
||||
@@ -250,14 +318,14 @@ export default function NetworkMonitorsTableNew({
|
||||
<Trans>Response time monitoring from agents.</Trans>
|
||||
</div>
|
||||
</div>
|
||||
<div className="md:ms-auto flex items-center gap-2">
|
||||
<div className="md-lg:ms-auto flex items-center gap-2">
|
||||
{monitors.length > 0 && (
|
||||
<div className="relative">
|
||||
<div className="relative grow">
|
||||
<Input
|
||||
placeholder={t`Filter...`}
|
||||
value={globalFilter}
|
||||
onChange={(e) => setGlobalFilter(e.target.value)}
|
||||
className="ms-auto px-4 w-full max-w-full md:w-50"
|
||||
className="ms-auto px-4 w-full max-w-full md-lg:w-50"
|
||||
/>
|
||||
{globalFilter && (
|
||||
<Button
|
||||
@@ -273,6 +341,74 @@ export default function NetworkMonitorsTableNew({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<Settings2Icon className="me-1.5 size-4 opacity-80" />
|
||||
<Trans>View</Trans>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="h-72 md:h-auto min-w-48 md:min-w-auto overflow-y-auto">
|
||||
<div className="grid grid-cols-2 divide-y md:divide-s md:divide-y-0">
|
||||
<div className="border-r">
|
||||
<DropdownMenuLabel className="pt-2 px-3.5 flex items-center gap-2">
|
||||
<ArrowUpDownIcon className="size-4" />
|
||||
<Trans>Sort By</Trans>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<div className="px-1 pb-1">
|
||||
{table.getAllColumns().map((column) => {
|
||||
if (!column.getCanSort()) return null
|
||||
let Icon = <span className="w-6"></span>
|
||||
if (sorting[0]?.id === column.id) {
|
||||
Icon = sorting[0]?.desc ? (
|
||||
<ArrowUpIcon className="me-2 size-4" />
|
||||
) : (
|
||||
<ArrowDownIcon className="me-2 size-4" />
|
||||
)
|
||||
}
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onSelect={(e) => {
|
||||
e.preventDefault()
|
||||
handleSortingChange([
|
||||
{ id: column.id, desc: sorting[0]?.id === column.id && !sorting[0]?.desc },
|
||||
])
|
||||
}}
|
||||
key={column.id}
|
||||
>
|
||||
{Icon}
|
||||
{column.columnDef.meta?.label ?? column.id}
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<DropdownMenuLabel className="pt-2 px-3.5 flex items-center gap-2">
|
||||
<EyeIcon className="size-4" />
|
||||
<Trans>Visible Fields</Trans>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<div className="px-1.5 pb-1">
|
||||
{table
|
||||
.getAllColumns()
|
||||
.filter((column) => column.getCanHide())
|
||||
.map((column) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={column.id}
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
checked={column.getIsVisible()}
|
||||
onCheckedChange={(value) => column.toggleVisibility(!!value)}
|
||||
>
|
||||
{column.columnDef.meta?.label ?? column.id}
|
||||
</DropdownMenuCheckboxItem>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
{canManageMonitors ? <AddMonitorDialog systemId={systemId} monitors={monitors} /> : null}
|
||||
{canManageMonitors ? (
|
||||
<EditMonitorDialog
|
||||
@@ -325,6 +461,7 @@ export default function NetworkMonitorsTableNew({
|
||||
table={table}
|
||||
rows={rows}
|
||||
colLength={visibleColumns.length}
|
||||
visibleColumnsKey={visibleColumnsKey}
|
||||
rowSelection={rowSelection}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
@@ -337,12 +474,14 @@ const NetworkMonitorsTable = memo(function NetworkMonitorTable({
|
||||
table,
|
||||
rows,
|
||||
colLength,
|
||||
visibleColumnsKey,
|
||||
rowSelection,
|
||||
isLoading,
|
||||
}: {
|
||||
table: TableType<NetworkMonitorRecord>
|
||||
rows: Row<NetworkMonitorRecord>[]
|
||||
colLength: number
|
||||
visibleColumnsKey: string
|
||||
rowSelection: RowSelectionState
|
||||
isLoading: boolean
|
||||
}) {
|
||||
@@ -390,6 +529,7 @@ const NetworkMonitorsTable = memo(function NetworkMonitorTable({
|
||||
virtualRow={virtualRow}
|
||||
isSelected={row.getIsSelected()}
|
||||
rowSelection={rowSelection}
|
||||
visibleColumnsKey={visibleColumnsKey}
|
||||
openSheet={openSheet}
|
||||
/>
|
||||
)
|
||||
@@ -442,6 +582,9 @@ const NetworkMonitorTableRow = memo(function NetworkMonitorTableRow({
|
||||
virtualRow,
|
||||
isSelected,
|
||||
rowSelection: _rowSelection,
|
||||
// Column visibility doesn't change the row object identity, so this prop exists only
|
||||
// to force a re-render (and a fresh row.getVisibleCells() read) when columns are toggled.
|
||||
visibleColumnsKey: _visibleColumnsKey,
|
||||
openSheet,
|
||||
}: {
|
||||
row: Row<NetworkMonitorRecord>
|
||||
@@ -449,6 +592,7 @@ const NetworkMonitorTableRow = memo(function NetworkMonitorTableRow({
|
||||
isSelected: boolean
|
||||
// Menus depend on the entire selection, including changes to other rows.
|
||||
rowSelection: RowSelectionState
|
||||
visibleColumnsKey: string
|
||||
openSheet: (monitor: NetworkMonitorRecord) => void
|
||||
}) {
|
||||
const system = useStore($allSystemsById)[row.original.system]
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
|
||||
--breakpoint-xs: 26.6rem;
|
||||
--breakpoint-450: 28rem;
|
||||
--breakpoint-md-lg: 53rem;
|
||||
--breakpoint-2xl: 90rem;
|
||||
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
@@ -117,6 +118,7 @@
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
|
||||
/* Fonts */
|
||||
@supports (font-variation-settings: normal) {
|
||||
:root {
|
||||
@@ -147,11 +149,11 @@
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
/* cosmetic patch for half pixel gap in table headers when scrolling content shows at top */
|
||||
thead.sticky:before {
|
||||
content: "";
|
||||
@apply absolute -top-2 left-0 w-full h-4 bg-table-header z-50
|
||||
@apply absolute -top-2 left-0 w-full h-4 bg-table-header z-50
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,6 +174,7 @@
|
||||
@utility scrollbar-hide {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
@@ -188,4 +191,4 @@
|
||||
|
||||
.recharts-yAxis {
|
||||
@apply tabular-nums;
|
||||
}
|
||||
}
|
||||
3
internal/site/src/types.d.ts
vendored
3
internal/site/src/types.d.ts
vendored
@@ -377,6 +377,9 @@ export interface UserSettings {
|
||||
statusFilter?: "all" | "up" | "down" | "paused" | "pending"
|
||||
viewMode?: "table" | "grid"
|
||||
sortMode?: Array<{ id: string; desc: boolean }>
|
||||
monitorCols?: Record<string, boolean>
|
||||
monitorSortMode?: Array<{ id: string; desc: boolean }>
|
||||
monitorSortModeSystem?: Array<{ id: string; desc: boolean }>
|
||||
grid?: boolean
|
||||
displayMode?: "default" | "tabs"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user