"use client"; import { useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; import { z } from "zod"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Switch } from "@/components/ui/switch"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { StrategySelect } from "@app/components/StrategySelect"; import { HeadersInput } from "@app/components/HeadersInput"; import { HorizontalTabs } from "@app/components/HorizontalTabs"; import { Credenza, CredenzaBody, CredenzaClose, CredenzaContent, CredenzaDescription, CredenzaFooter, CredenzaHeader, CredenzaTitle } from "@/components/Credenza"; import { toast } from "@app/hooks/useToast"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { useTranslations } from "next-intl"; import { ContactSalesBanner } from "@app/components/ContactSalesBanner"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { SitesSelector } from "@app/components/site-selector"; import type { Selectedsite } from "@app/components/site-selector"; import { CaretSortIcon } from "@radix-ui/react-icons"; import { cn } from "@app/lib/cn"; export type HealthCheckConfig = { hcEnabled: boolean; hcPath: string; hcMethod: string; hcInterval: number; hcTimeout: number; hcStatus: number | null; hcHeaders?: { name: string; value: string }[] | null; hcScheme?: string; hcHostname: string; hcPort: number; hcFollowRedirects: boolean; hcMode: string; hcUnhealthyInterval: number; hcTlsServerName: string; hcHealthyThreshold: number; hcUnhealthyThreshold: number; }; export type HealthCheckRow = { targetHealthCheckId: number; name: string; hcEnabled: boolean; hcHealth: "unknown" | "healthy" | "unhealthy"; hcMode: string | null; hcHostname: string | null; hcPort: number | null; hcPath: string | null; hcScheme: string | null; hcMethod: string | null; hcInterval: number | null; hcUnhealthyInterval: number | null; hcTimeout: number | null; hcHeaders: string | null; hcFollowRedirects: boolean | null; hcStatus: number | null; hcTlsServerName: string | null; hcHealthyThreshold: number | null; hcUnhealthyThreshold: number | null; resourceId: number | null; resourceName: string | null; resourceNiceId: string | null; siteId: number | null; siteName: string | null; siteNiceId: string | null; }; export type HealthCheckCredenzaProps = | { mode: "autoSave"; open: boolean; setOpen: (v: boolean) => void; orgId?: string; targetAddress: string; targetMethod?: string; initialConfig?: Partial; onChanges: (config: HealthCheckConfig) => Promise; } | { mode: "submit"; open: boolean; setOpen: (v: boolean) => void; orgId: string; initialValues?: HealthCheckRow | null; onSaved: () => void; }; const DEFAULT_VALUES = { name: "", hcEnabled: true, hcMode: "http", hcScheme: "https", hcMethod: "GET", hcHostname: "", hcPort: "", hcPath: "/", hcInterval: 30, hcUnhealthyInterval: 30, hcTimeout: 5, hcHealthyThreshold: 1, hcUnhealthyThreshold: 1, hcFollowRedirects: true, hcTlsServerName: "", hcStatus: null as number | null, hcHeaders: [] as { name: string; value: string }[] }; export function HealthCheckCredenza(props: HealthCheckCredenzaProps) { const { mode, open, setOpen, orgId } = props; const t = useTranslations(); const api = createApiClient(useEnvContext()); const [loading, setLoading] = useState(false); const [selectedSite, setSelectedSite] = useState(null); const healthCheckSchema = z .object({ ...(mode === "submit" ? { name: z .string() .min(1, { message: t("standaloneHcNameLabel") }) } : {}), hcEnabled: z.boolean(), hcPath: z.string().optional(), hcMethod: z.string().optional(), hcInterval: z .int() .positive() .min(5, { message: t("healthCheckIntervalMin") }), hcTimeout: z .int() .positive() .min(1, { message: t("healthCheckTimeoutMin") }), hcStatus: z.int().positive().min(100).optional().nullable(), hcHeaders: z .array(z.object({ name: z.string(), value: z.string() })) .nullable() .optional(), hcScheme: z.string().optional(), hcHostname: z.string(), hcPort: z .string() .min(1, { message: t("healthCheckPortInvalid") }) .refine( (val) => { const port = parseInt(val); return port > 0 && port <= 65535; }, { message: t("healthCheckPortInvalid") } ), hcFollowRedirects: z.boolean(), hcMode: z.string(), hcUnhealthyInterval: z.int().positive().min(5), hcTlsServerName: z.string(), hcHealthyThreshold: z .int() .positive() .min(1, { message: t("healthCheckHealthyThresholdMin") }), hcUnhealthyThreshold: z .int() .positive() .min(1, { message: t("healthCheckUnhealthyThresholdMin") }) }) .superRefine((data, ctx) => { if (data.hcMode !== "tcp") { if (!data.hcPath || data.hcPath.length < 1) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: t("healthCheckPathRequired"), path: ["hcPath"] }); } if (!data.hcMethod || data.hcMethod.length < 1) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: t("healthCheckMethodRequired"), path: ["hcMethod"] }); } } }); type FormValues = z.infer; const form = useForm({ resolver: zodResolver(healthCheckSchema), defaultValues: mode === "submit" ? DEFAULT_VALUES : {} }); const watchedEnabled = form.watch("hcEnabled"); const watchedMode = form.watch("hcMode"); useEffect(() => { if (!open) return; if (mode === "autoSave") { const { initialConfig, targetMethod } = props; const getDefaultScheme = () => { if (initialConfig?.hcScheme) return initialConfig.hcScheme; if (targetMethod === "https") return "https"; return "http"; }; form.reset({ hcEnabled: initialConfig?.hcEnabled, hcPath: initialConfig?.hcPath, hcMethod: initialConfig?.hcMethod, hcInterval: initialConfig?.hcInterval, hcTimeout: initialConfig?.hcTimeout, hcStatus: initialConfig?.hcStatus, hcHeaders: initialConfig?.hcHeaders, hcScheme: getDefaultScheme(), hcHostname: initialConfig?.hcHostname, hcPort: initialConfig?.hcPort ? initialConfig.hcPort.toString() : "", hcFollowRedirects: initialConfig?.hcFollowRedirects, hcMode: initialConfig?.hcMode ?? "http", hcUnhealthyInterval: initialConfig?.hcUnhealthyInterval, hcTlsServerName: initialConfig?.hcTlsServerName ?? "", hcHealthyThreshold: initialConfig?.hcHealthyThreshold ?? 1, hcUnhealthyThreshold: initialConfig?.hcUnhealthyThreshold ?? 1 }); } else { const { initialValues } = props; if (initialValues) { let parsedHeaders: { name: string; value: string }[] = []; if (initialValues.hcHeaders) { try { parsedHeaders = JSON.parse(initialValues.hcHeaders); } catch { parsedHeaders = []; } } form.reset({ name: initialValues.name, hcEnabled: initialValues.hcEnabled, hcMode: initialValues.hcMode ?? "http", hcScheme: initialValues.hcScheme ?? "https", hcMethod: initialValues.hcMethod ?? "GET", hcHostname: initialValues.hcHostname ?? "", hcPort: initialValues.hcPort ? initialValues.hcPort.toString() : "", hcPath: initialValues.hcPath ?? "/", hcInterval: initialValues.hcInterval ?? 30, hcUnhealthyInterval: initialValues.hcUnhealthyInterval ?? 30, hcTimeout: initialValues.hcTimeout ?? 5, hcHealthyThreshold: initialValues.hcHealthyThreshold ?? 1, hcUnhealthyThreshold: initialValues.hcUnhealthyThreshold ?? 1, hcFollowRedirects: initialValues.hcFollowRedirects ?? true, hcTlsServerName: initialValues.hcTlsServerName ?? "", hcStatus: initialValues.hcStatus ?? null, hcHeaders: parsedHeaders }); if (initialValues.siteId && initialValues.siteName) { setSelectedSite({ siteId: initialValues.siteId, name: initialValues.siteName, type: "" }); } else { setSelectedSite(null); } } else { form.reset(DEFAULT_VALUES); setSelectedSite(null); } } }, [open]); const handleFieldChange = async (fieldName: string, value: any) => { if (mode !== "autoSave") return; try { const currentValues = form.getValues(); const updatedValues = { ...currentValues, [fieldName]: value }; const configToSend: HealthCheckConfig = { ...updatedValues, hcPath: updatedValues.hcPath ?? "", hcMethod: updatedValues.hcMethod ?? "", hcPort: parseInt(updatedValues.hcPort), hcStatus: updatedValues.hcStatus || null, hcHealthyThreshold: updatedValues.hcHealthyThreshold, hcUnhealthyThreshold: updatedValues.hcUnhealthyThreshold }; await props.onChanges(configToSend); } catch (error) { toast({ title: t("healthCheckError"), description: t("healthCheckErrorDescription"), variant: "destructive" }); } }; const handleChange = ( fieldName: string, value: any, fieldOnChange: (v: any) => void ) => { fieldOnChange(value); if (mode === "autoSave") { handleFieldChange(fieldName, value); } }; const onSubmit = async (values: FormValues) => { if (mode !== "submit") return; const { initialValues, onSaved } = props; setLoading(true); try { const payload = { name: (values as any).name, siteId: selectedSite?.siteId, hcEnabled: values.hcEnabled, hcMode: values.hcMode, hcScheme: values.hcScheme, hcMethod: values.hcMethod, hcHostname: values.hcHostname, hcPort: parseInt(values.hcPort), hcPath: values.hcPath ?? "", hcInterval: values.hcInterval, hcUnhealthyInterval: values.hcUnhealthyInterval, hcTimeout: values.hcTimeout, hcHealthyThreshold: values.hcHealthyThreshold, hcUnhealthyThreshold: values.hcUnhealthyThreshold, hcFollowRedirects: values.hcFollowRedirects, hcTlsServerName: values.hcTlsServerName, hcStatus: values.hcStatus || null, hcHeaders: values.hcHeaders && values.hcHeaders.length > 0 ? JSON.stringify(values.hcHeaders) : null }; if (initialValues) { await api.post( `/org/${orgId}/health-check/${initialValues.targetHealthCheckId}`, payload ); } else { await api.put(`/org/${orgId}/health-check`, payload); } toast({ title: t("standaloneHcSaved") }); onSaved(); setOpen(false); } catch (e) { toast({ title: t("error"), description: formatAxiosError(e), variant: "destructive" }); } finally { setLoading(false); } }; const isEditing = mode === "submit" && !!(props as any).initialValues; const title = mode === "autoSave" ? t("configureHealthCheck") : isEditing ? t("standaloneHcEditTitle") : t("standaloneHcCreateTitle"); const description = mode === "autoSave" ? t("configureHealthCheckDescription", { target: (props as any).targetAddress }) : t("standaloneHcDescription"); const showFields = mode === "submit" || watchedEnabled; const isSnmpOrIcmp = watchedMode === "snmp" || watchedMode === "icmp"; const isTcp = watchedMode === "tcp"; return ( {title} {description}
{/* Name (submit mode only) */} {mode === "submit" && ( ( {t("standaloneHcNameLabel")} )} /> )} {/* Site picker (submit mode only) */} {mode === "submit" && (
{t("site")} { setSelectedSite(site); }} />
)}
{/* ── Strategy tab ──────────────────────── */}
{/* Enable toggle (autoSave mode only) */} {mode === "autoSave" && ( (
{t( "enableHealthChecks" )}
handleChange( "hcEnabled", value, field.onChange ) } />
)} /> )} {/* Strategy picker */} {showFields && ( ( handleChange( "hcMode", value, field.onChange ) } /> )} /> )}
{/* ── Connection tab ────────────────────── */}
{!showFields && (

{t("enableHealthChecks")}

)} {/* Contact-sales banner for SNMP / ICMP */} {showFields && isSnmpOrIcmp && ( )} {showFields && !isSnmpOrIcmp && ( <> {/* Scheme / Hostname / Port */} {isTcp ? (
( {t( "healthHostname" )} handleChange( "hcHostname", e .target .value, () => field.onChange( e ) ) } /> )} /> ( {t( "healthPort" )} handleChange( "hcPort", e .target .value, field.onChange ) } /> )} />
) : (
( {t( "healthScheme" )} )} /> ( {t( "healthHostname" )} handleChange( "hcHostname", e .target .value, () => field.onChange( e ) ) } /> )} /> ( {t( "healthPort" )} handleChange( "hcPort", e .target .value, field.onChange ) } /> )} />
)} {/* Method / Path / Timeout (HTTP) */} {!isTcp && (
( {t( "httpMethod" )} )} /> ( {t( "healthCheckPath" )} handleChange( "hcPath", e .target .value, () => field.onChange( e ) ) } /> )} /> ( {t( "timeoutSeconds" )} handleChange( "hcTimeout", parseInt( e .target .value ), field.onChange ) } /> )} />
)} {/* Timeout for TCP */} {isTcp && ( ( {t( "timeoutSeconds" )} handleChange( "hcTimeout", parseInt( e .target .value ), field.onChange ) } /> )} /> )} )}
{/* ── Advanced tab ──────────────────────── */}
{!showFields && (

{t("enableHealthChecks")}

)} {/* Contact-sales banner for SNMP / ICMP */} {showFields && isSnmpOrIcmp && ( )} {showFields && !isSnmpOrIcmp && ( <> {/* Healthy interval + threshold */}
( {t( "healthyIntervalSeconds" )} handleChange( "hcInterval", parseInt( e .target .value ), field.onChange ) } /> )} /> ( {t( "healthyThreshold" )} handleChange( "hcHealthyThreshold", parseInt( e .target .value ), field.onChange ) } /> )} />
{/* Unhealthy interval + threshold */}
( {t( "unhealthyIntervalSeconds" )} handleChange( "hcUnhealthyInterval", parseInt( e .target .value ), field.onChange ) } /> )} /> ( {t( "unhealthyThreshold" )} handleChange( "hcUnhealthyThreshold", parseInt( e .target .value ), field.onChange ) } /> )} />
{/* HTTP-only advanced fields */} {!isTcp && ( <> {/* Expected status + TLS server name */}
( {t( "expectedResponseCodes" )} { const val = e .target .value; const value = val ? parseInt( val ) : null; handleChange( "hcStatus", value, field.onChange ); }} /> )} /> ( {t( "tlsServerName" )} handleChange( "hcTlsServerName", e .target .value, () => field.onChange( e ) ) } /> )} />
{/* Follow redirects */} ( {t( "followRedirects" )} handleChange( "hcFollowRedirects", value, field.onChange ) } /> )} /> {/* Custom headers */} ( {t( "customHeaders" )} handleChange( "hcHeaders", value, field.onChange ) } rows={ 4 } /> {t( "customHeadersDescription" )} )} /> )} )}
{mode === "autoSave" ? ( ) : ( <> )}
); } export default HealthCheckCredenza;