Openstatus www.openstatus.dev
at main 197 lines 6.7 kB view raw
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}