Openstatus
www.openstatus.dev
1"use client";
2
3import {
4 EmptyStateContainer,
5 EmptyStateTitle,
6} from "@/components/content/empty-state";
7import {
8 FormCard,
9 FormCardContent,
10 FormCardDescription,
11 FormCardFooter,
12 FormCardHeader,
13 FormCardTitle,
14} from "@/components/forms/form-card";
15import { Badge } from "@/components/ui/badge";
16import { Button } from "@/components/ui/button";
17import { Checkbox } from "@/components/ui/checkbox";
18import {
19 Form,
20 FormControl,
21 FormField,
22 FormItem,
23 FormLabel,
24 FormMessage,
25} from "@/components/ui/form";
26import { config } from "@/data/notifications.client";
27import { cn } from "@/lib/utils";
28import { zodResolver } from "@hookform/resolvers/zod";
29import type { NotificationProvider } from "@openstatus/db/src/schema";
30import { isTRPCClientError } from "@trpc/client";
31import { useTransition } from "react";
32import { useForm } from "react-hook-form";
33import { toast } from "sonner";
34import { z } from "zod";
35
36const schema = z.object({
37 notifiers: z.array(z.number()),
38});
39
40type FormValues = z.infer<typeof schema>;
41
42export function FormNotifiers({
43 defaultValues,
44 onSubmit,
45 notifiers,
46 ...props
47}: Omit<React.ComponentProps<"form">, "onSubmit"> & {
48 defaultValues?: FormValues;
49 onSubmit: (values: FormValues) => Promise<void>;
50 notifiers: { id: number; name: string; provider: NotificationProvider }[];
51}) {
52 const form = useForm<FormValues>({
53 resolver: zodResolver(schema),
54 defaultValues: defaultValues ?? {
55 notifiers: [],
56 },
57 });
58 const watchNotifiers = form.watch("notifiers");
59 const [isPending, startTransition] = useTransition();
60
61 function submitAction(values: FormValues) {
62 if (isPending) return;
63
64 startTransition(async () => {
65 try {
66 const promise = onSubmit(values);
67 toast.promise(promise, {
68 loading: "Saving...",
69 success: () => "Saved",
70 error: (error) => {
71 if (isTRPCClientError(error)) {
72 return error.message;
73 }
74 return "Failed to save";
75 },
76 });
77 await promise;
78 } catch (error) {
79 console.error(error);
80 }
81 });
82 }
83
84 return (
85 <Form {...form}>
86 <form onSubmit={form.handleSubmit(submitAction)} {...props}>
87 <FormCard>
88 <FormCardHeader>
89 <FormCardTitle>Notifications</FormCardTitle>
90 <FormCardDescription>
91 Get notified when your monitor is degraded or down.
92 </FormCardDescription>
93 </FormCardHeader>
94 <FormCardContent>
95 {notifiers.length > 0 ? (
96 <FormField
97 control={form.control}
98 name="notifiers"
99 render={() => (
100 <FormItem>
101 <div className="flex items-center justify-between">
102 <FormLabel className="text-base">
103 List of Notifications
104 </FormLabel>
105 <Button
106 variant="ghost"
107 size="sm"
108 type="button"
109 className={cn(
110 watchNotifiers.length === notifiers.length &&
111 "text-muted-foreground",
112 )}
113 onClick={() => {
114 const allSelected = notifiers.every((item) =>
115 watchNotifiers.includes(item.id),
116 );
117
118 if (!allSelected) {
119 form.setValue(
120 "notifiers",
121 notifiers.map((item) => item.id),
122 );
123 } else {
124 form.setValue("notifiers", []);
125 }
126 }}
127 >
128 Select all
129 </Button>
130 </div>
131 {notifiers.map((item) => (
132 <FormField
133 key={item.id}
134 control={form.control}
135 name="notifiers"
136 render={({ field }) => {
137 const Icon = config[item.provider].icon;
138 const label = config[item.provider].label;
139 return (
140 <FormItem
141 key={item.id}
142 className="flex items-center"
143 >
144 <FormControl>
145 <Checkbox
146 checked={
147 field.value?.includes(item.id) || false
148 }
149 onCheckedChange={(checked) => {
150 return checked
151 ? field.onChange([
152 ...field.value,
153 item.id,
154 ])
155 : field.onChange(
156 field.value?.filter(
157 (value) => value !== item.id,
158 ),
159 );
160 }}
161 />
162 </FormControl>
163 <FormLabel className="font-normal text-sm">
164 {item.name}{" "}
165 <Badge
166 variant="secondary"
167 className="px-1.5 py-px font-mono text-[10px]"
168 >
169 <Icon className="size-2.5" />
170 {label}
171 </Badge>
172 </FormLabel>
173 </FormItem>
174 );
175 }}
176 />
177 ))}
178 <FormMessage />
179 </FormItem>
180 )}
181 />
182 ) : (
183 <EmptyStateContainer>
184 <EmptyStateTitle>No notifications</EmptyStateTitle>
185 </EmptyStateContainer>
186 )}
187 </FormCardContent>
188 <FormCardFooter>
189 <Button type="submit" disabled={isPending}>
190 {isPending ? "Submitting..." : "Submit"}
191 </Button>
192 </FormCardFooter>
193 </FormCard>
194 </form>
195 </Form>
196 );
197}