Openstatus www.openstatus.dev
at 4c0f4c00a38753a5d0dfd7e7b7b7706dec6f1503 224 lines 6.8 kB view raw
1"use client"; 2 3import { Checkbox } from "@/components/ui/checkbox"; 4import { 5 FormControl, 6 FormDescription, 7 FormField, 8 FormItem, 9 FormLabel, 10 FormMessage, 11} from "@/components/ui/form"; 12 13import { Link } from "@/components/common/link"; 14import { 15 FormCardContent, 16 FormCardSeparator, 17} from "@/components/forms/form-card"; 18import { useFormSheetDirty } from "@/components/forms/form-sheet"; 19import { Button } from "@/components/ui/button"; 20import { Form } from "@/components/ui/form"; 21import { Input } from "@/components/ui/input"; 22import { Label } from "@/components/ui/label"; 23import { config } from "@/data/notifications.client"; 24import { cn } from "@/lib/utils"; 25import { zodResolver } from "@hookform/resolvers/zod"; 26import { isTRPCClientError } from "@trpc/client"; 27import React, { useTransition } from "react"; 28import { useForm } from "react-hook-form"; 29import { toast } from "sonner"; 30import { z } from "zod"; 31 32const schema = z.object({ 33 name: z.string(), 34 provider: z.literal("webhook"), 35 data: z.record(z.string(), z.string()), 36 monitors: z.array(z.number()), 37}); 38 39type FormValues = z.infer<typeof schema>; 40 41export function FormWebhook({ 42 defaultValues, 43 onSubmit, 44 className, 45 monitors, 46 ...props 47}: Omit<React.ComponentProps<"form">, "onSubmit"> & { 48 defaultValues?: FormValues; 49 onSubmit: (values: FormValues) => Promise<void>; 50 monitors: { id: number; name: string }[]; 51}) { 52 const form = useForm<FormValues>({ 53 resolver: zodResolver(schema), 54 defaultValues: defaultValues ?? { 55 name: "", 56 provider: "webhook", 57 data: { 58 endpoint: "", 59 // headers: [] 60 }, 61 monitors: [], 62 }, 63 }); 64 const [isPending, startTransition] = useTransition(); 65 const { setIsDirty } = useFormSheetDirty(); 66 67 const formIsDirty = form.formState.isDirty; 68 React.useEffect(() => { 69 setIsDirty(formIsDirty); 70 }, [formIsDirty, setIsDirty]); 71 72 function submitAction(values: FormValues) { 73 if (isPending) return; 74 75 startTransition(async () => { 76 try { 77 const promise = onSubmit(values); 78 toast.promise(promise, { 79 loading: "Saving...", 80 success: "Saved", 81 error: (error) => { 82 if (isTRPCClientError(error)) { 83 return error.message; 84 } 85 return "Failed to save"; 86 }, 87 }); 88 await promise; 89 } catch (error) { 90 console.error(error); 91 } 92 }); 93 } 94 95 function testAction() { 96 if (isPending) return; 97 98 startTransition(async () => { 99 try { 100 const provider = form.getValues("provider"); 101 const data = form.getValues("data.endpoint"); 102 toast.promise(config[provider].sendTest({ url: data }), { 103 loading: "Sending test...", 104 success: "Test sent", 105 error: "Failed to send test", 106 }); 107 } catch (error) { 108 console.error(error); 109 } 110 }); 111 } 112 113 return ( 114 <Form {...form}> 115 <form 116 className={cn("grid gap-4", className)} 117 onSubmit={form.handleSubmit(submitAction)} 118 {...props} 119 > 120 <FormCardContent className="grid gap-4"> 121 <FormField 122 control={form.control} 123 name="name" 124 render={({ field }) => ( 125 <FormItem> 126 <FormLabel>Name</FormLabel> 127 <FormControl> 128 <Input placeholder="My Notifier" {...field} /> 129 </FormControl> 130 <FormMessage /> 131 <FormDescription> 132 Enter a descriptive name for your notifier. 133 </FormDescription> 134 </FormItem> 135 )} 136 /> 137 <FormField 138 control={form.control} 139 name="data.endpoint" 140 render={({ field }) => ( 141 <FormItem> 142 <FormLabel>Webhook URL</FormLabel> 143 <FormControl> 144 <Input placeholder="https://example.com/webhook" {...field} /> 145 </FormControl> 146 <FormMessage /> 147 <FormDescription> 148 Send notifications to a custom webhook URL.{" "} 149 <Link 150 href="https://docs.openstatus.dev/reference/notification/#webhook" 151 rel="noreferrer" 152 target="_blank" 153 > 154 Read more 155 </Link> 156 . 157 </FormDescription> 158 </FormItem> 159 )} 160 /> 161 <div> 162 <Button 163 variant="outline" 164 size="sm" 165 type="button" 166 onClick={testAction} 167 > 168 Send Test 169 </Button> 170 </div> 171 </FormCardContent> 172 <FormCardSeparator /> 173 <FormCardContent> 174 <FormField 175 control={form.control} 176 name="monitors" 177 render={({ field }) => ( 178 <FormItem> 179 <FormLabel>Monitors</FormLabel> 180 <FormDescription> 181 Select the monitors you want to notify. 182 </FormDescription> 183 <div className="grid gap-3"> 184 <div className="flex items-center gap-2"> 185 <FormControl> 186 <Checkbox 187 id="all" 188 checked={field.value?.length === monitors.length} 189 onCheckedChange={(checked) => { 190 field.onChange( 191 checked ? monitors.map((m) => m.id) : [], 192 ); 193 }} 194 /> 195 </FormControl> 196 <Label htmlFor="all">Select all</Label> 197 </div> 198 {monitors.map((item) => ( 199 <div key={item.id} className="flex items-center gap-2"> 200 <FormControl> 201 <Checkbox 202 id={String(item.id)} 203 checked={field.value?.includes(item.id)} 204 onCheckedChange={(checked) => { 205 const newValue = checked 206 ? [...(field.value || []), item.id] 207 : field.value?.filter((id) => id !== item.id); 208 field.onChange(newValue); 209 }} 210 /> 211 </FormControl> 212 <Label htmlFor={String(item.id)}>{item.name}</Label> 213 </div> 214 ))} 215 </div> 216 <FormMessage /> 217 </FormItem> 218 )} 219 /> 220 </FormCardContent> 221 </form> 222 </Form> 223 ); 224}