Openstatus www.openstatus.dev

๐Ÿš€ monitor and page creation (#45)

* ๐Ÿ”ฅ creation form

* ๐Ÿงน

* ๐ŸŸข fix build

* ๐ŸŸข fix build

authored by

Thibault Le Ouay and committed by
GitHub
0bcd600e 9beb5c70

+589 -159
+2
apps/web/package.json
··· 10 10 }, 11 11 "dependencies": { 12 12 "@clerk/nextjs": "4.21.14", 13 + "@hookform/resolvers": "^3.1.1", 13 14 "@openstatus/api": "workspace:^", 14 15 "@openstatus/db": "workspace:^", 15 16 "@openstatus/tinybird": "workspace:*", ··· 41 42 "next-plausible": "3.7.2", 42 43 "react": "18.2.0", 43 44 "react-dom": "18.2.0", 45 + "react-hook-form": "^7.45.1", 44 46 "resend": "^0.15.3", 45 47 "superjson": "1.9.1", 46 48 "svix": "1.4.12",
-13
apps/web/src/app/app/(dashboard)/[workspaceId]/endpoint/loading.tsx
··· 1 - import { Container } from "@/components/dashboard/container"; 2 - import { Header } from "@/components/dashboard/header"; 3 - 4 - export default function Loading() { 5 - return ( 6 - <div className="grid gap-6 md:grid-cols-2 md:gap-8"> 7 - <Header.Skeleton /> 8 - <Container.Skeleton /> 9 - <Container.Skeleton /> 10 - <Container.Skeleton /> 11 - </div> 12 - ); 13 - }
-16
apps/web/src/app/app/(dashboard)/[workspaceId]/endpoint/page.tsx
··· 1 - import * as React from "react"; 2 - 3 - import { Container } from "@/components/dashboard/container"; 4 - import { Header } from "@/components/dashboard/header"; 5 - import { wait } from "@/lib/utils"; 6 - 7 - export default async function EndpointPage() { 8 - await wait(1000); 9 - return ( 10 - <div className="grid gap-6 md:grid-cols-2 md:gap-8"> 11 - <Header title="Endpoint" description="Overview of all your sites." /> 12 - <Container title="Hello"></Container> 13 - <Container title="World"></Container> 14 - </div> 15 - ); 16 - }
apps/web/src/app/app/(dashboard)/[workspaceId]/incident/loading.tsx apps/web/src/app/app/(dashboard)/[workspaceId]/incidents/loading.tsx
+1 -2
apps/web/src/app/app/(dashboard)/[workspaceId]/incident/page.tsx apps/web/src/app/app/(dashboard)/[workspaceId]/incidents/page.tsx
··· 2 2 3 3 import { Container } from "@/components/dashboard/container"; 4 4 import { Header } from "@/components/dashboard/header"; 5 - import { wait } from "@/lib/utils"; 6 5 import { api } from "@/trpc/server"; 7 6 8 7 export default async function IncidentPage({ ··· 15 14 }); 16 15 return ( 17 16 <div className="grid gap-6 md:grid-cols-2 md:gap-8"> 18 - <Header title="Monitor" description="Overview of all the responses." /> 17 + <Header title="Incidents" description="Overview of all your incidents." /> 19 18 <Container title="Hello"></Container> 20 19 <Container title="World"></Container> 21 20 </div>
apps/web/src/app/app/(dashboard)/[workspaceId]/monitor/loading.tsx apps/web/src/app/app/(dashboard)/[workspaceId]/monitors/loading.tsx
-24
apps/web/src/app/app/(dashboard)/[workspaceId]/monitor/page.tsx
··· 1 - import * as React from "react"; 2 - 3 - import { Container } from "@/components/dashboard/container"; 4 - import { Header } from "@/components/dashboard/header"; 5 - import { api } from "@/trpc/server"; 6 - 7 - export default async function MonitorPage({ 8 - params, 9 - }: { 10 - params: { workspaceId: string }; 11 - }) { 12 - const monitors = await api.monitor.getMonitorsByWorkspace.query({ 13 - workspaceId: Number(params.workspaceId), 14 - }); 15 - console.log(monitors); 16 - // iterate over monitors 17 - return ( 18 - <div className="grid gap-6 md:grid-cols-2 md:gap-8"> 19 - <Header title="Monitor" description="Overview of all the responses." /> 20 - <Container title="Hello"></Container> 21 - <Container title="World"></Container> 22 - </div> 23 - ); 24 - }
+31
apps/web/src/app/app/(dashboard)/[workspaceId]/monitors/page.tsx
··· 1 + import * as React from "react"; 2 + 3 + import { Container } from "@/components/dashboard/container"; 4 + import { Header } from "@/components/dashboard/header"; 5 + import { MonitorCreateForm } from "@/components/forms/montitor-form"; 6 + import { api } from "@/trpc/server"; 7 + 8 + export default async function MonitorPage({ 9 + params, 10 + }: { 11 + params: { workspaceId: string }; 12 + }) { 13 + const monitors = await api.monitor.getMonitorsByWorkspace.query({ 14 + workspaceId: Number(params.workspaceId), 15 + }); 16 + // iterate over monitors 17 + return ( 18 + <div className="grid gap-6 md:grid-cols-2 md:gap-8"> 19 + <Header title="Monitors" description="Overview of all your monitors."> 20 + <MonitorCreateForm /> 21 + </Header> 22 + {monitors.map((monitor, index) => ( 23 + <Container 24 + key={index} 25 + title={monitor.url} 26 + description={monitor.name} 27 + ></Container> 28 + ))} 29 + </div> 30 + ); 31 + }
+2 -7
apps/web/src/app/app/(dashboard)/[workspaceId]/page.tsx
··· 2 2 3 3 import { Container } from "@/components/dashboard/container"; 4 4 import { Header } from "@/components/dashboard/header"; 5 - import { DialogForm } from "@/components/forms/dialog-form"; 6 - import { Button } from "@/components/ui/button"; 7 - import { wait } from "@/lib/utils"; 5 + import { StatusPageCreateForm } from "@/components/forms/status-page-form"; 8 6 import { api } from "@/trpc/server"; 9 7 import Loading from "./loading"; 10 8 ··· 17 15 return ( 18 16 <div className="grid gap-6 md:grid-cols-2 md:gap-8"> 19 17 <div className="col-span-full flex w-full justify-between"> 20 - <Header title="Dashboard" description="Overview of all your websites"> 21 - {/* <Button>Create</Button> */} 22 - <DialogForm /> 23 - </Header> 18 + <Header title="Dashboard" description="Overview of all your websites" /> 24 19 </div> 25 20 <Container title="Hello"></Container> 26 21 <Container title="World"></Container>
apps/web/src/app/app/(dashboard)/[workspaceId]/page/loading.tsx apps/web/src/app/app/(dashboard)/[workspaceId]/status-pages/loading.tsx
+7 -4
apps/web/src/app/app/(dashboard)/[workspaceId]/page/page.tsx apps/web/src/app/app/(dashboard)/[workspaceId]/status-pages/page.tsx
··· 2 2 3 3 import { Container } from "@/components/dashboard/container"; 4 4 import { Header } from "@/components/dashboard/header"; 5 - import { wait } from "@/lib/utils"; 5 + import { StatusPageCreateForm } from "@/components/forms/status-page-form"; 6 6 import { api } from "@/trpc/server"; 7 7 8 8 export default async function Page({ ··· 19 19 <Header 20 20 title="Status Page" 21 21 description="Overview of all your status page." 22 - /> 23 - <Container title="Hello"></Container> 24 - <Container title="World"></Container> 22 + > 23 + <StatusPageCreateForm /> 24 + </Header> 25 + {pages.map((page, index) => ( 26 + <Container key={index} title={page.title}></Container> 27 + ))} 25 28 </div> 26 29 ); 27 30 }
-71
apps/web/src/components/forms/dialog-form.tsx
··· 1 - "use client"; 2 - 3 - import * as React from "react"; 4 - import { Loader2 } from "lucide-react"; 5 - 6 - import { 7 - Dialog, 8 - DialogContent, 9 - DialogDescription, 10 - DialogFooter, 11 - DialogHeader, 12 - DialogTitle, 13 - DialogTrigger, 14 - } from "@/components/ui/dialog"; 15 - import { wait } from "@/lib/utils"; 16 - import { Button } from "../ui/button"; 17 - import { Input } from "../ui/input"; 18 - import { Label } from "../ui/label"; 19 - 20 - // EXAMPLE 21 - export function DialogForm() { 22 - const [saving, setSaving] = React.useState(false); 23 - const [open, setOpen] = React.useState(false); 24 - 25 - // either like that or with a user action 26 - async function handleSubmit(e: React.FormEvent<HTMLFormElement>) { 27 - e.preventDefault(); 28 - setSaving(true); 29 - const data = Object.fromEntries(new FormData(e.currentTarget)); 30 - 31 - console.log(data); // { url: "" } 32 - await wait(1500); 33 - // save data 34 - 35 - setOpen(false); 36 - setSaving(false); 37 - } 38 - 39 - return ( 40 - <Dialog open={open} onOpenChange={(value) => setOpen(value)}> 41 - <DialogTrigger asChild> 42 - <Button>Create</Button> 43 - </DialogTrigger> 44 - <DialogContent> 45 - <DialogHeader> 46 - <DialogTitle>Create Monitor</DialogTitle> 47 - <DialogDescription> 48 - Type an URL that you want to ping periodically. 49 - </DialogDescription> 50 - </DialogHeader> 51 - <form onSubmit={handleSubmit} id="monitor"> 52 - <div className="grid w-full items-center gap-1.5"> 53 - <Label htmlFor="url">Link</Label> 54 - <Input 55 - id="url" 56 - name="url" 57 - type="url" 58 - placeholder="https://" 59 - required 60 - /> 61 - </div> 62 - </form> 63 - <DialogFooter> 64 - <Button type="submit" form="monitor" disabled={saving}> 65 - {!saving ? "Confirm" : <Loader2 className="h-4 w-4 animate-spin" />} 66 - </Button> 67 - </DialogFooter> 68 - </DialogContent> 69 - </Dialog> 70 - ); 71 - }
+183
apps/web/src/components/forms/montitor-form.tsx
··· 1 + "use client"; 2 + 3 + import * as React from "react"; 4 + import { useParams, useRouter } from "next/navigation"; 5 + import { zodResolver } from "@hookform/resolvers/zod"; 6 + import { Loader2 } from "lucide-react"; 7 + import { useForm } from "react-hook-form"; 8 + import type * as z from "zod"; 9 + 10 + import { 11 + insertMonitorSchema, 12 + periodicityEnum, 13 + } from "@openstatus/db/src/schema"; 14 + 15 + import { Button } from "@/components/ui/button"; 16 + import { 17 + Dialog, 18 + DialogContent, 19 + DialogDescription, 20 + DialogFooter, 21 + DialogHeader, 22 + DialogTitle, 23 + DialogTrigger, 24 + } from "@/components/ui/dialog"; 25 + import { 26 + Form, 27 + FormControl, 28 + FormDescription, 29 + FormField, 30 + FormItem, 31 + FormLabel, 32 + FormMessage, 33 + } from "@/components/ui/form"; 34 + import { Input } from "@/components/ui/input"; 35 + import { api } from "@/trpc/client"; 36 + import { 37 + Select, 38 + SelectContent, 39 + SelectItem, 40 + SelectTrigger, 41 + SelectValue, 42 + } from "../ui/select"; 43 + 44 + // EXAMPLE 45 + export function MonitorCreateForm() { 46 + const [saving, setSaving] = React.useState(false); 47 + const [open, setOpen] = React.useState(false); 48 + const params = useParams(); 49 + const router = useRouter(); 50 + 51 + const form = useForm<z.infer<typeof insertMonitorSchema>>({ 52 + resolver: zodResolver(insertMonitorSchema), 53 + defaultValues: { 54 + name: "", 55 + url: "", 56 + description: "", 57 + workspaceId: Number(params.workspaceId), 58 + }, 59 + }); 60 + 61 + // either like that or with a user action 62 + async function onSubmit(values: z.infer<typeof insertMonitorSchema>) { 63 + setSaving(true); 64 + // await api.monitor.getMonitorsByWorkspace.revalidate(); 65 + await api.monitor.createMonitor.mutate(values); 66 + router.refresh(); 67 + setOpen(false); 68 + setSaving(false); 69 + } 70 + 71 + return ( 72 + <Dialog open={open} onOpenChange={(value) => setOpen(value)}> 73 + <DialogTrigger asChild> 74 + <Button>Create</Button> 75 + </DialogTrigger> 76 + <DialogContent> 77 + <DialogHeader> 78 + <DialogTitle>Create Monitor</DialogTitle> 79 + <DialogDescription>Create a monitor</DialogDescription> 80 + </DialogHeader> 81 + <Form {...form}> 82 + <form onSubmit={form.handleSubmit(onSubmit)} id="monitor"> 83 + <div className="grid w-full items-center space-y-6"> 84 + <FormField 85 + control={form.control} 86 + name="url" 87 + render={({ field }) => ( 88 + <FormItem> 89 + <FormLabel>URL</FormLabel> 90 + <FormControl> 91 + <Input placeholder="" {...field} /> 92 + </FormControl> 93 + <FormDescription> 94 + This is url you want to monitor. 95 + </FormDescription> 96 + <FormMessage /> 97 + </FormItem> 98 + )} 99 + /> 100 + <FormField 101 + control={form.control} 102 + name="name" 103 + render={({ field }) => ( 104 + <FormItem> 105 + <FormLabel>Name</FormLabel> 106 + <FormControl> 107 + <Input placeholder="" {...field} /> 108 + </FormControl> 109 + <FormDescription> 110 + The name of the monitor that will be displayed. 111 + </FormDescription> 112 + <FormMessage /> 113 + </FormItem> 114 + )} 115 + /> 116 + <FormField 117 + control={form.control} 118 + name="description" 119 + render={({ field }) => ( 120 + <FormItem> 121 + <FormLabel>Description</FormLabel> 122 + <FormControl> 123 + <Input placeholder="" {...field} /> 124 + </FormControl> 125 + <FormDescription> 126 + Give your user some information about it. 127 + </FormDescription> 128 + <FormMessage /> 129 + </FormItem> 130 + )} 131 + /> 132 + <FormField 133 + control={form.control} 134 + name="periodicity" 135 + render={({ field }) => ( 136 + <FormItem> 137 + <FormLabel>Email</FormLabel> 138 + <Select 139 + onValueChange={(value) => 140 + field.onChange(periodicityEnum.parse(value)) 141 + } 142 + defaultValue={field.value} 143 + > 144 + <FormControl> 145 + <SelectTrigger> 146 + <SelectValue placeholder="How often it should check" /> 147 + </SelectTrigger> 148 + </FormControl> 149 + <SelectContent> 150 + <SelectItem value="1m" disabled> 151 + 1 minute 152 + </SelectItem> 153 + <SelectItem value="5m" disabled> 154 + 5 minutes 155 + </SelectItem> 156 + <SelectItem value="10m">10 minutes</SelectItem> 157 + <SelectItem value="30m" disabled> 158 + 30 minutes 159 + </SelectItem> 160 + <SelectItem value="1h" disabled> 161 + 1 hour 162 + </SelectItem> 163 + </SelectContent> 164 + </Select> 165 + <FormDescription> 166 + You can manage email addresses in your{" "} 167 + </FormDescription> 168 + <FormMessage /> 169 + </FormItem> 170 + )} 171 + /> 172 + </div> 173 + </form> 174 + </Form> 175 + <DialogFooter> 176 + <Button type="submit" form="monitor" disabled={saving}> 177 + {!saving ? "Confirm" : <Loader2 className="h-4 w-4 animate-spin" />} 178 + </Button> 179 + </DialogFooter> 180 + </DialogContent> 181 + </Dialog> 182 + ); 183 + }
+139
apps/web/src/components/forms/status-page-form.tsx
··· 1 + "use client"; 2 + 3 + import { useState } from "react"; 4 + import { useParams, useRouter } from "next/navigation"; 5 + import { zodResolver } from "@hookform/resolvers/zod"; 6 + import { Loader2 } from "lucide-react"; 7 + import { useForm } from "react-hook-form"; 8 + import type * as z from "zod"; 9 + 10 + import type { insertMonitorSchema } from "@openstatus/db/src/schema"; 11 + import { insertPageSchema, periodicityEnum } from "@openstatus/db/src/schema"; 12 + 13 + import { Button } from "@/components/ui/button"; 14 + import { 15 + Dialog, 16 + DialogContent, 17 + DialogDescription, 18 + DialogFooter, 19 + DialogHeader, 20 + DialogTitle, 21 + DialogTrigger, 22 + } from "@/components/ui/dialog"; 23 + import { 24 + Form, 25 + FormControl, 26 + FormDescription, 27 + FormField, 28 + FormItem, 29 + FormLabel, 30 + FormMessage, 31 + } from "@/components/ui/form"; 32 + import { Input } from "@/components/ui/input"; 33 + import { api } from "@/trpc/client"; 34 + 35 + // EXAMPLE 36 + export function StatusPageCreateForm() { 37 + const [saving, setSaving] = useState(false); 38 + const [open, setOpen] = useState(false); 39 + const params = useParams(); 40 + const router = useRouter(); 41 + 42 + const form = useForm<z.infer<typeof insertPageSchema>>({ 43 + resolver: zodResolver(insertPageSchema), 44 + defaultValues: { 45 + title: "", 46 + slug: "", 47 + description: "", 48 + workspaceId: Number(params.workspaceId), 49 + }, 50 + }); 51 + 52 + // either like that or with a user action 53 + async function onSubmit(values: z.infer<typeof insertPageSchema>) { 54 + setSaving(true); 55 + // await api.monitor.getMonitorsByWorkspace.revalidate(); 56 + await api.page.createPage.mutate(values); 57 + router.refresh(); 58 + setOpen(false); 59 + setSaving(false); 60 + } 61 + 62 + return ( 63 + <Dialog open={open} onOpenChange={(value) => setOpen(value)}> 64 + <DialogTrigger asChild> 65 + <Button>Create</Button> 66 + </DialogTrigger> 67 + <DialogContent> 68 + <DialogHeader> 69 + <DialogTitle>Create Monitor</DialogTitle> 70 + <DialogDescription>Create a monitor</DialogDescription> 71 + </DialogHeader> 72 + <Form {...form}> 73 + <form 74 + onSubmit={form.handleSubmit(onSubmit, (e) => { 75 + console.log(e); 76 + })} 77 + id="monitor" 78 + > 79 + <div className="grid w-full items-center space-y-6"> 80 + <FormField 81 + control={form.control} 82 + name="title" 83 + render={({ field }) => ( 84 + <FormItem> 85 + <FormLabel>Title</FormLabel> 86 + <FormControl> 87 + <Input placeholder="" {...field} /> 88 + </FormControl> 89 + <FormDescription> 90 + This is title of your page. 91 + </FormDescription> 92 + <FormMessage /> 93 + </FormItem> 94 + )} 95 + /> 96 + <FormField 97 + control={form.control} 98 + name="slug" 99 + render={({ field }) => ( 100 + <FormItem> 101 + <FormLabel>Slug</FormLabel> 102 + <FormControl> 103 + <Input placeholder="" {...field} /> 104 + </FormControl> 105 + <FormDescription> 106 + This is your url of your page. 107 + </FormDescription> 108 + <FormMessage /> 109 + </FormItem> 110 + )} 111 + /> 112 + <FormField 113 + control={form.control} 114 + name="description" 115 + render={({ field }) => ( 116 + <FormItem> 117 + <FormLabel>Description</FormLabel> 118 + <FormControl> 119 + <Input placeholder="" {...field} /> 120 + </FormControl> 121 + <FormDescription> 122 + Give your user some information about it. 123 + </FormDescription> 124 + <FormMessage /> 125 + </FormItem> 126 + )} 127 + /> 128 + </div> 129 + </form> 130 + </Form> 131 + <DialogFooter> 132 + <Button type="submit" form="monitor" disabled={saving}> 133 + {!saving ? "Confirm" : <Loader2 className="h-4 w-4 animate-spin" />} 134 + </Button> 135 + </DialogFooter> 136 + </DialogContent> 137 + </Dialog> 138 + ); 139 + }
+1 -2
apps/web/src/components/layout/app-sidebar.tsx
··· 12 12 const params = useParams(); 13 13 return ( 14 14 <ul className="grid gap-1"> 15 - {pagesConfig.map(({ title, href, icon, disabled }) => { 15 + {pagesConfig.map(({ title, href, icon }) => { 16 16 const Icon = Icons[icon]; 17 17 const link = `/app/${params.workspaceId}${href}`; // TODO: add 18 18 return ( ··· 23 23 "hover:bg-muted/50 hover:text-foreground text-muted-foreground group flex w-full min-w-[200px] items-center rounded-md border border-transparent px-3 py-1", 24 24 pathname === link && 25 25 "bg-muted/50 border-border text-foreground", 26 - disabled && "pointer-events-none opacity-60", 27 26 )} 28 27 > 29 28 <Icon className={cn("mr-2 h-4 w-4")} />
+171
apps/web/src/components/ui/form.tsx
··· 1 + import * as React from "react"; 2 + import type * as LabelPrimitive from "@radix-ui/react-label"; 3 + import { Slot } from "@radix-ui/react-slot"; 4 + import type { ControllerProps, FieldPath, FieldValues } from "react-hook-form"; 5 + import { Controller, FormProvider, useFormContext } from "react-hook-form"; 6 + 7 + import { Label } from "@/components/ui/label"; 8 + import { cn } from "@/lib/utils"; 9 + 10 + const Form = FormProvider; 11 + 12 + type FormFieldContextValue< 13 + TFieldValues extends FieldValues = FieldValues, 14 + TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>, 15 + > = { 16 + name: TName; 17 + }; 18 + 19 + const FormFieldContext = React.createContext<FormFieldContextValue>( 20 + {} as FormFieldContextValue, 21 + ); 22 + 23 + const FormField = < 24 + TFieldValues extends FieldValues = FieldValues, 25 + TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>, 26 + >({ 27 + ...props 28 + }: ControllerProps<TFieldValues, TName>) => { 29 + return ( 30 + <FormFieldContext.Provider value={{ name: props.name }}> 31 + <Controller {...props} /> 32 + </FormFieldContext.Provider> 33 + ); 34 + }; 35 + 36 + const useFormField = () => { 37 + const fieldContext = React.useContext(FormFieldContext); 38 + const itemContext = React.useContext(FormItemContext); 39 + const { getFieldState, formState } = useFormContext(); 40 + 41 + const fieldState = getFieldState(fieldContext.name, formState); 42 + 43 + if (!fieldContext) { 44 + throw new Error("useFormField should be used within <FormField>"); 45 + } 46 + 47 + const { id } = itemContext; 48 + 49 + return { 50 + id, 51 + name: fieldContext.name, 52 + formItemId: `${id}-form-item`, 53 + formDescriptionId: `${id}-form-item-description`, 54 + formMessageId: `${id}-form-item-message`, 55 + ...fieldState, 56 + }; 57 + }; 58 + 59 + type FormItemContextValue = { 60 + id: string; 61 + }; 62 + 63 + const FormItemContext = React.createContext<FormItemContextValue>( 64 + {} as FormItemContextValue, 65 + ); 66 + 67 + const FormItem = React.forwardRef< 68 + HTMLDivElement, 69 + React.HTMLAttributes<HTMLDivElement> 70 + >(({ className, ...props }, ref) => { 71 + const id = React.useId(); 72 + 73 + return ( 74 + <FormItemContext.Provider value={{ id }}> 75 + <div ref={ref} className={cn("space-y-2", className)} {...props} /> 76 + </FormItemContext.Provider> 77 + ); 78 + }); 79 + FormItem.displayName = "FormItem"; 80 + 81 + const FormLabel = React.forwardRef< 82 + React.ElementRef<typeof LabelPrimitive.Root>, 83 + React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> 84 + >(({ className, ...props }, ref) => { 85 + const { error, formItemId } = useFormField(); 86 + 87 + return ( 88 + <Label 89 + ref={ref} 90 + className={cn(error && "text-destructive", className)} 91 + htmlFor={formItemId} 92 + {...props} 93 + /> 94 + ); 95 + }); 96 + FormLabel.displayName = "FormLabel"; 97 + 98 + const FormControl = React.forwardRef< 99 + React.ElementRef<typeof Slot>, 100 + React.ComponentPropsWithoutRef<typeof Slot> 101 + >(({ ...props }, ref) => { 102 + const { error, formItemId, formDescriptionId, formMessageId } = 103 + useFormField(); 104 + 105 + return ( 106 + <Slot 107 + ref={ref} 108 + id={formItemId} 109 + aria-describedby={ 110 + !error 111 + ? `${formDescriptionId}` 112 + : `${formDescriptionId} ${formMessageId}` 113 + } 114 + aria-invalid={!!error} 115 + {...props} 116 + /> 117 + ); 118 + }); 119 + FormControl.displayName = "FormControl"; 120 + 121 + const FormDescription = React.forwardRef< 122 + HTMLParagraphElement, 123 + React.HTMLAttributes<HTMLParagraphElement> 124 + >(({ className, ...props }, ref) => { 125 + const { formDescriptionId } = useFormField(); 126 + 127 + return ( 128 + <p 129 + ref={ref} 130 + id={formDescriptionId} 131 + className={cn("text-muted-foreground text-sm", className)} 132 + {...props} 133 + /> 134 + ); 135 + }); 136 + FormDescription.displayName = "FormDescription"; 137 + 138 + const FormMessage = React.forwardRef< 139 + HTMLParagraphElement, 140 + React.HTMLAttributes<HTMLParagraphElement> 141 + >(({ className, children, ...props }, ref) => { 142 + const { error, formMessageId } = useFormField(); 143 + const body = error ? String(error?.message) : children; 144 + 145 + if (!body) { 146 + return null; 147 + } 148 + 149 + return ( 150 + <p 151 + ref={ref} 152 + id={formMessageId} 153 + className={cn("text-destructive text-sm font-medium", className)} 154 + {...props} 155 + > 156 + {body} 157 + </p> 158 + ); 159 + }); 160 + FormMessage.displayName = "FormMessage"; 161 + 162 + export { 163 + useFormField, 164 + Form, 165 + FormItem, 166 + FormLabel, 167 + FormControl, 168 + FormDescription, 169 + FormMessage, 170 + FormField, 171 + };
+5 -11
apps/web/src/config/pages.ts
··· 15 15 href: "", 16 16 icon: "layout-dashboard", 17 17 }, 18 - { 19 - title: "Endpoints", 20 - description: "Keep track of all your endpoints.", 21 - href: "/endpoint", 22 - icon: "link", 23 - }, 18 + 24 19 { 25 20 title: "Monitors", 26 21 description: "Check all the responses in one place.", 27 - href: "/monitor", 22 + href: "/monitors", 28 23 icon: "activity", 29 24 }, 30 25 { 31 - title: "Pages", 26 + title: "Status Pages", 32 27 description: "Wher you can see all the pages.", 33 - href: "/page", 28 + href: "/status-pages", 34 29 icon: "panel-top", 35 30 }, 36 31 { 37 32 title: "Incidents", 38 33 description: "War room where you handle the incidents.", 39 - href: "/incident", 34 + href: "/incidents", 40 35 icon: "siren", 41 - disabled: true, 42 36 }, 43 37 // ... 44 38 ] satisfies Page[];
-1
apps/web/src/middleware.ts
··· 32 32 auth.userId && 33 33 (req.nextUrl.pathname === "/app" || req.nextUrl.pathname === "/app/") 34 34 ) { 35 - console.log(auth.userId); 36 35 // improve on sign-up if the webhook has not been triggered yet 37 36 const userQuery = db 38 37 .select()
+16 -4
packages/db/src/schema/monitor.ts
··· 8 8 varchar, 9 9 } from "drizzle-orm/mysql-core"; 10 10 import { createInsertSchema, createSelectSchema } from "drizzle-zod"; 11 + import { z } from "zod"; 11 12 12 13 import { page } from "./page"; 13 14 import { workspace } from "./workspace"; ··· 31 32 .default("inactive") 32 33 .notNull(), 33 34 34 - url: varchar("url", { length: 512 }), 35 + url: varchar("url", { length: 512 }).notNull(), 35 36 36 - name: varchar("name", { length: 256 }), 37 - description: text("description"), 37 + name: varchar("name", { length: 256 }).default("").notNull(), 38 + description: text("description").default("").notNull(), 38 39 39 40 pageId: int("page_id"), 40 41 workspaceId: int("workspace_id"), ··· 54 55 }), 55 56 })); 56 57 58 + export const periodicityEnum = z.enum([ 59 + "1m", 60 + "5m", 61 + "10m", 62 + "30m", 63 + "1h", 64 + "other", 65 + ]); 57 66 // Schema for inserting a Monitor - can be used to validate API requests 58 - export const insertMonitorSchema = createInsertSchema(monitor); 67 + export const insertMonitorSchema = createInsertSchema(monitor, { 68 + periodicity: periodicityEnum, 69 + url: z.string().url(), 70 + }); 59 71 60 72 // Schema for selecting a Monitor - can be used to validate API responses 61 73 export const selectMonitorSchema = createSelectSchema(monitor);
+8 -4
packages/db/src/schema/page.ts
··· 7 7 varchar, 8 8 } from "drizzle-orm/mysql-core"; 9 9 import { createInsertSchema, createSelectSchema } from "drizzle-zod"; 10 + import { z } from "zod"; 10 11 11 12 import { incident } from "./incident"; 12 13 import { monitor } from "./monitor"; ··· 16 17 17 18 workspaceId: int("workspace_id").notNull(), 18 19 19 - title: text("title"), // title of the page 20 + title: text("title").notNull(), // title of the page 21 + description: text("description").notNull(), // description of the page 20 22 icon: varchar("icon", { length: 256 }), // icon of the page 21 - slug: varchar("slug", { length: 256 }), // which is used for https://slug.openstatus.dev 22 - customDomain: varchar("custom_domain", { length: 256 }), 23 + slug: varchar("slug", { length: 256 }).notNull(), // which is used for https://slug.openstatus.dev 24 + customDomain: varchar("custom_domain", { length: 256 }).notNull().default(""), 23 25 24 26 // We should store settings of the page 25 27 // theme ··· 38 40 })); 39 41 40 42 // Schema for inserting a Page - can be used to validate API requests 41 - export const insertPageSchema = createInsertSchema(page); 43 + export const insertPageSchema = createInsertSchema(page, { 44 + customDomain: z.string().optional(), 45 + }); 42 46 43 47 // Schema for selecting a Page - can be used to validate API responses 44 48 export const selectPageSchema = createSelectSchema(page);
+23
pnpm-lock.yaml
··· 38 38 '@clerk/nextjs': 39 39 specifier: 4.21.14 40 40 version: 4.21.14(next@13.4.8)(react-dom@18.2.0)(react@18.2.0) 41 + '@hookform/resolvers': 42 + specifier: ^3.1.1 43 + version: 3.1.1(react-hook-form@7.45.1) 41 44 '@openstatus/api': 42 45 specifier: workspace:^ 43 46 version: link:../../packages/api ··· 131 134 react-dom: 132 135 specifier: 18.2.0 133 136 version: 18.2.0(react@18.2.0) 137 + react-hook-form: 138 + specifier: ^7.45.1 139 + version: 7.45.1(react@18.2.0) 134 140 resend: 135 141 specifier: ^0.15.3 136 142 version: 0.15.3 ··· 1374 1380 '@floating-ui/dom': 1.4.3 1375 1381 react: 18.2.0 1376 1382 react-dom: 18.2.0(react@18.2.0) 1383 + dev: false 1384 + 1385 + /@hookform/resolvers@3.1.1(react-hook-form@7.45.1): 1386 + resolution: {integrity: sha512-tS16bAUkqjITNSvbJuO1x7MXbn7Oe8ZziDTJdA9mMvsoYthnOOiznOTGBYwbdlYBgU+tgpI/BtTU3paRbCuSlg==} 1387 + peerDependencies: 1388 + react-hook-form: ^7.0.0 1389 + dependencies: 1390 + react-hook-form: 7.45.1(react@18.2.0) 1377 1391 dev: false 1378 1392 1379 1393 /@humanwhocodes/config-array@0.11.10: ··· 6233 6247 loose-envify: 1.4.0 6234 6248 react: 18.2.0 6235 6249 scheduler: 0.23.0 6250 + dev: false 6251 + 6252 + /react-hook-form@7.45.1(react@18.2.0): 6253 + resolution: {integrity: sha512-6dWoFJwycbuFfw/iKMcl+RdAOAOHDiF11KWYhNDRN/OkUt+Di5qsZHwA0OwsVnu9y135gkHpTw9DJA+WzCeR9w==} 6254 + engines: {node: '>=12.22.0'} 6255 + peerDependencies: 6256 + react: ^16.8.0 || ^17 || ^18 6257 + dependencies: 6258 + react: 18.2.0 6236 6259 dev: false 6237 6260 6238 6261 /react-is@16.13.1: