import { t } from "@lingui/core/macro" import { Trans } from "@lingui/react/macro" import { 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 { listenKeys } from "nanostores" import { memo, useEffect, 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 { isReadOnlyUser, pb } from "@/lib/api" import { $allSystemsById } from "@/lib/stores" import { cn, getVisualStringWidth, useBrowserStorage } from "@/lib/utils" import type { NetworkProbeRecord } from "@/types" import { AddProbeDialog } from "./probe-dialog" const NETWORK_PROBE_FIELDS = "id,name,system,target,protocol,port,interval,latency,loss,enabled,updated" export default function NetworkProbesTableNew({ systemId }: { systemId?: string }) { const loadTime = Date.now() const [data, setData] = useState([]) 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 [globalFilter, setGlobalFilter] = useState("") // clear old data when systemId changes useEffect(() => { return setData([]) }, [systemId]) useEffect(() => { function fetchData(systemId?: string) { pb.collection("network_probes") .getList(0, 2000, { fields: NETWORK_PROBE_FIELDS, filter: systemId ? pb.filter("system={:system}", { system: systemId }) : undefined, }) .then((res) => setData(res.items)) } // initial load fetchData(systemId) // if no systemId, pull after every system update if (!systemId) { return $allSystemsById.listen((_value, _oldValue, systemId) => { // exclude initial load of systems if (Date.now() - loadTime > 500) { fetchData(systemId) } }) } // if systemId, fetch after the system is updated return listenKeys($allSystemsById, [systemId], (_newSystems) => { fetchData(systemId) }) }, [systemId]) // Subscribe to updates useEffect(() => { let unsubscribe: (() => void) | undefined const pbOptions = systemId ? { fields: NETWORK_PROBE_FIELDS, filter: pb.filter("system = {:system}", { system: systemId }) } : { fields: NETWORK_PROBE_FIELDS } ;(async () => { try { unsubscribe = await pb.collection("network_probes").subscribe( "*", (event) => { const record = event.record setData((currentProbes) => { const probes = currentProbes ?? [] const matchesSystemScope = !systemId || record.system === systemId if (event.action === "delete") { return probes.filter((device) => device.id !== record.id) } if (!matchesSystemScope) { // Record moved out of scope; ensure it disappears locally. return probes.filter((device) => device.id !== record.id) } const existingIndex = probes.findIndex((device) => device.id === record.id) if (existingIndex === -1) { return [record, ...probes] } const next = [...probes] next[existingIndex] = record return next }) }, pbOptions ) } catch (error) { console.error("Failed to subscribe to SMART device updates:", error) } })() return () => { unsubscribe?.() } }, [systemId]) const { longestName, longestTarget } = useMemo(() => { let longestName = 0 let longestTarget = 0 for (const p of data) { longestName = Math.max(longestName, getVisualStringWidth(p.name || p.target)) longestTarget = Math.max(longestTarget, getVisualStringWidth(p.target)) } return { longestName, longestTarget } }, [data]) // Filter columns based on whether systemId is provided const columns = useMemo(() => { let columns = getProbeColumns(longestName, longestTarget) columns = systemId ? columns.filter((col) => col.id !== "system") : columns columns = isReadOnlyUser() ? columns.filter((col) => col.id !== "actions") : columns return columns }, [systemId, longestName, longestTarget]) const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), getFilteredRowModel: getFilteredRowModel(), onSortingChange: setSorting, onColumnFiltersChange: setColumnFilters, onColumnVisibilityChange: setColumnVisibility, defaultColumn: { sortUndefined: "last", size: 900, minSize: 0, }, state: { sorting, columnFilters, columnVisibility, 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
ICMP/TCP/HTTP latency monitoring from this agent
{data.length > 0 && ( setGlobalFilter(e.target.value)} className="ms-auto px-4 w-full max-w-full md:w-64" /> )} {!isReadOnlyUser() ? : null}
) } const NetworkProbesTable = memo(function NetworkProbeTable({ 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: () => 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. )}
) }) 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, }: { row: Row virtualRow: VirtualItem }) { return ( {row.getVisibleCells().map((cell) => ( {flexRender(cell.column.columnDef.cell, cell.getContext())} ))} ) })