import { t } from "@lingui/core/macro" import { Trans } from "@lingui/react/macro" import { type ColumnFiltersState, flexRender, getCoreRowModel, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, type SortingState, useReactTable, type VisibilityState, } from "@tanstack/react-table" import { ChevronLeftIcon, ChevronRightIcon, ChevronsLeftIcon, ChevronsRightIcon, DownloadIcon, Trash2Icon, } from "lucide-react" import { memo, useEffect, useState } from "react" import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog" import { Button, buttonVariants } from "@/components/ui/button" import { Checkbox } from "@/components/ui/checkbox" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import { useToast } from "@/components/ui/use-toast" import { alertInfo } from "@/lib/alerts" import { pb } from "@/lib/api" import { cn, formatDuration, formatShortDate } from "@/lib/utils" import type { AlertsHistoryRecord } from "@/types" import { alertsHistoryColumns } from "../../alerts-history-columns" const SectionIntro = memo(() => { return (

Alert History

View your 200 most recent alerts.

) }) export default function AlertsHistoryDataTable() { const [data, setData] = useState([]) const [sorting, setSorting] = useState([]) const [columnFilters, setColumnFilters] = useState([]) const [columnVisibility, setColumnVisibility] = useState({}) const [rowSelection, setRowSelection] = useState({}) const [globalFilter, setGlobalFilter] = useState("") const { toast } = useToast() const [deleteOpen, setDeleteDialogOpen] = useState(false) useEffect(() => { let unsubscribe: (() => void) | undefined const pbOptions = { expand: "system", fields: "id,name,value,state,created,resolved,expand.system.name", } // Initial load pb.collection("alerts_history") .getList(0, 200, { ...pbOptions, sort: "-created", }) .then(({ items }) => setData(items)) // Subscribe to changes ;(async () => { unsubscribe = await pb.collection("alerts_history").subscribe( "*", (e) => { if (e.action === "create") { setData((current) => [e.record as AlertsHistoryRecord, ...current]) } if (e.action === "update") { setData((current) => current.map((r) => (r.id === e.record.id ? (e.record as AlertsHistoryRecord) : r))) } if (e.action === "delete") { setData((current) => current.filter((r) => r.id !== e.record.id)) } }, pbOptions ) })() // Unsubscribe on unmount return () => unsubscribe?.() }, []) const table = useReactTable({ data, columns: [ { id: "select", header: ({ table }) => ( table.toggleAllPageRowsSelected(!!value)} aria-label="Select all" /> ), cell: ({ row }) => ( row.toggleSelected(!!value)} aria-label="Select row" /> ), enableSorting: false, enableHiding: false, }, ...alertsHistoryColumns, ], getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), getSortedRowModel: getSortedRowModel(), getFilteredRowModel: getFilteredRowModel(), onSortingChange: setSorting, onColumnFiltersChange: setColumnFilters, onColumnVisibilityChange: setColumnVisibility, onRowSelectionChange: setRowSelection, state: { sorting, columnFilters, columnVisibility, rowSelection, globalFilter, }, onGlobalFilterChange: setGlobalFilter, globalFilterFn: (row, _columnId, filterValue) => { const system = row.original.expand?.system?.name ?? "" const name = row.getValue("name") ?? "" const created = row.getValue("created") ?? "" const search = String(filterValue).toLowerCase() return ( system.toLowerCase().includes(search) || (name as string).toLowerCase().includes(search) || (created as string).toLowerCase().includes(search) ) }, }) // Bulk delete handler const handleBulkDelete = async () => { setDeleteDialogOpen(false) const selectedIds = table.getSelectedRowModel().rows.map((row) => row.original.id) try { let batch = pb.createBatch() let inBatch = 0 for (const id of selectedIds) { batch.collection("alerts_history").delete(id) inBatch++ if (inBatch > 20) { await batch.send() batch = pb.createBatch() inBatch = 0 } } inBatch && (await batch.send()) table.resetRowSelection() } catch (e) { toast({ variant: "destructive", title: t`Error`, description: `Failed to delete records.`, }) } } // Export to CSV handler const handleExportCSV = () => { const selectedRows = table.getSelectedRowModel().rows if (!selectedRows.length) return const cells: Record string> = { system: (record) => record.expand?.system?.name || record.system, name: (record) => alertInfo[record.name]?.name() || record.name, value: (record) => record.value + (alertInfo[record.name]?.unit ?? ""), state: (record) => (record.resolved ? t`Resolved` : t`Active`), created: (record) => formatShortDate(record.created), resolved: (record) => (record.resolved ? formatShortDate(record.resolved) : ""), duration: (record) => (record.resolved ? formatDuration(record.created, record.resolved) : ""), } const csvRows = [Object.keys(cells).join(",")] for (const row of selectedRows) { const r = row.original csvRows.push( Object.values(cells) .map((val) => val(r)) .join(",") ) } const blob = new Blob([csvRows.join("\n")], { type: "text/csv" }) const url = URL.createObjectURL(blob) const a = document.createElement("a") a.href = url a.download = "alerts_history.csv" a.click() URL.revokeObjectURL(url) } return (
{table.getFilteredSelectedRowModel().rows.length > 0 && (
setDeleteDialogOpen(open)}> Are you sure? This will permanently delete all selected records from the database. Cancel Continue
)} setGlobalFilter(e.target.value)} className="px-4 w-full max-w-full @3xl:w-64" />
{table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => ( {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} ))} ))} {table.getRowModel().rows.length ? ( table.getRowModel().rows.map((row) => ( {row.getVisibleCells().map((cell) => ( {flexRender(cell.column.columnDef.cell, cell.getContext())} ))} )) ) : ( No results. )}
{table.getFilteredSelectedRowModel().rows.length} of {table.getFilteredRowModel().rows.length} row(s) selected.
Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
) }