import { t } from "@lingui/core/macro" import { Trans } from "@lingui/react/macro" import { type ColumnFiltersState, flexRender, getCoreRowModel, getFilteredRowModel, getSortedRowModel, type Row, type RowSelectionState, type SortingState, type Table as TableType, useReactTable, type VisibilityState, } from "@tanstack/react-table" import { useVirtualizer, type VirtualItem } from "@tanstack/react-virtual" import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog" import { Button, buttonVariants } from "@/components/ui/button" import { memo, useCallback, useMemo, useRef, useState } from "react" import { getProbeColumns } from "@/components/network-probes-table/network-probes-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 { pb } from "@/lib/api" import { $allSystemsById, $chartTime, $direction } from "@/lib/stores" import { cn, isVisuallyLonger, useBrowserStorage } from "@/lib/utils" import type { NetworkProbeRecord } from "@/types" import { AddProbeDialog, EditProbeDialog } from "./probe-dialog" import { ArrowLeftRightIcon, EthernetPortIcon, GlobeIcon, ServerIcon, XIcon } from "lucide-react" 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/probes-charts" import { useNetworkProbeStats } from "@/lib/use-network-probes" import { useStore } from "@nanostores/react" import type { ChartData } from "@/types" import { parseSemVer } from "@/lib/utils" import { Separator } from "../ui/separator" import { $router, Link } from "../router" import { getPagePath } from "@nanostores/router" export default function NetworkProbesTableNew({ systemId, probes, }: { systemId?: string probes: NetworkProbeRecord[] }) { const [sorting, setSorting] = useBrowserStorage( `sort-np-${systemId ? 1 : 0}`, [{ id: systemId ? "name" : "system", desc: false }], sessionStorage ) const [columnFilters, setColumnFilters] = useState([]) const [columnVisibility, setColumnVisibility] = useState({}) const [rowSelection, setRowSelection] = useState({}) const [globalFilter, setGlobalFilter] = useState("") const [deleteOpen, setDeleteOpen] = useState(false) const [pendingDeleteIds, setPendingDeleteIds] = useState([]) const [editingProbe, setEditingProbe] = useState() const { toast } = useToast() const canManageProbes = !isReadOnlyUser() const [longestName, longestTarget] = useMemo(() => { let longestName = "" let longestTarget = "" for (const p of probes) { const name = p.name || p.target if (isVisuallyLonger(name, longestName)) { longestName = name } if (isVisuallyLonger(p.target, longestTarget)) { longestTarget = p.target } } return [longestName, longestTarget] }, [probes]) const runProbeBatch = useCallback( async (ids: string[], enqueue: (batch: ReturnType, id: string) => void) => { let batch = pb.createBatch() let inBatch = 0 for (const id of ids) { enqueue(batch, id) if (++inBatch >= 20) { await batch.send() batch = pb.createBatch() inBatch = 0 } } if (inBatch) { await batch.send() } }, [] ) const handleDeleteRequest = useCallback( async (probesToDelete: NetworkProbeRecord[]) => { if (!probesToDelete.length) { return } const ids = probesToDelete.map((probe) => probe.id) if (ids.length === 1) { try { await pb.collection("network_probes").delete(ids[0]) } catch (err: unknown) { toast({ variant: "destructive", title: t`Error`, description: (err as Error)?.message || t`Failed to delete probes.`, }) } return } setPendingDeleteIds(ids) setDeleteOpen(true) }, [toast] ) const handleBulkDelete = async () => { setDeleteOpen(false) if (!pendingDeleteIds.length) { return } try { await runProbeBatch(pendingDeleteIds, (batch, id) => batch.collection("network_probes").delete(id)) setPendingDeleteIds([]) setRowSelection({}) } catch (err: unknown) { toast({ variant: "destructive", title: t`Error`, description: (err as Error)?.message || t`Failed to delete probes.`, }) } } const handleSetEnabled = useCallback( async (probesToUpdate: NetworkProbeRecord[], enabled: boolean) => { if (!probesToUpdate.length) { return } const pendingUpdates = probesToUpdate.filter((probe) => probe.enabled !== enabled) if (!pendingUpdates.length) { return } try { if (pendingUpdates.length === 1) { await pb.collection("network_probes").update(pendingUpdates[0].id, { enabled }) return } await runProbeBatch( pendingUpdates.map((probe) => probe.id), (batch, id) => batch.collection("network_probes").update(id, { enabled }) ) if (probesToUpdate.length > 1) { setRowSelection({}) } } catch (err: unknown) { toast({ variant: "destructive", title: t`Error`, description: (err as Error)?.message || t`Failed to update probes.`, }) } }, [runProbeBatch, toast] ) const columns = useMemo(() => { let columns = getProbeColumns(longestName, longestTarget, { onEdit: setEditingProbe, onDelete: handleDeleteRequest, onSetEnabled: handleSetEnabled, }) columns = systemId ? columns.filter((col) => col.id !== "system") : columns columns = canManageProbes ? columns : columns.filter((col) => col.id !== "actions") return columns }, [canManageProbes, handleDeleteRequest, handleSetEnabled, longestName, systemId, longestTarget]) const table = useReactTable({ data: probes, columns, getRowId: (row) => row.id, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), getFilteredRowModel: getFilteredRowModel(), onSortingChange: setSorting, onColumnFiltersChange: setColumnFilters, onColumnVisibilityChange: setColumnVisibility, onRowSelectionChange: setRowSelection, defaultColumn: { sortUndefined: "last", size: 900, minSize: 0, }, state: { sorting, columnFilters, columnVisibility, rowSelection, globalFilter, }, onGlobalFilterChange: setGlobalFilter, globalFilterFn: (row, _columnId, filterValue) => { const probe = row.original const systemName = $allSystemsById.get()[probe.system]?.name ?? "" const searchString = `${probe.name}${probe.target}${probe.protocol}${systemName}`.toLocaleLowerCase() return (filterValue as string) .toLowerCase() .split(" ") .every((term) => searchString.includes(term)) }, }) const rows = table.getRowModel().rows const visibleColumns = table.getVisibleLeafColumns() return (
Network Probes
Response time monitoring from agents.
{probes.length > 0 && (
setGlobalFilter(e.target.value)} className="ms-auto px-4 w-full max-w-full md:w-50" /> {globalFilter && ( )}
)} {canManageProbes ? : null} {canManageProbes ? ( { if (!open) { setEditingProbe(undefined) } }} /> ) : null} { setDeleteOpen(open) if (!open) { setPendingDeleteIds([]) } }} > Are you sure? This will permanently delete all selected records from the database. Cancel Continue
) } const NetworkProbesTable = memo(function NetworkProbeTable({ table, rows, colLength, rowSelection: _rowSelection, }: { table: TableType rows: Row[] colLength: number rowSelection: RowSelectionState }) { // The virtualizer will need a reference to the scrollable container element const scrollRef = useRef(null) const [sheetOpen, setSheetOpen] = useState(false) const [activeProbeId, setActiveProbeId] = useState(null) const activeProbe = activeProbeId ? table.options.data.find((probe) => probe.id === activeProbeId) : undefined const openSheet = useCallback((probe: NetworkProbeRecord) => { setActiveProbeId(probe.id) setSheetOpen(true) }, []) const virtualizer = useVirtualizer({ count: rows.length, estimateSize: () => 54, 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] return ( ) }) ) : ( No results. )}
{ setSheetOpen(nextOpen) }} probe={activeProbe} />
) }) function NetworkProbeTableHead({ table }: { table: TableType }) { return ( {table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => { return ( {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} ) })} ))} ) } const NetworkProbeTableRow = memo(function NetworkProbeTableRow({ row, virtualRow, isSelected, openSheet, }: { row: Row virtualRow: VirtualItem isSelected: boolean openSheet: (probe: NetworkProbeRecord) => void }) { return ( openSheet(row.original)} > {row.getVisibleCells().map((cell) => ( {flexRender(cell.column.columnDef.cell, cell.getContext())} ))} ) }) function NetworkProbeSheet({ open, onOpenChange, probe, }: { open: boolean onOpenChange: (open: boolean) => void probe?: NetworkProbeRecord }) { if (!probe) { return null } return } function NetworkProbeSheetContent({ open, onOpenChange, probe, }: { open: boolean onOpenChange: (open: boolean) => void probe: NetworkProbeRecord }) { const chartTime = useStore($chartTime) const direction = useStore($direction) const system = useStore($allSystemsById)[probe.system] const probeStats = useNetworkProbeStats({ systemId: probe.system, chartTime }) const chartData = useMemo( () => ({ agentVersion: parseSemVer(system?.info?.v), orientation: direction === "rtl" ? "right" : "left", chartTime, }), [probeStats] ) const hasProbeStats = probeStats.some((record) => record.stats?.[probe.id] != null) const probeLabel = probe.name || probe.target return ( {probeLabel} {system?.name ?? ""} {probe.protocol.toUpperCase()} {probe.target} {probe.port > 0 && ( <> {probe.port} )}
) }