Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/openstatusHQ/openstatus. ๐ซ Status page with uptime monitoring & API monitoring as code ๐ซ openstatus.dev
bun drizzle-orm monitoring monitoring-as-code nextjs observability on-call open-source shadcn-ui status-page statuspage synthetic-monitoring tinybird turso uptime uptime-checker uptime-monitor
Something went wrong. Try again.
8.8 kB ยท 298 lines
TSX
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299"use client";
import { zodResolver } from "@hookform/resolvers/zod";import { Add, Close } from "@openstatus/icons";import { Button } from "@openstatus/ui/components/ui/button";import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage,} from "@openstatus/ui/components/ui/form";import { Form } from "@openstatus/ui/components/ui/form";import { Input } from "@openstatus/ui/components/ui/input";import { cn } from "@openstatus/ui/lib/utils";import { useMutation } from "@tanstack/react-query";import { isTRPCClientError } from "@trpc/client";import React, { useTransition } from "react";import { useFieldArray, useForm } from "react-hook-form";import { toast } from "sonner";import { z } from "zod";
import { Link } from "@/components/common/link";import { FormCardContent, FormCardSeparator,} from "@/components/forms/form-card";import { useFormSheetDirty } from "@/components/forms/form-sheet";import { CheckboxTree } from "@/components/ui/checkbox-tree";import { useTRPC } from "@/lib/trpc/client";
const schema = z.object({ name: z.string().trim().min(1, "Name is required"), provider: z.literal("webhook"), data: z.object({ endpoint: z.string().url(), headers: z.array( z.object({ key: z.string().min(1, "Key is required"), value: z.string(), }), ), }), monitors: z.array(z.number()),});
type FormValues = z.input<typeof schema>;
export function FormWebhook({ defaultValues, onSubmit, className, monitors, ...props}: Omit<React.ComponentProps<"form">, "onSubmit"> & { defaultValues?: FormValues; onSubmit: (values: FormValues) => Promise<void>; monitors: { id: number; name: string }[];}) { const form = useForm<FormValues>({ resolver: zodResolver(schema), defaultValues: defaultValues ?? { name: "", provider: "webhook", data: { endpoint: "", headers: [], }, monitors: [], }, }); const { fields, append, remove } = useFieldArray({ control: form.control, name: "data.headers", }); const [isPending, startTransition] = useTransition(); const { setIsDirty } = useFormSheetDirty(); const trpc = useTRPC();
const sendTestMutation = useMutation( trpc.notification.sendTest.mutationOptions(), );
const formIsDirty = form.formState.isDirty; React.useEffect(() => { setIsDirty(formIsDirty); }, [formIsDirty, setIsDirty]);
function submitAction(values: FormValues) { if (isPending) return;
startTransition(async () => { try { const promise = onSubmit(values); toast.promise(promise, { loading: "Saving...", success: "Saved", error: (error) => { if (isTRPCClientError(error)) { return error.message; } return "Failed to save"; }, }); await promise; } catch (error) { console.error(error); } }); }
function testAction() { if (isPending) return;
// Validate webhook endpoint before sending test const endpoint = form.getValues("data.endpoint"); if (!endpoint || endpoint.trim() === "") { toast.error("Please enter a webhook URL before sending test"); return; }
startTransition(async () => { try { const provider = form.getValues("provider"); const headers = form.getValues("data.headers"); const promise = sendTestMutation.mutateAsync({ provider, data: { webhook: { endpoint, headers }, }, }); toast.promise(promise, { loading: "Sending test...", success: "Test sent", error: (error) => { if (error instanceof Error) { return error.message; } return "Failed to send test"; }, }); await promise; } catch (error) { console.error(error); } }); }
return ( <Form {...form}> <form className={cn("grid gap-4", className)} onSubmit={form.handleSubmit(submitAction)} {...props} > <FormCardContent className="grid gap-4"> <FormField control={form.control} name="name" render={({ field }) => ( <FormItem> <FormLabel>Name</FormLabel> <FormControl> <Input placeholder="My Notifier" {...field} /> </FormControl> <FormMessage /> <FormDescription> Enter a descriptive name for your notifier. </FormDescription> </FormItem> )} /> <FormField control={form.control} name="data.endpoint" render={({ field }) => ( <FormItem> <FormLabel>Webhook URL</FormLabel> <FormControl> <Input placeholder="https://example.com/webhook" {...field} /> </FormControl> <FormMessage /> <FormDescription> Send notifications to a custom webhook URL.{" "} <Link href="https://www.openstatus.dev/docs/reference/notification/#webhook" rel="noreferrer" target="_blank" > Read more </Link> . </FormDescription> </FormItem> )} /> <FormItem> <FormLabel>Request Headers</FormLabel> <FormDescription> Custom headers to include in every webhook request. </FormDescription> {fields.map((field, index) => ( <div key={field.id} className="grid gap-2 sm:grid-cols-5"> <FormField control={form.control} name={`data.headers.${index}.key`} render={({ field }) => ( <FormItem className="col-span-2"> <FormControl> <Input placeholder="Key" {...field} /> </FormControl> <FormMessage /> </FormItem> )} /> <FormField control={form.control} name={`data.headers.${index}.value`} render={({ field }) => ( <FormItem className="col-span-2"> <FormControl> <Input placeholder="Value" {...field} /> </FormControl> </FormItem> )} /> <Button size="icon" variant="ghost" type="button" aria-label="Remove header" onClick={() => remove(index)} > <Close /> </Button> </div> ))} <div> <Button size="sm" variant="outline" type="button" onClick={() => append({ key: "", value: "" })} > <Add /> Add Header </Button> </div> <FormMessage /> </FormItem> <div> <Button variant="outline" size="sm" type="button" onClick={testAction} > Send Test </Button> </div> </FormCardContent> <FormCardSeparator /> <FormCardContent> <FormField control={form.control} name="monitors" render={({ field }) => ( <FormItem> <FormLabel>Monitors</FormLabel> <FormDescription> Select the monitors you want to notify. </FormDescription> <FormControl> <CheckboxTree items={[ { id: -1, label: "Select all", children: monitors.map((m) => ({ id: m.id, label: m.name, })), }, ]} value={field.value ?? []} onValueChange={field.onChange} /> </FormControl> <FormMessage /> </FormItem> )} /> </FormCardContent> </form> </Form> );}