"use client"; import UptimeMiniBar from "@app/components/UptimeMiniBar"; import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; import HealthCheckCredenza, { HealthCheckRow } from "@app/components/HealthCheckCredenza"; import { ColumnFilterButton } from "@app/components/ColumnFilterButton"; import { Badge } from "@app/components/ui/badge"; import { Button } from "@app/components/ui/button"; import { ControlledDataTable, type ExtendedColumnDef } from "@app/components/ui/controlled-data-table"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@app/components/ui/dropdown-menu"; import { Popover, PopoverContent, PopoverTrigger } from "@app/components/ui/popover"; import { Switch } from "@app/components/ui/switch"; import { toast } from "@app/hooks/useToast"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { Selectedsite, SitesSelector } from "@app/components/site-selector"; import { ResourceSelector, SelectedResource } from "@app/components/resource-selector"; import { ArrowUpDown, ArrowUpRight, Funnel, MoreHorizontal } from "lucide-react"; import { useTranslations } from "next-intl"; import { useState, useTransition, useEffect, useMemo } from "react"; import type { PaginationState } from "@tanstack/react-table"; import { useNavigationContext } from "@app/hooks/useNavigationContext"; import { useDebouncedCallback } from "use-debounce"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert"; import { usePaidStatus } from "@app/hooks/usePaidStatus"; import { tierMatrix } from "@server/lib/billing/tierMatrix"; import { cn } from "@app/lib/cn"; import { dataTableFilterPopoverContentClassName } from "@app/lib/dataTableFilterPopover"; type StandaloneHealthChecksTableProps = { orgId: string; healthChecks: HealthCheckRow[]; rowCount: number; pagination: PaginationState; initialFilterSite?: Selectedsite | null; initialFilterResource?: SelectedResource | null; }; function formatTarget(row: HealthCheckRow): string { if (!row.hcHostname) return "-"; if (row.hcMode === "tcp") { if (!row.hcPort) return row.hcHostname; return `${row.hcHostname}:${row.hcPort}`; } if (row.hcMode === "snmp" || row.hcMode === "ping") { if (row.hcPort) { return `${row.hcHostname}:${row.hcPort}`; } return row.hcHostname; } // HTTP / default const scheme = row.hcScheme ?? "http"; const host = row.hcHostname; const port = row.hcPort ? `:${row.hcPort}` : ""; const path = row.hcPath ?? "/"; return `${scheme}://${host}${port}${path}`; } export default function HealthChecksTable({ orgId, healthChecks, rowCount, pagination, initialFilterSite = null, initialFilterResource = null }: StandaloneHealthChecksTableProps) { const router = useRouter(); const t = useTranslations(); const api = createApiClient(useEnvContext()); const [isRefreshing, startRefresh] = useTransition(); const { isPaidUser } = usePaidStatus(); const isPaid = isPaidUser(tierMatrix.standaloneHealthChecks); const [credenzaOpen, setCredenzaOpen] = useState(false); const { navigate: filter, isNavigating: isFiltering, searchParams } = useNavigationContext(); const [deleteOpen, setDeleteOpen] = useState(false); const [selected, setSelected] = useState(null); const [togglingId, setTogglingId] = useState(null); const [siteFilterOpen, setSiteFilterOpen] = useState(false); const [resourceFilterOpen, setResourceFilterOpen] = useState(false); const pageSize = pagination.pageSize; const query = searchParams.get("query") ?? undefined; const siteIdQ = searchParams.get("siteId"); const siteIdNum = siteIdQ ? parseInt(siteIdQ, 10) : NaN; const selectedSite: Selectedsite | null = useMemo(() => { if (!siteIdQ || !Number.isInteger(siteIdNum) || siteIdNum <= 0) { return null; } if (initialFilterSite && initialFilterSite.siteId === siteIdNum) { return initialFilterSite; } return { siteId: siteIdNum, name: t("standaloneHcFilterSiteIdFallback", { id: siteIdNum }), type: "newt" }; }, [initialFilterSite, siteIdQ, siteIdNum, t]); const resourceIdQ = searchParams.get("resourceId"); const resourceIdNum = resourceIdQ ? parseInt(resourceIdQ, 10) : NaN; const selectedResource: SelectedResource | null = useMemo(() => { if ( !resourceIdQ || !Number.isInteger(resourceIdNum) || resourceIdNum <= 0 ) { return null; } if ( initialFilterResource && initialFilterResource.resourceId === resourceIdNum ) { return initialFilterResource; } return { name: t("standaloneHcFilterResourceIdFallback", { id: resourceIdNum }), resourceId: resourceIdNum, fullDomain: null, niceId: "", ssl: false, wildcard: false }; }, [initialFilterResource, resourceIdQ, resourceIdNum, t]); const rows = healthChecks; function refreshList() { startRefresh(() => { router.refresh(); }); } useEffect(() => { const interval = setInterval(() => { router.refresh(); }, 30_000); return () => clearInterval(interval); }, [router]); 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 handleFilterChange( column: string, value: string | undefined | null ) { const sp = new URLSearchParams(searchParams); sp.delete(column); sp.delete("page"); if (value) { sp.set(column, value); } filter({ searchParams: sp }); } const clearSiteFilter = () => { handleFilterChange("siteId", undefined); setSiteFilterOpen(false); }; const clearResourceFilter = () => { handleFilterChange("resourceId", undefined); setResourceFilterOpen(false); }; const onPickSite = (site: Selectedsite) => { handleFilterChange("siteId", String(site.siteId)); setSiteFilterOpen(false); }; const onPickResource = (resource: SelectedResource) => { handleFilterChange("resourceId", String(resource.resourceId)); setResourceFilterOpen(false); }; const handleToggleEnabled = async ( row: HealthCheckRow, enabled: boolean ) => { setTogglingId(row.targetHealthCheckId); try { await api.post( `/org/${orgId}/health-check/${row.targetHealthCheckId}`, { hcEnabled: enabled } ); refreshList(); } catch (e) { toast({ title: t("error"), description: formatAxiosError(e), variant: "destructive" }); } finally { setTogglingId(null); } }; const handleDelete = async () => { if (!selected) return; try { await api.delete( `/org/${orgId}/health-check/${selected.targetHealthCheckId}` ); refreshList(); toast({ title: t("standaloneHcDeleted") }); } catch (e) { toast({ title: t("error"), description: formatAxiosError(e), variant: "destructive" }); } finally { setDeleteOpen(false); setSelected(null); } }; const modeParam = searchParams.get("hcMode"); const selectedHcMode = modeParam === "http" || modeParam === "tcp" || modeParam === "snmp" || modeParam === "ping" ? modeParam : undefined; const healthParam = searchParams.get("hcHealth"); const selectedHcHealth = healthParam === "healthy" || healthParam === "unhealthy" || healthParam === "unknown" ? healthParam : undefined; const enabledParam = searchParams.get("hcEnabled"); const selectedHcEnabled = enabledParam === "true" || enabledParam === "false" ? enabledParam : undefined; const columns: ExtendedColumnDef[] = [ { accessorKey: "name", enableHiding: false, friendlyName: t("name"), header: ({ column }) => ( ), cell: ({ row }) => ( {row.original.name ? row.original.name : "-"} ) }, { id: "mode", friendlyName: t("standaloneHcColumnMode"), header: () => ( handleFilterChange("hcMode", value) } searchPlaceholder={t("searchPlaceholder")} emptyMessage={t("emptySearchOptions")} label={t("standaloneHcColumnMode")} className="p-3" /> ), cell: ({ row }) => ( {row.original.hcMode?.toUpperCase() ?? "-"} ) }, { id: "target", friendlyName: t("standaloneHcColumnTarget"), header: () => ( {t("standaloneHcColumnTarget")} ), cell: ({ row }) => {formatTarget(row.original)} }, { id: "resource", friendlyName: t("resource"), header: () => (
), cell: ({ row }) => { const r = row.original; if (!r.resourceId || !r.resourceName || !r.resourceNiceId) { return -; } return ( ); } }, { id: "site", friendlyName: t("site"), header: () => (
), cell: ({ row }) => { const r = row.original; if (!r.siteId || !r.siteName || !r.siteNiceId) { return -; } return ( ); } }, { id: "health", friendlyName: t("standaloneHcColumnHealth"), header: () => ( handleFilterChange("hcHealth", value) } searchPlaceholder={t("searchPlaceholder")} emptyMessage={t("emptySearchOptions")} label={t("standaloneHcColumnHealth")} className="p-3" /> ), cell: ({ row }) => { const health = row.original.hcHealth; if (health === "healthy") { return (
{t("standaloneHcHealthStateHealthy")} ); } else if (health === "unhealthy") { return (
{t("standaloneHcHealthStateUnhealthy")} ); } else { return (
{t("standaloneHcHealthStateUnknown")} ); } } }, { id: "uptime", friendlyName: t("uptime30d"), header: () => {t("uptime30d")}, cell: ({ row }) => { return ( ); } }, { accessorKey: "hcEnabled", friendlyName: t("alertingColumnEnabled"), header: () => ( handleFilterChange("hcEnabled", value) } searchPlaceholder={t("searchPlaceholder")} emptyMessage={t("emptySearchOptions")} label={t("alertingColumnEnabled")} className="p-3" /> ), cell: ({ row }) => { const r = row.original; return ( handleToggleEnabled(r, v)} /> ); } }, { id: "rowActions", enableHiding: false, header: () => , cell: ({ row }) => { const r = row.original; return (
{ setSelected(r); setDeleteOpen(true); }} > {t("delete")} {r.resourceId && r.resourceName && r.resourceNiceId ? ( ) : ( )}
); } } ]; return ( <> {selected && deleteOpen && ( { setDeleteOpen(val); if (!val) setSelected(null); }} dialog={

{t("standaloneHcDeleteQuestion")}

} buttonText={t("delete")} onConfirm={handleDelete} string={selected.name} title={t("standaloneHcDeleteTitle")} /> )} { setCredenzaOpen(val); if (!val) setSelected(null); }} orgId={orgId} initialValues={selected} onSaved={refreshList} /> { setSelected(null); setCredenzaOpen(true); }} addButtonDisabled={!isPaid} onRefresh={refreshList} isRefreshing={isRefreshing || isFiltering} addButtonText={t("standaloneHcAddButton")} enableColumnVisibility stickyLeftColumn="name" stickyRightColumn="rowActions" pagination={pagination} onPaginationChange={handlePaginationChange} rowCount={rowCount} /> ); }