"use client"; import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert"; import { Button } from "@app/components/ui/button"; import { DataTable, ExtendedColumnDef } from "@app/components/ui/data-table"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@app/components/ui/dropdown-menu"; import { Switch } from "@app/components/ui/switch"; import { toast } from "@app/hooks/useToast"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { useNavigationContext } from "@app/hooks/useNavigationContext"; import { usePaidStatus } from "@app/hooks/usePaidStatus"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { getNextSortOrder, getSortDirection } from "@app/lib/sortColumn"; import { alertRuleAllHealthChecksSelected, alertRuleAllResourcesSelected, alertRuleAllSitesSelected } from "@app/lib/alertRuleForm"; import { tierMatrix } from "@server/lib/billing/tierMatrix"; import { ArrowDown01Icon, ArrowUp10Icon, ChevronsUpDownIcon, MoreHorizontal } from "lucide-react"; import moment from "moment"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; import { useMemo, useState, useTransition } from "react"; import z from "zod"; import { ColumnFilterButton } from "./ColumnFilterButton"; import type { PaginationState } from "@tanstack/react-table"; import type { DataTablePaginationState } from "@app/components/ui/data-table"; import { useDebouncedCallback } from "use-debounce"; const alertRulesEnabledQuerySchema = z .enum(["true", "false"]) .optional() .catch(undefined); export type AlertRuleRow = { alertRuleId: number; orgId: string; name: string; eventType: string; enabled: boolean; cooldownSeconds: number; lastTriggeredAt: number | null; createdAt: number; updatedAt: number; siteIds: number[]; healthCheckIds: number[]; resourceIds: number[]; }; type AlertingRulesTableProps = { orgId: string; alertRules: AlertRuleRow[]; rowCount: number; }; function ruleHref(orgId: string, ruleId: number) { return `/${orgId}/settings/alerting/${ruleId}`; } function sourceSummary( rule: AlertRuleRow, t: (k: string, o?: Record) => string ) { if (alertRuleAllSitesSelected(rule.eventType, rule.siteIds)) { return t("alertingSummaryAllSites"); } if ( rule.eventType === "site_online" || rule.eventType === "site_offline" || rule.eventType === "site_toggle" ) { return t("alertingSummarySites", { count: rule.siteIds.length }); } if (alertRuleAllResourcesSelected(rule.eventType, rule.resourceIds)) { return t("alertingSummaryAllResources"); } if (rule.eventType.startsWith("resource_")) { return t("alertingSummaryResources", { count: rule.resourceIds.length }); } if (alertRuleAllHealthChecksSelected(rule.eventType, rule.healthCheckIds)) { return t("alertingSummaryAllHealthChecks"); } return t("alertingSummaryHealthChecks", { count: rule.healthCheckIds.length }); } function triggerLabel(rule: AlertRuleRow, t: (k: string) => string) { switch (rule.eventType) { case "site_online": return t("alertingTriggerSiteOnline"); case "site_offline": return t("alertingTriggerSiteOffline"); case "site_toggle": return t("alertingTriggerSiteToggle"); case "health_check_healthy": return t("alertingTriggerHcHealthy"); case "health_check_unhealthy": return t("alertingTriggerHcUnhealthy"); case "health_check_toggle": return t("alertingTriggerHcToggle"); case "resource_healthy": return t("alertingTriggerResourceHealthy"); case "resource_unhealthy": return t("alertingTriggerResourceUnhealthy"); case "resource_degraded": return t("alertingTriggerResourceDegraded"); case "resource_toggle": return t("alertingTriggerResourceToggle"); default: return rule.eventType; } } export default function AlertingRulesTable({ orgId, alertRules, rowCount }: AlertingRulesTableProps) { const router = useRouter(); const t = useTranslations(); const api = createApiClient(useEnvContext()); const [isRefreshing, startRefresh] = useTransition(); const { isPaidUser } = usePaidStatus(); const isPaid = isPaidUser(tierMatrix.alertingRules); const { navigate: filter, isNavigating: isFiltering, searchParams } = useNavigationContext(); const [deleteOpen, setDeleteOpen] = useState(false); const [selected, setSelected] = useState(null); const [togglingId, setTogglingId] = useState(null); const page = Math.max(1, Number(searchParams.get("page") ?? 1)); const pageSize = Math.max(1, Number(searchParams.get("pageSize") ?? 20)); const pageIndex = page - 1; const query = searchParams.get("query") ?? undefined; const sortBy = searchParams.get("sort_by") ?? undefined; const order = searchParams.get("order") ?? undefined; const enabledForQuery = alertRulesEnabledQuerySchema.parse( searchParams.get("enabled") ?? undefined ); const enabledFilterOptions = useMemo( () => [ { value: "true", label: t("enabled") }, { value: "false", label: t("disabled") } ], [t] ); const rows = alertRules; const total = rowCount; const pageCount = Math.max(1, Math.ceil(total / pageSize)); function refreshList() { startRefresh(() => { router.refresh(); }); } const paginationState: DataTablePaginationState = { pageIndex, pageSize, pageCount }; const handlePaginationChange = (newState: PaginationState) => { searchParams.set("page", (newState.pageIndex + 1).toString()); searchParams.set("pageSize", newState.pageSize.toString()); filter({ searchParams }); }; const handleSearchChange = useDebouncedCallback((value: string) => { if (value) { searchParams.set("query", value); } else { searchParams.delete("query"); } searchParams.delete("page"); filter({ searchParams }); }, 300); function toggleSort(column: string) { filter({ searchParams: getNextSortOrder(column, searchParams) }); } function handleEnabledFilter(value: string | undefined | null) { const sp = new URLSearchParams(searchParams); sp.delete("enabled"); sp.delete("page"); if (value) { sp.set("enabled", value); } filter({ searchParams: sp }); } const setEnabled = async (rule: AlertRuleRow, enabled: boolean) => { setTogglingId(rule.alertRuleId); try { await api.post(`/org/${orgId}/alert-rule/${rule.alertRuleId}`, { enabled }); refreshList(); } catch (e) { toast({ title: t("error"), description: formatAxiosError(e), variant: "destructive" }); } finally { setTogglingId(null); } }; const confirmDelete = async () => { if (!selected) return; try { await api.delete( `/org/${orgId}/alert-rule/${selected.alertRuleId}` ); refreshList(); toast({ title: t("alertingRuleDeleted") }); } catch (e) { toast({ title: t("error"), description: formatAxiosError(e), variant: "destructive" }); } finally { setDeleteOpen(false); setSelected(null); } }; const columns: ExtendedColumnDef[] = [ { accessorKey: "name", enableHiding: false, friendlyName: t("name"), header: () => { const nameOrder = getSortDirection("name", searchParams); const Icon = nameOrder === "asc" ? ArrowDown01Icon : nameOrder === "desc" ? ArrowUp10Icon : ChevronsUpDownIcon; return ( ); }, cell: ({ row }) => {row.original.name} }, { id: "source", friendlyName: t("alertingColumnSource"), header: () => ( {t("alertingColumnSource")} ), cell: ({ row }) => {sourceSummary(row.original, t)} }, { id: "trigger", friendlyName: t("alertingColumnTrigger"), header: () => ( {t("alertingColumnTrigger")} ), cell: ({ row }) => {triggerLabel(row.original, t)} }, { accessorKey: "lastTriggeredAt", friendlyName: t("lastTriggeredAt"), header: () => { const triggerOrder = getSortDirection( "last_triggered_at", searchParams ); const Icon = triggerOrder === "asc" ? ArrowDown01Icon : triggerOrder === "desc" ? ArrowUp10Icon : ChevronsUpDownIcon; return ( ); }, cell: ({ row }) => ( {row.original.lastTriggeredAt ? moment(row.original.lastTriggeredAt).format("lll") : "-"} ) }, { accessorKey: "enabled", friendlyName: t("alertingColumnEnabled"), header: () => ( ), cell: ({ row }) => { const r = row.original; return ( setEnabled(r, v)} /> ); } }, { id: "rowActions", enableHiding: false, header: () => , cell: ({ row }) => { const r = row.original; return (
{ setSelected(r); setDeleteOpen(true); }} > {t("delete")}
); } } ]; return ( <> {selected && ( { setDeleteOpen(val); if (!val) setSelected(null); }} dialog={

{t("alertingDeleteQuestion")}

} buttonText={t("delete")} onConfirm={confirmDelete} string={selected.name} title={t("alertingDeleteRule")} /> )} { router.push(`/${orgId}/settings/alerting/create`); }} onRefresh={refreshList} isRefreshing={isRefreshing || isFiltering} addButtonText={t("alertingAddRule")} enableColumnVisibility stickyLeftColumn="name" stickyRightColumn="rowActions" pagination={paginationState} onPaginationChange={handlePaginationChange} /> ); }