"use client"; import { Link } from "@/components/common/link"; import { FormCard, FormCardContent, FormCardDescription, FormCardFooter, FormCardFooterInfo, FormCardHeader, FormCardSeparator, FormCardTitle, } from "@/components/forms/form-card"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { Form } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; import { Textarea } from "@/components/ui/textarea"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; import { zodResolver } from "@hookform/resolvers/zod"; import { dnsRecords, headerAssertion, jsonBodyAssertion, numberCompareDictionary, recordAssertion, recordCompareDictionary, statusAssertion, stringCompareDictionary, textBodyAssertion, } from "@openstatus/assertions"; import { monitorMethods } from "@openstatus/db/src/schema"; import { isTRPCClientError } from "@trpc/client"; import { Globe, Network, Plus, Server, X } from "lucide-react"; import { useEffect, useState, useTransition } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; const TYPES = ["http", "tcp", "dns"] as const; const HTTP_ASSERTION_TYPES = ["status", "header", "textBody"] as const; const DNS_ASSERTION_TYPES = dnsRecords; const schema = z.object({ name: z.string().min(1, "Name is required"), type: z.enum(TYPES), method: z.enum(monitorMethods), url: z.string().min(1, "URL is required"), headers: z.array( z.object({ key: z.string(), value: z.string(), }), ), active: z.boolean().optional().prefault(true), assertions: z.array( z.discriminatedUnion("type", [ statusAssertion, headerAssertion, textBodyAssertion, jsonBodyAssertion, recordAssertion, ]), ), body: z.string().optional(), skipCheck: z.boolean().optional().prefault(false), saveCheck: z.boolean().optional().prefault(false), }); type FormValues = z.input; export function FormGeneral({ defaultValues, disabled, onSubmit, ...props }: Omit, "onSubmit"> & { defaultValues?: FormValues; onSubmit: (values: FormValues) => Promise; disabled?: boolean; }) { const [error, setError] = useState(null); const form = useForm({ resolver: zodResolver(schema), defaultValues: defaultValues ?? { active: true, name: "", type: undefined, method: "GET", url: "", headers: [], body: "", assertions: [], skipCheck: false, saveCheck: false, }, }); const [isPending, startTransition] = useTransition(); const watchType = form.watch("type"); const watchMethod = form.watch("method"); useEffect(() => { // NOTE: reset form when type changes if (watchType && !defaultValues) { form.setValue("assertions", []); form.setValue("body", ""); form.setValue("headers", []); form.setValue("method", "GET"); form.setValue("url", ""); } }, [watchType, defaultValues, form]); function submitAction(values: FormValues) { console.log("submitAction", values); if (isPending || disabled) return; // Validate assertions based on type for (let i = 0; i < values.assertions.length; i++) { const assertion = values.assertions[i]; if (assertion.type === "status") { if (typeof assertion.target !== "number" || assertion.target <= 0) { form.setError(`assertions.${i}.target`, { message: "Status target must be a positive number", }); return; } } else if (assertion.type === "header") { if (!assertion.key || assertion.key.trim() === "") { form.setError(`assertions.${i}.key`, { message: "Header key is required", }); return; } if (!assertion.target || assertion.target.trim() === "") { form.setError(`assertions.${i}.target`, { message: "Header target is required", }); return; } } else if (assertion.type === "textBody") { if (!assertion.target || assertion.target.trim() === "") { form.setError(`assertions.${i}.target`, { message: "Body target is required", }); return; } } else if (assertion.type === "dnsRecord") { if (!assertion.key || assertion.key.trim() === "") { form.setError(`assertions.${i}.key`, { message: "DNS record key is required", }); return; } if (!assertion.target || assertion.target.trim() === "") { form.setError(`assertions.${i}.target`, { message: "DNS record target is required", }); return; } } } startTransition(async () => { try { const promise = onSubmit(values); toast.promise(promise, { loading: "Saving...", success: "Saved", error: (error) => { if (isTRPCClientError(error)) { setError(error.message); return error.message; } return "Failed to save"; }, }); await promise; } catch (error) { console.error(error); } }); } return (
Monitor Configuration Configure your monitor settings and endpoints. ( Name Displayed on the status page. )} /> ( Active )} /> ( Monitoring Type {[ { value: "http", icon: Globe, label: "HTTP" }, { value: "tcp", icon: Network, label: "TCP" }, { value: "dns", icon: Server, label: "DNS" }, ].map((type) => { return ( Monitor type cannot be changed after creation. ); })}
Missing a type?{" "} Contact us
)} />
{watchType ? : null} {watchType === "http" && ( <>
( Method )} />
( URL )} />
( Request Headers {field.value.map((header, index) => (
{ const newHeaders = [...field.value]; newHeaders[index] = { ...newHeaders[index], key: e.target.value, }; field.onChange(newHeaders); }} /> { const newHeaders = [...field.value]; newHeaders[index] = { ...newHeaders[index], value: e.target.value, }; field.onChange(newHeaders); }} />
))}
)} /> {["POST", "PUT", "PATCH", "DELETE"].includes(watchMethod) && ( ( Body