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.
18 kB ยท 484 lines
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485"use client";
import { zodResolver } from "@hookform/resolvers/zod";import { Calendar as CalendarIcon, Clock } from "@openstatus/icons";import { Button } from "@openstatus/ui/components/ui/button";import { Calendar } from "@openstatus/ui/components/ui/calendar";import { Checkbox } from "@openstatus/ui/components/ui/checkbox";import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage,} from "@openstatus/ui/components/ui/form";import { Input } from "@openstatus/ui/components/ui/input";import { Label } from "@openstatus/ui/components/ui/label";import { Popover, PopoverContent, PopoverTrigger,} from "@openstatus/ui/components/ui/popover";import { TabsContent } from "@openstatus/ui/components/ui/tabs";import { TabsList, TabsTrigger } from "@openstatus/ui/components/ui/tabs";import { Tabs } from "@openstatus/ui/components/ui/tabs";import { Textarea } from "@openstatus/ui/components/ui/textarea";import { useIsMobile } from "@openstatus/ui/hooks/use-mobile";import { cn } from "@openstatus/ui/lib/utils";import { useQuery } from "@tanstack/react-query";import { isTRPCClientError } from "@trpc/client";import { addDays, format } from "date-fns";import React, { useTransition } from "react";import { useForm } from "react-hook-form";import { toast } from "sonner";import { z } from "zod";
import { EmptyStateContainer, EmptyStateTitle,} from "@/components/content/empty-state";import { ProcessMessage } from "@/components/content/process-message";import { FormCardContent, FormCardSeparator,} from "@/components/forms/form-card";import { useFormSheetDirty } from "@/components/forms/form-sheet";import { CheckboxTree, type CheckboxTreeItem,} from "@/components/ui/checkbox-tree";import { useTRPC } from "@/lib/trpc/client";
const schema = z .object({ title: z.string().trim().min(1, "Title is required"), message: z.string(), startDate: z.date(), endDate: z.date(), pageComponents: z.array(z.number()), notifySubscribers: z.boolean().optional(), }) .refine((data) => data.endDate > data.startDate, { error: "End date cannot be earlier than start date.", path: ["endDate"], });
export type FormValues = z.infer<typeof schema>;
export function FormMaintenance({ defaultValues, onSubmit, className, items, ...props}: Omit<React.ComponentProps<"form">, "onSubmit"> & { defaultValues?: FormValues; items: CheckboxTreeItem[]; onSubmit: (values: FormValues) => Promise<void>;}) { const trpc = useTRPC(); const { data: workspace } = useQuery(trpc.workspace.get.queryOptions()); const mobile = useIsMobile(); const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; const form = useForm<FormValues>({ resolver: zodResolver(schema), defaultValues: defaultValues ?? { title: "", message: "", startDate: new Date(), endDate: addDays(new Date(), 1), pageComponents: [], notifySubscribers: !!workspace?.limits["status-subscribers"], }, });
const watchEndDate = form.watch("endDate"); const watchMessage = form.watch("message"); const [isPending, startTransition] = useTransition(); const { setIsDirty } = useFormSheetDirty();
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); } }); }
return ( <Form {...form}> <form className={cn("grid gap-4", className)} onSubmit={form.handleSubmit(submitAction)} {...props} > <FormCardContent> <FormField control={form.control} name="title" render={({ field }) => ( <FormItem> <FormLabel>Title</FormLabel> <FormControl> <Input placeholder="DB migration..." {...field} /> </FormControl> <FormMessage /> </FormItem> )} /> </FormCardContent> <FormCardSeparator /> <FormCardContent> {/* TODO: */} <FormField control={form.control} name="startDate" render={({ field }) => ( <FormItem className="flex flex-col"> <FormLabel>Start Date</FormLabel> <Popover modal> <FormControl> <PopoverTrigger asChild> <Button type="button" variant="outline" size="sm" className={cn( "w-[240px] pl-3 text-left font-normal", !field.value && "text-muted-foreground", )} > {field.value ? ( format(field.value, "PPP 'at' h:mm a") ) : ( <span>Pick a date</span> )} <CalendarIcon className="ml-auto h-4 w-4 opacity-50" /> </Button> </PopoverTrigger> </FormControl> <PopoverContent className="pointer-events-auto w-auto p-0" align="start" side={mobile ? "bottom" : "left"} > <Calendar mode="single" selected={field.value} onSelect={(selectedDate) => { if (!selectedDate) return;
const newDate = new Date(selectedDate); newDate.setHours( field.value.getHours(), field.value.getMinutes(), field.value.getSeconds(), field.value.getMilliseconds(), ); field.onChange(newDate);
// NOTE: if end date is before start date, set it to the same day as the start date if (watchEndDate && newDate > watchEndDate) { form.setValue("endDate", newDate); } }} initialFocus /> <div className="border-t p-3"> <div className="flex items-center gap-3"> <Label htmlFor="time-start" className="text-xs"> Enter time </Label> <div className="relative grow"> <Input id="time-start" type="time" step="1" value={ field.value ? field.value.toTimeString().slice(0, 8) : new Date().toTimeString().slice(0, 8) } className="peer appearance-none ps-9 [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none" onChange={(e) => { try { const timeValue = e.target.value; if (!timeValue || !field.value) return;
const [hours, minutes, seconds] = timeValue .split(":") .map(Number);
const newDate = new Date(field.value); newDate.setHours( hours, minutes, seconds || 0, 0, );
field.onChange(newDate); } catch (error) { console.error(error); } }} /> <div className="text-muted-foreground/80 pointer-events-none absolute inset-y-0 start-0 flex items-center justify-center ps-3 peer-disabled:opacity-50"> <Clock size={16} aria-hidden="true" /> </div> </div> </div> </div> </PopoverContent> </Popover> <FormDescription> When the maintenance starts. Shown in your timezone ( <code className="font-commit-mono text-foreground/70"> {timezone} </code> ) and saved as Unix time ( <code className="font-commit-mono text-foreground/70"> UTC </code> ). </FormDescription> <FormMessage /> </FormItem> )} /> </FormCardContent> <FormCardSeparator /> <FormCardContent> <FormField control={form.control} name="endDate" render={({ field }) => ( <FormItem className="flex flex-col"> <FormLabel>End Date</FormLabel> <Popover modal> <FormControl> <PopoverTrigger asChild> <Button type="button" variant="outline" size="sm" className={cn( "w-[240px] pl-3 text-left font-normal", !field.value && "text-muted-foreground", )} > {field.value ? ( format(field.value, "PPP 'at' h:mm a") ) : ( <span>Pick a date</span> )} <CalendarIcon className="ml-auto h-4 w-4 opacity-50" /> </Button> </PopoverTrigger> </FormControl> <PopoverContent className="pointer-events-auto w-auto p-0" align="start" side={mobile ? "bottom" : "left"} > <Calendar mode="single" selected={field.value} onSelect={(selectedDate) => { if (!selectedDate) return;
const newDate = new Date(selectedDate); newDate.setHours( field.value.getHours(), field.value.getMinutes(), field.value.getSeconds(), field.value.getMilliseconds(), ); field.onChange(newDate); }} initialFocus /> <div className="border-t p-3"> <div className="flex items-center gap-3"> <Label htmlFor="time-end" className="text-xs"> Enter time </Label> <div className="relative grow"> <Input id="time-end" type="time" step="1" value={ field.value ? field.value.toTimeString().slice(0, 8) : new Date().toTimeString().slice(0, 8) } className="peer appearance-none ps-9 [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none" onChange={(e) => { try { const timeValue = e.target.value; if (!timeValue || !field.value) return;
const [hours, minutes, seconds] = timeValue .split(":") .map(Number);
const newDate = new Date(field.value); newDate.setHours( hours, minutes, seconds || 0, 0, );
field.onChange(newDate); } catch (error) { console.error(error); } }} /> <div className="text-muted-foreground/80 pointer-events-none absolute inset-y-0 start-0 flex items-center justify-center ps-3 peer-disabled:opacity-50"> <Clock size={16} aria-hidden="true" /> </div> </div> </div> </div> </PopoverContent> </Popover> <FormDescription> When the maintenance ends. Shown in your timezone ( <code className="font-commit-mono text-foreground/70"> {timezone} </code> ) and saved as Unix time ( <code className="font-commit-mono text-foreground/70"> UTC </code> ). </FormDescription> <FormMessage /> </FormItem> )} /> </FormCardContent> <FormCardSeparator /> <FormCardContent> <Tabs defaultValue="tab-1"> <TabsList> <TabsTrigger value="tab-1">Writing</TabsTrigger> <TabsTrigger value="tab-2">Preview</TabsTrigger> </TabsList> <TabsContent value="tab-1"> <FormField control={form.control} name="message" render={({ field }) => ( <FormItem> <FormLabel>Message</FormLabel> <FormControl> <Textarea rows={6} {...field} /> </FormControl> <FormMessage /> <FormDescription>Markdown support</FormDescription> </FormItem> )} /> </TabsContent> <TabsContent value="tab-2"> <div className="grid gap-2"> <Label>Preview</Label> <div className="prose dark:prose-invert prose-sm text-foreground rounded-md border px-3 py-2 text-sm"> <ProcessMessage value={watchMessage} /> </div> </div> </TabsContent> </Tabs> </FormCardContent> <FormCardSeparator /> <FormCardContent> <FormField control={form.control} name="pageComponents" render={({ field }) => ( <FormItem> <FormLabel>Page Components</FormLabel> <FormDescription> Connected page components will be affected for the period of time. </FormDescription> {items.length ? ( <FormControl> <CheckboxTree items={items} value={field.value ?? []} onValueChange={field.onChange} /> </FormControl> ) : ( <EmptyStateContainer> <EmptyStateTitle>No page components found</EmptyStateTitle> </EmptyStateContainer> )} <FormMessage /> </FormItem> )} /> </FormCardContent> {!defaultValues && workspace?.limits["status-subscribers"] ? ( <> <FormCardSeparator /> <FormCardContent> <FormField control={form.control} name="notifySubscribers" render={({ field }) => ( <FormItem> <FormLabel>Notify Subscribers</FormLabel> <FormControl> <div className="flex items-center gap-2"> <Checkbox id="notifySubscribers" checked={field.value} onCheckedChange={field.onChange} /> <Label htmlFor="notifySubscribers"> Send notification to subscribers </Label> </div> </FormControl> <FormMessage /> <FormDescription> Subscribers will be notified when creating a maintenance. </FormDescription> </FormItem> )} /> </FormCardContent> </> ) : null} </form> </Form> );}