Openstatus
www.openstatus.dev
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}