Openstatus www.openstatus.dev
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

Feat/user workflow (#165)

* feat: new user workflow forms

* fix: clerk provider

* fix: remove ts-ignore

* chore: remove log

authored by

Maximilian Kaske and committed by
GitHub
221c90da f6865edc

+666 -614
apps/web/src/app/app/(dashboard)/[workspaceSlug]/loading.tsx apps/web/src/app/app/(dashboard)/[workspaceSlug]/status-pages/loading.tsx
+44 -104
apps/web/src/app/app/(dashboard)/[workspaceSlug]/monitors/_components/action-button.tsx
··· 8 8 9 9 import type { insertMonitorSchema } from "@openstatus/db/src/schema"; 10 10 11 - import { MonitorForm } from "@/components/forms/montitor-form"; 12 11 import { LoadingAnimation } from "@/components/loading-animation"; 13 12 import { 14 13 AlertDialog, ··· 22 21 AlertDialogTrigger, 23 22 } from "@/components/ui/alert-dialog"; 24 23 import { Button } from "@/components/ui/button"; 25 - import { 26 - Dialog, 27 - DialogContent, 28 - DialogDescription, 29 - DialogFooter, 30 - DialogHeader, 31 - DialogTitle, 32 - DialogTrigger, 33 - } from "@/components/ui/dialog"; 34 24 import { 35 25 DropdownMenu, 36 26 DropdownMenuContent, 37 27 DropdownMenuItem, 38 28 DropdownMenuTrigger, 39 29 } from "@/components/ui/dropdown-menu"; 40 - import { useToast } from "@/components/ui/use-toast"; 41 30 import { api } from "@/trpc/client"; 42 31 43 32 type Schema = z.infer<typeof insertMonitorSchema>; 44 33 45 34 export function ActionButton(props: Schema & { workspaceSlug: string }) { 46 35 const router = useRouter(); 47 - const [dialogOpen, setDialogOpen] = React.useState(false); 48 36 const [alertOpen, setAlertOpen] = React.useState(false); 49 37 const [isPending, startTransition] = React.useTransition(); 50 - const { toast } = useToast(); 51 - 52 - async function onUpdate(values: Schema) { 53 - startTransition(async () => { 54 - try { 55 - await api.monitor.updateMonitor.mutate({ ...values }); 56 - router.refresh(); 57 - setDialogOpen(false); 58 - } catch { 59 - toast({ 60 - title: "Something went wrong.", 61 - description: "If you are in the limits, please try again.", 62 - }); 63 - } 64 - }); 65 - } 66 38 67 39 async function onDelete() { 68 40 startTransition(async () => { ··· 74 46 } 75 47 76 48 return ( 77 - <Dialog open={dialogOpen} onOpenChange={(value) => setDialogOpen(value)}> 78 - <AlertDialog 79 - open={alertOpen} 80 - onOpenChange={(value) => setAlertOpen(value)} 81 - > 82 - <DropdownMenu> 83 - <DropdownMenuTrigger asChild> 84 - <Button 85 - variant="ghost" 86 - className="data-[state=open]:bg-accent absolute right-6 top-6 h-8 w-8 p-0" 87 - > 88 - <span className="sr-only">Open menu</span> 89 - <MoreVertical className="h-4 w-4" /> 90 - </Button> 91 - </DropdownMenuTrigger> 92 - <DropdownMenuContent align="end"> 93 - <DialogTrigger asChild> 94 - <DropdownMenuItem>Edit</DropdownMenuItem> 95 - </DialogTrigger> 96 - <Link 97 - href={`/app/${props.workspaceSlug}/monitors/${props.id}/data`} 98 - > 99 - <DropdownMenuItem>View data</DropdownMenuItem> 100 - </Link> 101 - <AlertDialogTrigger asChild> 102 - <DropdownMenuItem className="text-destructive focus:bg-destructive focus:text-background"> 103 - Delete 104 - </DropdownMenuItem> 105 - </AlertDialogTrigger> 106 - </DropdownMenuContent> 107 - </DropdownMenu> 108 - <AlertDialogContent> 109 - <AlertDialogHeader> 110 - <AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle> 111 - <AlertDialogDescription> 112 - This action cannot be undone. This will permanently delete the 113 - monitor. 114 - </AlertDialogDescription> 115 - </AlertDialogHeader> 116 - <AlertDialogFooter> 117 - <AlertDialogCancel>Cancel</AlertDialogCancel> 118 - <AlertDialogAction 119 - onClick={(e) => { 120 - e.preventDefault(); 121 - onDelete(); 122 - }} 123 - disabled={isPending} 124 - className="bg-destructive text-destructive-foreground hover:bg-destructive/90" 125 - > 126 - {!isPending ? "Delete" : <LoadingAnimation />} 127 - </AlertDialogAction> 128 - </AlertDialogFooter> 129 - </AlertDialogContent> 130 - </AlertDialog> 131 - <DialogContent className="flex max-h-screen flex-col"> 132 - <DialogHeader> 133 - <DialogTitle>Update Monitor</DialogTitle> 134 - <DialogDescription>Change your settings.</DialogDescription> 135 - </DialogHeader> 136 - <div className="-mx-1 flex-1 overflow-y-auto px-1"> 137 - <MonitorForm 138 - id="monitor-update" 139 - onSubmit={onUpdate} 140 - defaultValues={props} 141 - /> 142 - </div> 143 - <DialogFooter> 49 + <AlertDialog open={alertOpen} onOpenChange={(value) => setAlertOpen(value)}> 50 + <DropdownMenu> 51 + <DropdownMenuTrigger asChild> 144 52 <Button 145 - type="submit" 146 - form="monitor-update" 147 - disabled={isPending} 148 - onSubmit={(e) => { 53 + variant="ghost" 54 + className="data-[state=open]:bg-accent absolute right-6 top-6 h-8 w-8 p-0" 55 + > 56 + <span className="sr-only">Open menu</span> 57 + <MoreVertical className="h-4 w-4" /> 58 + </Button> 59 + </DropdownMenuTrigger> 60 + <DropdownMenuContent align="end"> 61 + <Link href={`./monitors/edit?id=${props.id}`}> 62 + <DropdownMenuItem>Edit</DropdownMenuItem> 63 + </Link> 64 + <Link href={`/app/${props.workspaceSlug}/monitors/${props.id}/data`}> 65 + <DropdownMenuItem>View data</DropdownMenuItem> 66 + </Link> 67 + <AlertDialogTrigger asChild> 68 + <DropdownMenuItem className="text-destructive focus:bg-destructive focus:text-background"> 69 + Delete 70 + </DropdownMenuItem> 71 + </AlertDialogTrigger> 72 + </DropdownMenuContent> 73 + </DropdownMenu> 74 + <AlertDialogContent> 75 + <AlertDialogHeader> 76 + <AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle> 77 + <AlertDialogDescription> 78 + This action cannot be undone. This will permanently delete the 79 + monitor. 80 + </AlertDialogDescription> 81 + </AlertDialogHeader> 82 + <AlertDialogFooter> 83 + <AlertDialogCancel>Cancel</AlertDialogCancel> 84 + <AlertDialogAction 85 + onClick={(e) => { 149 86 e.preventDefault(); 87 + onDelete(); 150 88 }} 89 + disabled={isPending} 90 + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" 151 91 > 152 - {!isPending ? "Confirm" : <LoadingAnimation />} 153 - </Button> 154 - </DialogFooter> 155 - </DialogContent> 156 - </Dialog> 92 + {!isPending ? "Delete" : <LoadingAnimation />} 93 + </AlertDialogAction> 94 + </AlertDialogFooter> 95 + </AlertDialogContent> 96 + </AlertDialog> 157 97 ); 158 98 }
-76
apps/web/src/app/app/(dashboard)/[workspaceSlug]/monitors/_components/create-form.tsx
··· 1 - "use client"; 2 - 3 - import * as React from "react"; 4 - import { useRouter } from "next/navigation"; 5 - import type * as z from "zod"; 6 - 7 - import type { insertMonitorSchema } from "@openstatus/db/src/schema"; 8 - 9 - import { MonitorForm } from "@/components/forms/montitor-form"; 10 - import { LoadingAnimation } from "@/components/loading-animation"; 11 - import { Button } from "@/components/ui/button"; 12 - import { 13 - Dialog, 14 - DialogContent, 15 - DialogDescription, 16 - DialogFooter, 17 - DialogHeader, 18 - DialogTitle, 19 - DialogTrigger, 20 - } from "@/components/ui/dialog"; 21 - import { useToast } from "@/components/ui/use-toast"; 22 - import { api } from "@/trpc/client"; 23 - 24 - type MonitorSchema = z.infer<typeof insertMonitorSchema>; 25 - 26 - interface Props { 27 - workspaceSlug: string; 28 - disabled?: boolean; 29 - } 30 - 31 - export function CreateForm({ workspaceSlug, disabled }: Props) { 32 - const router = useRouter(); 33 - const [open, setOpen] = React.useState(false); 34 - const [isPending, startTransition] = React.useTransition(); 35 - const { toast } = useToast(); 36 - 37 - async function onCreate(values: MonitorSchema) { 38 - startTransition(async () => { 39 - try { 40 - await api.monitor.createMonitor.mutate({ 41 - data: values, 42 - workspaceSlug, 43 - }); 44 - router.refresh(); 45 - setOpen(false); 46 - } catch { 47 - toast({ 48 - title: "Something went wrong.", 49 - description: "If you are in the limits, please try again.", 50 - }); 51 - } 52 - }); 53 - } 54 - 55 - return ( 56 - <Dialog open={open} onOpenChange={(value) => setOpen(value)}> 57 - <DialogTrigger asChild> 58 - <Button disabled={disabled}>Create</Button> 59 - </DialogTrigger> 60 - <DialogContent className="flex max-h-screen flex-col"> 61 - <DialogHeader> 62 - <DialogTitle>Create Monitor</DialogTitle> 63 - <DialogDescription>Choose the settings.</DialogDescription> 64 - </DialogHeader> 65 - <div className="-mx-1 flex-1 overflow-y-auto px-1"> 66 - <MonitorForm id="monitor-create" onSubmit={onCreate} /> 67 - </div> 68 - <DialogFooter> 69 - <Button type="submit" form="monitor-create" disabled={isPending}> 70 - {!isPending ? "Confirm" : <LoadingAnimation />} 71 - </Button> 72 - </DialogFooter> 73 - </DialogContent> 74 - </Dialog> 75 - ); 76 - }
+9 -3
apps/web/src/app/app/(dashboard)/[workspaceSlug]/monitors/_components/empty-state.tsx
··· 1 + import Link from "next/link"; 2 + 1 3 import { EmptyState as DefaultEmptyState } from "@/components/dashboard/empty-state"; 2 - import { CreateForm } from "./create-form"; 4 + import { Button } from "@/components/ui/button"; 3 5 4 - export function EmptyState({ workspaceSlug }: { workspaceSlug: string }) { 6 + export function EmptyState() { 5 7 return ( 6 8 <DefaultEmptyState 7 9 icon="activity" 8 10 title="No monitors" 9 11 description="Create your first monitor" 10 - action={<CreateForm {...{ workspaceSlug }} />} 12 + action={ 13 + <Button asChild> 14 + <Link href="./monitors/edit">Create</Link> 15 + </Button> 16 + } 11 17 /> 12 18 ); 13 19 }
+18
apps/web/src/app/app/(dashboard)/[workspaceSlug]/monitors/edit/loading.tsx
··· 1 + import { Header } from "@/components/dashboard/header"; 2 + import { SkeletonForm } from "@/components/forms/skeleton-form"; 3 + import { Skeleton } from "@/components/ui/skeleton"; 4 + 5 + export default function Loading() { 6 + return ( 7 + <div className="grid gap-6 md:grid-cols-2 md:gap-8"> 8 + <div className="col-span-full flex w-full justify-between"> 9 + <Header.Skeleton> 10 + <Skeleton className="h-9 w-20" /> 11 + </Header.Skeleton> 12 + </div> 13 + <div className="col-span-full"> 14 + <SkeletonForm /> 15 + </div> 16 + </div> 17 + ); 18 + }
+43
apps/web/src/app/app/(dashboard)/[workspaceSlug]/monitors/edit/page.tsx
··· 1 + import { notFound } from "next/navigation"; 2 + import * as z from "zod"; 3 + 4 + import { Header } from "@/components/dashboard/header"; 5 + import { MonitorForm } from "@/components/forms/montitor-form"; 6 + import { api } from "@/trpc/server"; 7 + 8 + /** 9 + * allowed URL search params 10 + */ 11 + const searchParamsSchema = z.object({ 12 + id: z.coerce.number().optional(), 13 + }); 14 + 15 + export default async function EditPage({ 16 + params, 17 + searchParams, 18 + }: { 19 + params: { workspaceSlug: string }; 20 + searchParams: { [key: string]: string | string[] | undefined }; 21 + }) { 22 + const search = searchParamsSchema.safeParse(searchParams); 23 + 24 + if (!search.success) { 25 + return notFound(); 26 + } 27 + 28 + const { id } = search.data; 29 + 30 + const monitor = id && (await api.monitor.getMonitorByID.query({ id })); 31 + 32 + return ( 33 + <div className="grid gap-6 md:grid-cols-2 md:gap-8"> 34 + <Header title="Monitor" description="Upsert your monitor." /> 35 + <div className="col-span-full"> 36 + <MonitorForm 37 + workspaceSlug={params.workspaceSlug} 38 + defaultValues={monitor || undefined} 39 + /> 40 + </div> 41 + </div> 42 + ); 43 + }
+6 -3
apps/web/src/app/app/(dashboard)/[workspaceSlug]/monitors/page.tsx
··· 1 1 import * as React from "react"; 2 + import Link from "next/link"; 2 3 3 4 import { allPlans } from "@openstatus/plans"; 4 5 ··· 6 7 import { Header } from "@/components/dashboard/header"; 7 8 import { Limit } from "@/components/dashboard/limit"; 8 9 import { Badge } from "@/components/ui/badge"; 10 + import { Button } from "@/components/ui/button"; 9 11 import { cn } from "@/lib/utils"; 10 12 import { api } from "@/trpc/server"; 11 13 import { ActionButton } from "./_components/action-button"; 12 - import { CreateForm } from "./_components/create-form"; 13 14 import { EmptyState } from "./_components/empty-state"; 14 15 15 16 const limit = allPlans.free.limits.monitors; ··· 28 29 return ( 29 30 <div className="grid gap-6 md:grid-cols-2 md:gap-8"> 30 31 <Header title="Monitors" description="Overview of all your monitors."> 31 - <CreateForm workspaceSlug={params.workspaceSlug} disabled={isLimit} /> 32 + <Button asChild={!isLimit} disabled={isLimit}> 33 + <Link href="./monitors/edit">Create </Link> 34 + </Button> 32 35 </Header> 33 36 {Boolean(monitors?.length) ? ( 34 37 monitors?.map((monitor, index) => ( ··· 70 73 </Container> 71 74 )) 72 75 ) : ( 73 - <EmptyState workspaceSlug={params.workspaceSlug} /> 76 + <EmptyState /> 74 77 )} 75 78 {isLimit ? <Limit /> : null} 76 79 </div>
+53 -113
apps/web/src/app/app/(dashboard)/[workspaceSlug]/status-pages/_components/action-button.tsx
··· 6 6 import { MoreVertical } from "lucide-react"; 7 7 import type * as z from "zod"; 8 8 9 - import type { 10 - allMonitorsSchema, 11 - insertPageSchemaWithMonitors, 12 - } from "@openstatus/db/src/schema"; 9 + import type { insertPageSchemaWithMonitors } from "@openstatus/db/src/schema"; 13 10 14 - import { StatusPageForm } from "@/components/forms/status-page-form"; 15 11 import { LoadingAnimation } from "@/components/loading-animation"; 16 12 import { 17 13 AlertDialog, ··· 26 22 } from "@/components/ui/alert-dialog"; 27 23 import { Button } from "@/components/ui/button"; 28 24 import { 29 - Dialog, 30 - DialogContent, 31 - DialogDescription, 32 - DialogFooter, 33 - DialogHeader, 34 - DialogTitle, 35 - DialogTrigger, 36 - } from "@/components/ui/dialog"; 37 - import { 38 25 DropdownMenu, 39 26 DropdownMenuContent, 40 27 DropdownMenuItem, ··· 44 31 45 32 type PageSchema = z.infer<typeof insertPageSchemaWithMonitors>; 46 33 47 - // allMonitors 48 34 interface ActionButtonProps { 49 35 page: PageSchema; 50 - allMonitors?: z.infer<typeof allMonitorsSchema>; 51 36 } 52 37 53 - export function ActionButton({ page, allMonitors }: ActionButtonProps) { 38 + export function ActionButton({ page }: ActionButtonProps) { 54 39 const router = useRouter(); 55 - const [dialogOpen, setDialogOpen] = React.useState(false); 56 40 const [alertOpen, setAlertOpen] = React.useState(false); 57 41 const [isPending, startTransition] = React.useTransition(); 58 42 59 - async function onUpdate({ 60 - ...props 61 - }: z.infer<typeof insertPageSchemaWithMonitors>) { 62 - startTransition(async () => { 63 - await api.page.updatePage.mutate(props); 64 - router.refresh(); 65 - setDialogOpen(false); 66 - }); 67 - } 68 - 69 43 async function onDelete() { 70 44 startTransition(async () => { 71 45 if (!page.id) return; ··· 76 50 } 77 51 78 52 return ( 79 - <Dialog open={dialogOpen} onOpenChange={(value) => setDialogOpen(value)}> 80 - <AlertDialog 81 - open={alertOpen} 82 - onOpenChange={(value) => setAlertOpen(value)} 83 - > 84 - <DropdownMenu> 85 - <DropdownMenuTrigger asChild> 86 - <Button 87 - variant="ghost" 88 - className="data-[state=open]:bg-accent absolute right-6 top-6 h-8 w-8 p-0" 89 - > 90 - <span className="sr-only">Open menu</span> 91 - <MoreVertical className="h-4 w-4" /> 92 - </Button> 93 - </DropdownMenuTrigger> 94 - <DropdownMenuContent align="end"> 95 - <DialogTrigger asChild> 96 - <DropdownMenuItem>Edit</DropdownMenuItem> 97 - </DialogTrigger> 98 - <Link 99 - href={ 100 - process.env.NODE_ENV === "production" 101 - ? `https://${page.slug}.openstatus.dev` 102 - : `/status-page/${page.slug}` 103 - } 104 - target="_blank" 105 - > 106 - <DropdownMenuItem> 107 - {/* TODO: forward directly to subdomain */} 108 - View Page 109 - </DropdownMenuItem> 110 - </Link> 111 - <AlertDialogTrigger asChild> 112 - <DropdownMenuItem className="text-destructive focus:bg-destructive focus:text-background"> 113 - Delete 114 - </DropdownMenuItem> 115 - </AlertDialogTrigger> 116 - </DropdownMenuContent> 117 - </DropdownMenu> 118 - <AlertDialogContent> 119 - <AlertDialogHeader> 120 - <AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle> 121 - <AlertDialogDescription> 122 - This action cannot be undone. This will permanently delete the 123 - monitor. 124 - </AlertDialogDescription> 125 - </AlertDialogHeader> 126 - <AlertDialogFooter> 127 - <AlertDialogCancel>Cancel</AlertDialogCancel> 128 - <AlertDialogAction 129 - onClick={(e) => { 130 - e.preventDefault(); 131 - onDelete(); 132 - }} 133 - disabled={isPending} 134 - className="bg-destructive text-destructive-foreground hover:bg-destructive/90" 135 - > 136 - {!isPending ? "Delete" : <LoadingAnimation />} 137 - </AlertDialogAction> 138 - </AlertDialogFooter> 139 - </AlertDialogContent> 140 - </AlertDialog> 141 - <DialogContent className="flex max-h-screen flex-col"> 142 - <DialogHeader> 143 - <DialogTitle>Update Page</DialogTitle> 144 - <DialogDescription>Change your settings.</DialogDescription> 145 - </DialogHeader> 146 - <div className="overflow-y-none -mx-1 flex-1 px-1"> 147 - <StatusPageForm 148 - id="status-page-update" 149 - onSubmit={onUpdate} 150 - defaultValues={page} 151 - allMonitors={allMonitors} 152 - /> 153 - </div> 154 - <DialogFooter> 53 + <AlertDialog open={alertOpen} onOpenChange={(value) => setAlertOpen(value)}> 54 + <DropdownMenu> 55 + <DropdownMenuTrigger asChild> 155 56 <Button 156 - type="submit" 157 - form="status-page-update" 158 - disabled={isPending} 159 - onSubmit={(e) => { 57 + variant="ghost" 58 + className="data-[state=open]:bg-accent absolute right-6 top-6 h-8 w-8 p-0" 59 + > 60 + <span className="sr-only">Open menu</span> 61 + <MoreVertical className="h-4 w-4" /> 62 + </Button> 63 + </DropdownMenuTrigger> 64 + <DropdownMenuContent align="end"> 65 + <Link href={`./status-pages/edit?id=${page.id}`}> 66 + <DropdownMenuItem>Edit</DropdownMenuItem> 67 + </Link> 68 + <Link 69 + href={ 70 + process.env.NODE_ENV === "production" 71 + ? `https://${page.slug}.openstatus.dev` 72 + : `/status-page/${page.slug}` 73 + } 74 + target="_blank" 75 + > 76 + <DropdownMenuItem>Visit</DropdownMenuItem> 77 + </Link> 78 + <AlertDialogTrigger asChild> 79 + <DropdownMenuItem className="text-destructive focus:bg-destructive focus:text-background"> 80 + Delete 81 + </DropdownMenuItem> 82 + </AlertDialogTrigger> 83 + </DropdownMenuContent> 84 + </DropdownMenu> 85 + <AlertDialogContent> 86 + <AlertDialogHeader> 87 + <AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle> 88 + <AlertDialogDescription> 89 + This action cannot be undone. This will permanently delete the 90 + monitor. 91 + </AlertDialogDescription> 92 + </AlertDialogHeader> 93 + <AlertDialogFooter> 94 + <AlertDialogCancel>Cancel</AlertDialogCancel> 95 + <AlertDialogAction 96 + onClick={(e) => { 160 97 e.preventDefault(); 98 + onDelete(); 161 99 }} 100 + disabled={isPending} 101 + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" 162 102 > 163 - {!isPending ? "Confirm" : <LoadingAnimation />} 164 - </Button> 165 - </DialogFooter> 166 - </DialogContent> 167 - </Dialog> 103 + {!isPending ? "Delete" : <LoadingAnimation />} 104 + </AlertDialogAction> 105 + </AlertDialogFooter> 106 + </AlertDialogContent> 107 + </AlertDialog> 168 108 ); 169 109 }
-84
apps/web/src/app/app/(dashboard)/[workspaceSlug]/status-pages/_components/create-form.tsx
··· 1 - "use client"; 2 - 3 - import * as React from "react"; 4 - import { useRouter } from "next/navigation"; 5 - import type * as z from "zod"; 6 - 7 - import type { 8 - allMonitorsSchema, 9 - insertPageSchemaWithMonitors, 10 - } from "@openstatus/db/src/schema"; 11 - 12 - import { StatusPageForm } from "@/components/forms/status-page-form"; 13 - import { LoadingAnimation } from "@/components/loading-animation"; 14 - import { Button } from "@/components/ui/button"; 15 - import { 16 - Dialog, 17 - DialogContent, 18 - DialogDescription, 19 - DialogFooter, 20 - DialogHeader, 21 - DialogTitle, 22 - DialogTrigger, 23 - } from "@/components/ui/dialog"; 24 - import { useToast } from "@/components/ui/use-toast"; 25 - import { api } from "@/trpc/client"; 26 - 27 - interface Props { 28 - workspaceSlug: string; 29 - allMonitors?: z.infer<typeof allMonitorsSchema>; 30 - disabled?: boolean; 31 - } 32 - 33 - export function CreateForm({ workspaceSlug, allMonitors, disabled }: Props) { 34 - const router = useRouter(); 35 - const [open, setOpen] = React.useState(false); 36 - const [isPending, startTransition] = React.useTransition(); 37 - const { toast } = useToast(); 38 - 39 - async function onCreate({ 40 - ...props 41 - }: z.infer<typeof insertPageSchemaWithMonitors>) { 42 - startTransition(async () => { 43 - try { 44 - await api.page.createPage.mutate({ 45 - ...props, 46 - workspaceSlug, 47 - }); 48 - router.refresh(); 49 - setOpen(false); 50 - } catch { 51 - toast({ 52 - title: "Something went wrong.", 53 - description: "If you are in the limits, please try again.", 54 - }); 55 - } 56 - }); 57 - } 58 - 59 - return ( 60 - <Dialog open={open} onOpenChange={(value) => setOpen(value)}> 61 - <DialogTrigger asChild> 62 - <Button disabled={disabled}>Create</Button> 63 - </DialogTrigger> 64 - <DialogContent className="flex max-h-screen flex-col"> 65 - <DialogHeader> 66 - <DialogTitle>Create Status Page</DialogTitle> 67 - <DialogDescription>Choose the settings.</DialogDescription> 68 - </DialogHeader> 69 - <div className="-mx-1 flex-1 overflow-y-auto px-1"> 70 - <StatusPageForm 71 - id="status-page-create" 72 - onSubmit={onCreate} 73 - allMonitors={allMonitors} 74 - /> 75 - </div> 76 - <DialogFooter> 77 - <Button type="submit" form="status-page-create" disabled={isPending}> 78 - {!isPending ? "Confirm" : <LoadingAnimation />} 79 - </Button> 80 - </DialogFooter> 81 - </DialogContent> 82 - </Dialog> 83 - ); 84 - }
+5 -4
apps/web/src/app/app/(dashboard)/[workspaceSlug]/status-pages/_components/empty-state.tsx
··· 5 5 6 6 import { EmptyState as DefaultEmptyState } from "@/components/dashboard/empty-state"; 7 7 import { Button } from "@/components/ui/button"; 8 - import { CreateForm } from "./create-form"; 9 8 10 9 export function EmptyState({ 11 - workspaceId, 12 10 allMonitors, 13 11 }: { 14 - workspaceId: string; 15 12 allMonitors?: z.infer<typeof allMonitorsSchema>; 16 13 }) { 17 14 // Navigate user to monitor if they don't have one ··· 34 31 icon="panel-top" 35 32 title="No pages" 36 33 description="Create your first page." 37 - action={<CreateForm {...{ workspaceSlug: workspaceId, allMonitors }} />} 34 + action={ 35 + <Button asChild> 36 + <Link href="./status-pages/edit">Create</Link> 37 + </Button> 38 + } 38 39 /> 39 40 ); 40 41 }
+18
apps/web/src/app/app/(dashboard)/[workspaceSlug]/status-pages/edit/loading.tsx
··· 1 + import { Header } from "@/components/dashboard/header"; 2 + import { SkeletonForm } from "@/components/forms/skeleton-form"; 3 + import { Skeleton } from "@/components/ui/skeleton"; 4 + 5 + export default function Loading() { 6 + return ( 7 + <div className="grid gap-6 md:grid-cols-2 md:gap-8"> 8 + <div className="col-span-full flex w-full justify-between"> 9 + <Header.Skeleton> 10 + <Skeleton className="h-9 w-20" /> 11 + </Header.Skeleton> 12 + </div> 13 + <div className="col-span-full"> 14 + <SkeletonForm /> 15 + </div> 16 + </div> 17 + ); 18 + }
+58
apps/web/src/app/app/(dashboard)/[workspaceSlug]/status-pages/edit/page.tsx
··· 1 + import { notFound } from "next/navigation"; 2 + import * as z from "zod"; 3 + 4 + import { Header } from "@/components/dashboard/header"; 5 + import { StatusPageForm } from "@/components/forms/status-page-form"; 6 + import { api } from "@/trpc/server"; 7 + 8 + /** 9 + * allowed URL search params 10 + */ 11 + const searchParamsSchema = z.object({ 12 + id: z.coerce.number().optional(), 13 + }); 14 + 15 + export default async function EditPage({ 16 + params, 17 + searchParams, 18 + }: { 19 + params: { workspaceSlug: string }; 20 + searchParams: { [key: string]: string | string[] | undefined }; 21 + }) { 22 + const search = searchParamsSchema.safeParse(searchParams); 23 + 24 + if (!search.success) { 25 + return notFound(); 26 + } 27 + 28 + const { id } = search.data; 29 + 30 + const page = id && (await api.page.getPageByID.query({ id })); 31 + const monitors = await api.monitor.getMonitorsByWorkspace.query({ 32 + workspaceSlug: params.workspaceSlug, 33 + }); 34 + 35 + console.log(monitors, page); 36 + 37 + return ( 38 + <div className="grid gap-6 md:grid-cols-2 md:gap-8"> 39 + <Header title="Status Page" description="Upsert your status page." /> 40 + <div className="col-span-full"> 41 + <StatusPageForm 42 + allMonitors={monitors} 43 + workspaceSlug={params.workspaceSlug} 44 + defaultValues={ 45 + page 46 + ? { 47 + ...page, 48 + monitors: page.monitorsToPages.map( 49 + ({ monitor }) => monitor.id, 50 + ), 51 + } 52 + : undefined 53 + } 54 + /> 55 + </div> 56 + </div> 57 + ); 58 + }
+11 -8
apps/web/src/app/app/(dashboard)/[workspaceSlug]/status-pages/page.tsx
··· 1 1 import * as React from "react"; 2 + import Link from "next/link"; 2 3 3 4 import { allPlans } from "@openstatus/plans"; 4 5 ··· 6 7 import { Header } from "@/components/dashboard/header"; 7 8 import { Limit } from "@/components/dashboard/limit"; 8 9 import { Badge } from "@/components/ui/badge"; 10 + import { Button } from "@/components/ui/button"; 9 11 import { cn } from "@/lib/utils"; 10 12 import { api } from "@/trpc/server"; 11 13 import { ActionButton } from "./_components/action-button"; 12 - import { CreateForm } from "./_components/create-form"; 13 14 import { EmptyState } from "./_components/empty-state"; 14 15 15 16 const limit = allPlans.free.limits["status-pages"]; 16 17 18 + // export const revalidate = 0; 19 + export const dynamic = "force-dynamic"; 20 + 17 21 export default async function Page({ 18 22 params, 19 23 }: { ··· 27 31 }); 28 32 29 33 const isLimit = (pages?.length || 0) >= limit; 34 + const disableButton = isLimit || !Boolean(monitors); 35 + 30 36 return ( 31 37 <div className="grid gap-6 md:grid-cols-2 md:gap-8"> 32 38 <Header 33 39 title="Status Page" 34 40 description="Overview of all your status page." 35 41 > 36 - <CreateForm 37 - workspaceSlug={params.workspaceSlug} 38 - allMonitors={monitors} 39 - disabled={isLimit || !Boolean(monitors)} 40 - /> 42 + <Button asChild={!disableButton} disabled={disableButton}> 43 + <Link href="./status-pages/edit">Create</Link> 44 + </Button> 41 45 </Header> 42 46 {Boolean(pages?.length) ? ( 43 47 pages?.map((page, index) => ( ··· 52 56 workspaceSlug: params.workspaceSlug, 53 57 monitors: page.monitorsToPages.map(({ monitor }) => monitor.id), 54 58 }} 55 - allMonitors={monitors} 56 59 /> 57 60 <dl className="[&_dt]:text-muted-foreground grid gap-2 [&>*]:text-sm [&_dt]:font-light"> 58 61 <div className="flex min-w-0 items-center justify-between gap-3"> ··· 81 84 </Container> 82 85 )) 83 86 ) : ( 84 - <EmptyState workspaceId={params.workspaceSlug} allMonitors={monitors} /> 87 + <EmptyState allMonitors={monitors} /> 85 88 )} 86 89 {isLimit ? <Limit /> : null} 87 90 </div>
+8 -8
apps/web/src/app/layout.tsx
··· 47 47 }) { 48 48 // If you want to develop locally without Clerk, Comment the provider below 49 49 return ( 50 - <ClerkProvider> 51 - <html lang="en"> 52 - {/* TODO: remove plausible from root layout (to avoid tracking subdomains) */} 53 - <PlausibleProvider domain="openstatus.dev"> 50 + <html lang="en"> 51 + {/* TODO: remove plausible from root layout (to avoid tracking subdomains) */} 52 + <PlausibleProvider domain="openstatus.dev"> 53 + <ClerkProvider> 54 54 <body className={`${inter.className} ${calSans.variable}`}> 55 55 <Background>{children}</Background> 56 56 <Toaster /> 57 57 <TailwindIndicator /> 58 58 </body> 59 - </PlausibleProvider> 60 - <ClientAnalytics />; 61 - </html> 62 - </ClerkProvider> 59 + </ClerkProvider> 60 + </PlausibleProvider> 61 + <ClientAnalytics /> 62 + </html> 63 63 ); 64 64 }
+227 -108
apps/web/src/components/forms/montitor-form.tsx
··· 1 1 "use client"; 2 2 3 3 import * as React from "react"; 4 + import { useRouter } from "next/navigation"; 4 5 import { zodResolver } from "@hookform/resolvers/zod"; 6 + import { Check, ChevronsUpDown } from "lucide-react"; 5 7 import { useForm } from "react-hook-form"; 6 8 import type * as z from "zod"; 7 9 ··· 11 13 } from "@openstatus/db/src/schema"; 12 14 import { allPlans } from "@openstatus/plans"; 13 15 16 + import { Button } from "@/components/ui/button"; 17 + import { 18 + Command, 19 + CommandEmpty, 20 + CommandGroup, 21 + CommandInput, 22 + CommandItem, 23 + } from "@/components/ui/command"; 14 24 import { 15 25 Form, 16 26 FormControl, ··· 22 32 } from "@/components/ui/form"; 23 33 import { Input } from "@/components/ui/input"; 24 34 import { 35 + Popover, 36 + PopoverContent, 37 + PopoverTrigger, 38 + } from "@/components/ui/popover"; 39 + import { 25 40 Select, 26 41 SelectContent, 27 42 SelectItem, ··· 29 44 SelectValue, 30 45 } from "@/components/ui/select"; 31 46 import { Switch } from "@/components/ui/switch"; 47 + import { regionsDict } from "@/data/regions-dictionary"; 48 + import { cn } from "@/lib/utils"; 49 + import { api } from "@/trpc/client"; 50 + import { LoadingAnimation } from "../loading-animation"; 51 + import { useToast } from "../ui/use-toast"; 32 52 33 53 const limit = allPlans.free.limits.periodicity; 34 54 const cronJobs = [ ··· 39 59 { value: "1h", label: "1 hour" }, 40 60 ] as const; 41 61 62 + type MonitorProps = z.infer<typeof insertMonitorSchema>; 63 + 42 64 interface Props { 43 - id: string; 44 - defaultValues?: z.infer<typeof insertMonitorSchema>; 45 - onSubmit: (values: z.infer<typeof insertMonitorSchema>) => Promise<void>; 65 + defaultValues?: MonitorProps; 66 + workspaceSlug: string; 46 67 } 47 68 48 - export function MonitorForm({ id, defaultValues, onSubmit }: Props) { 49 - const form = useForm<z.infer<typeof insertMonitorSchema>>({ 69 + export function MonitorForm({ defaultValues, workspaceSlug }: Props) { 70 + const form = useForm<MonitorProps>({ 50 71 resolver: zodResolver(insertMonitorSchema), // too much - we should only validate the values we ask inside of the form! 51 72 defaultValues: { 52 73 url: defaultValues?.url || "", 53 74 name: defaultValues?.name || "", 54 75 description: defaultValues?.description || "", 55 76 periodicity: defaultValues?.periodicity || "30m", 56 - active: defaultValues?.active || true, 77 + active: defaultValues?.active ?? true, 57 78 id: defaultValues?.id || undefined, 79 + regions: defaultValues?.regions || [], 58 80 }, 59 81 }); 82 + const router = useRouter(); 83 + const [isPending, startTransition] = React.useTransition(); 84 + const { toast } = useToast(); 85 + 86 + const onSubmit = ({ ...props }: MonitorProps) => { 87 + startTransition(async () => { 88 + try { 89 + // TODO: we could use an upsertPage function instead - insert if not exist otherwise update 90 + if (defaultValues) { 91 + await api.monitor.updateMonitor.mutate(props); 92 + } else { 93 + await api.monitor.createMonitor.mutate({ 94 + data: props, 95 + workspaceSlug, 96 + }); 97 + } 98 + router.push("./"); 99 + router.refresh(); 100 + } catch { 101 + toast({ 102 + title: "Something went wrong.", 103 + description: "If you are in the limits, please try again.", 104 + }); 105 + } 106 + }); 107 + }; 60 108 61 109 return ( 62 110 <Form {...form}> 63 - <form onSubmit={form.handleSubmit(onSubmit)} id={id}> 64 - <div className="grid w-full items-center space-y-6"> 65 - <FormField 66 - control={form.control} 67 - name="url" 68 - render={({ field }) => ( 69 - <FormItem> 70 - <FormLabel>URL</FormLabel> 111 + <form 112 + onSubmit={form.handleSubmit(onSubmit)} 113 + className="grid w-full grid-cols-1 items-center gap-6 sm:grid-cols-6" 114 + > 115 + <FormField 116 + control={form.control} 117 + name="name" 118 + render={({ field }) => ( 119 + <FormItem className="sm:col-span-3"> 120 + <FormLabel>Name</FormLabel> 121 + <FormControl> 122 + <Input placeholder="" {...field} /> 123 + </FormControl> 124 + <FormDescription> 125 + The name of the monitor displayed on the status page. 126 + </FormDescription> 127 + <FormMessage /> 128 + </FormItem> 129 + )} 130 + /> 131 + <FormField 132 + control={form.control} 133 + name="url" 134 + render={({ field }) => ( 135 + <FormItem className="sm:col-span-4"> 136 + <FormLabel>URL</FormLabel> 137 + <FormControl> 138 + <Input placeholder="" {...field} /> 139 + </FormControl> 140 + <FormDescription> 141 + Here is the URL you want to monitor.{" "} 142 + </FormDescription> 143 + <FormMessage /> 144 + </FormItem> 145 + )} 146 + /> 147 + <FormField 148 + control={form.control} 149 + name="description" 150 + render={({ field }) => ( 151 + <FormItem className="sm:col-span-5"> 152 + <FormLabel>Description</FormLabel> 153 + <FormControl> 154 + <Input placeholder="" {...field} /> 155 + </FormControl> 156 + <FormDescription> 157 + Provide your users with information about it.{" "} 158 + </FormDescription> 159 + <FormMessage /> 160 + </FormItem> 161 + )} 162 + /> 163 + <FormField 164 + control={form.control} 165 + name="periodicity" 166 + render={({ field }) => ( 167 + <FormItem className="sm:col-span-3 sm:self-baseline"> 168 + <FormLabel>Frequency</FormLabel> 169 + <Select 170 + onValueChange={(value) => 171 + field.onChange(periodicityEnum.parse(value)) 172 + } 173 + defaultValue={field.value} 174 + > 71 175 <FormControl> 72 - <Input placeholder="" {...field} /> 176 + <SelectTrigger> 177 + <SelectValue placeholder="How often should it check your endpoint?" /> 178 + </SelectTrigger> 73 179 </FormControl> 74 - <FormDescription> 75 - Here is the URL you want to monitor.{" "} 76 - </FormDescription> 77 - <FormMessage /> 78 - </FormItem> 79 - )} 80 - /> 81 - <FormField 82 - control={form.control} 83 - name="name" 84 - render={({ field }) => ( 85 - <FormItem> 86 - <FormLabel>Name</FormLabel> 87 - <FormControl> 88 - <Input placeholder="" {...field} /> 89 - </FormControl> 90 - <FormDescription> 91 - The name of the monitor displayed on the status page. 92 - </FormDescription> 93 - <FormMessage /> 94 - </FormItem> 95 - )} 96 - /> 97 - <FormField 98 - control={form.control} 99 - name="description" 100 - render={({ field }) => ( 101 - <FormItem> 102 - <FormLabel>Description</FormLabel> 103 - <FormControl> 104 - <Input placeholder="" {...field} /> 105 - </FormControl> 106 - <FormDescription> 107 - Provide your users with information about it.{" "} 108 - </FormDescription> 109 - <FormMessage /> 110 - </FormItem> 111 - )} 112 - /> 113 - <FormField 114 - control={form.control} 115 - name="active" 116 - render={({ field }) => ( 117 - <FormItem className="flex flex-row items-center justify-between"> 118 - <div className="space-y-0.5"> 119 - <FormLabel>Active</FormLabel> 120 - <FormDescription> 121 - This will start ping your endpoint on based on the selected 122 - frequence. 123 - </FormDescription> 124 - </div> 125 - <FormControl> 126 - <Switch 127 - checked={field.value || false} 128 - onCheckedChange={(value) => field.onChange(value)} 129 - /> 130 - </FormControl> 131 - <FormMessage /> 132 - </FormItem> 133 - )} 134 - /> 135 - <FormField 136 - control={form.control} 137 - name="periodicity" 138 - render={({ field }) => ( 139 - <FormItem> 140 - <FormLabel>Frequency</FormLabel> 141 - <Select 142 - onValueChange={(value) => 143 - field.onChange(periodicityEnum.parse(value)) 144 - } 145 - defaultValue={field.value} 146 - > 180 + <SelectContent> 181 + {cronJobs.map(({ label, value }) => ( 182 + <SelectItem 183 + key={value} 184 + value={value} 185 + disabled={!limit.includes(value)} 186 + > 187 + {label} 188 + </SelectItem> 189 + ))} 190 + </SelectContent> 191 + </Select> 192 + <FormDescription> 193 + Frequency of how often your endpoint will be pinged. 194 + </FormDescription> 195 + <FormMessage /> 196 + </FormItem> 197 + )} 198 + /> 199 + <FormField 200 + control={form.control} 201 + name="regions" 202 + render={({ field }) => ( 203 + <FormItem className="sm:col-span-3 sm:self-baseline"> 204 + <FormLabel>Regions</FormLabel> 205 + <Popover> 206 + <PopoverTrigger asChild> 147 207 <FormControl> 148 - <SelectTrigger> 149 - <SelectValue placeholder="How often should it check your endpoint?" /> 150 - </SelectTrigger> 208 + <Button 209 + variant="outline" 210 + role="combobox" 211 + className={cn( 212 + "h-10 w-full justify-between", 213 + !field.value && "text-muted-foreground", 214 + )} 215 + > 216 + {/* This is a hotfix */} 217 + {field.value?.length === 1 && field.value[0].length > 0 218 + ? regionsDict[ 219 + field.value[0] as keyof typeof regionsDict 220 + ].location 221 + : "Select region"} 222 + <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> 223 + </Button> 151 224 </FormControl> 152 - <SelectContent> 153 - {cronJobs.map(({ label, value }) => ( 154 - <SelectItem 155 - key={value} 156 - value={value} 157 - disabled={!limit.includes(value)} 158 - > 159 - {label} 160 - </SelectItem> 161 - ))} 162 - </SelectContent> 163 - </Select> 225 + </PopoverTrigger> 226 + <PopoverContent className="w-full p-0"> 227 + <Command> 228 + <CommandInput placeholder="Select a region..." /> 229 + <CommandEmpty>No regions found.</CommandEmpty> 230 + <CommandGroup className="max-h-[150px] overflow-y-scroll"> 231 + {Object.keys(regionsDict).map((region) => { 232 + const { code, location } = 233 + regionsDict[region as keyof typeof regionsDict]; 234 + const isSelected = field.value?.includes(code); 235 + return ( 236 + <CommandItem 237 + value={code} 238 + key={code} 239 + onSelect={() => { 240 + form.setValue("regions", [code]); // TODO: allow more than one to be selected in the future 241 + }} 242 + > 243 + <Check 244 + className={cn( 245 + "mr-2 h-4 w-4", 246 + isSelected ? "opacity-100" : "opacity-0", 247 + )} 248 + /> 249 + {location} 250 + </CommandItem> 251 + ); 252 + })} 253 + </CommandGroup> 254 + </Command> 255 + </PopoverContent> 256 + </Popover> 257 + <FormDescription> 258 + Select your region. Leave blank for random picked regions. 259 + </FormDescription> 260 + <FormMessage /> 261 + </FormItem> 262 + )} 263 + /> 264 + <FormField 265 + control={form.control} 266 + name="active" 267 + render={({ field }) => ( 268 + <FormItem className="flex flex-row items-center justify-between sm:col-span-3"> 269 + <div className="space-y-0.5"> 270 + <FormLabel>Active</FormLabel> 164 271 <FormDescription> 165 - Frequency of how often your endpoint will be pinged. 272 + This will start ping your endpoint on based on the selected 273 + frequence. 166 274 </FormDescription> 167 - <FormMessage /> 168 - </FormItem> 169 - )} 170 - /> 275 + </div> 276 + <FormControl> 277 + <Switch 278 + checked={field.value || false} 279 + onCheckedChange={(value) => field.onChange(value)} 280 + /> 281 + </FormControl> 282 + <FormMessage /> 283 + </FormItem> 284 + )} 285 + /> 286 + <div className="sm:col-span-full"> 287 + <Button className="w-full sm:w-auto"> 288 + {!isPending ? "Confirm" : <LoadingAnimation />} 289 + </Button> 171 290 </div> 172 291 </form> 173 292 </Form>
+26
apps/web/src/components/forms/skeleton-form.tsx
··· 1 + import { Skeleton } from "@/components/ui/skeleton"; 2 + 3 + export function SkeletonForm() { 4 + return ( 5 + <div className="grid w-full grid-cols-1 items-center space-y-6 sm:grid-cols-6"> 6 + <div className="space-y-2 sm:col-span-4"> 7 + <Skeleton className="h-6 w-24" /> 8 + <Skeleton className="h-9 w-full" /> 9 + <Skeleton className="h-5 w-64" /> 10 + </div> 11 + <div className="space-y-2 sm:col-span-5"> 12 + <Skeleton className="h-6 w-24" /> 13 + <Skeleton className="h-9 w-full" /> 14 + <Skeleton className="h-5 w-64" /> 15 + </div> 16 + <div className="space-y-2 sm:col-span-3"> 17 + <Skeleton className="h-6 w-24" /> 18 + <Skeleton className="h-9 w-full" /> 19 + <Skeleton className="h-5 w-64" /> 20 + </div> 21 + <div className="space-y-2 sm:col-span-full"> 22 + <Skeleton className="h-9 w-20" /> 23 + </div> 24 + </div> 25 + ); 26 + }
+137 -101
apps/web/src/components/forms/status-page-form.tsx
··· 1 1 "use client"; 2 2 3 3 import * as React from "react"; 4 + import { useRouter } from "next/navigation"; 4 5 import { zodResolver } from "@hookform/resolvers/zod"; 5 6 import { useForm } from "react-hook-form"; 6 7 import type * as z from "zod"; ··· 8 9 import type { allMonitorsSchema } from "@openstatus/db/src/schema"; 9 10 import { insertPageSchemaWithMonitors } from "@openstatus/db/src/schema"; 10 11 12 + import { Button } from "@/components/ui/button"; 13 + import { Checkbox } from "@/components/ui/checkbox"; 11 14 import { 12 15 Form, 13 16 FormControl, ··· 18 21 FormMessage, 19 22 } from "@/components/ui/form"; 20 23 import { Input } from "@/components/ui/input"; 24 + import { useToast } from "@/components/ui/use-toast"; 21 25 import { useDebounce } from "@/hooks/use-debounce"; 22 26 import { api } from "@/trpc/client"; 23 - import { Checkbox } from "../ui/checkbox"; 24 - import { useToast } from "../ui/use-toast"; 27 + import { LoadingAnimation } from "../loading-animation"; 25 28 26 29 // REMINDER: only use the props you need! 27 30 28 31 type Schema = z.infer<typeof insertPageSchemaWithMonitors>; 29 32 30 33 interface Props { 31 - id: string; 32 34 defaultValues?: Schema; 33 - onSubmit: (values: Schema) => Promise<void>; 35 + workspaceSlug: string; 34 36 allMonitors?: z.infer<typeof allMonitorsSchema>; 35 37 } 36 38 37 39 export function StatusPageForm({ 38 - id, 39 40 defaultValues, 40 - onSubmit, 41 + workspaceSlug, 41 42 allMonitors, 42 43 }: Props) { 43 44 const form = useForm<Schema>({ ··· 52 53 workspaceSlug: "", 53 54 }, 54 55 }); 56 + const router = useRouter(); 57 + const [isPending, startTransition] = React.useTransition(); 55 58 const watchSlug = form.watch("slug"); 56 59 const debouncedSlug = useDebounce(watchSlug, 1000); // using debounce to not exhaust the server 57 60 const { toast } = useToast(); ··· 78 81 // eslint-disable-next-line react-hooks/exhaustive-deps 79 82 }, [checkUniqueSlug]); 80 83 84 + const onSubmit = async ({ 85 + ...props 86 + }: z.infer<typeof insertPageSchemaWithMonitors>) => { 87 + startTransition(async () => { 88 + // TODO: we could use an upsertPage function instead - insert if not exist otherwise update 89 + try { 90 + if (defaultValues) { 91 + await api.page.updatePage.mutate(props); 92 + } else { 93 + await api.page.createPage.mutate({ 94 + ...props, 95 + workspaceSlug, 96 + }); 97 + } 98 + router.push("./"); 99 + router.refresh(); // this will actually revalidate the page after submission 100 + } catch { 101 + toast({ 102 + title: "Something went wrong.", 103 + description: "If you are in the limits, please try again.", 104 + }); 105 + } 106 + }); 107 + }; 108 + 81 109 return ( 82 110 <Form {...form}> 83 111 <form ··· 91 119 description: "Please select another slug. Every slug is unique.", 92 120 }); 93 121 } else { 94 - form.handleSubmit(onSubmit)(e); 122 + if (onSubmit) { 123 + form.handleSubmit(onSubmit)(e); 124 + } 95 125 } 96 126 }} 97 - id={id} 127 + className="grid w-full grid-cols-1 items-center gap-6 sm:grid-cols-6" 98 128 > 99 - <div className="grid w-full items-center space-y-6"> 100 - <FormField 101 - control={form.control} 102 - name="title" 103 - render={({ field }) => ( 104 - <FormItem> 105 - <FormLabel>Title</FormLabel> 106 - <FormControl> 107 - <Input placeholder="" {...field} /> 108 - </FormControl> 109 - <FormDescription>The title of your page.</FormDescription> 110 - <FormMessage /> 111 - </FormItem> 112 - )} 113 - /> 114 - <FormField 115 - control={form.control} 116 - name="slug" 117 - render={({ field }) => ( 118 - <FormItem> 119 - <FormLabel>Slug</FormLabel> 120 - <FormControl> 121 - <Input placeholder="" {...field} /> 122 - </FormControl> 123 - <FormDescription> 124 - The subdomain slug for your status page. At least 3 chars. 125 - </FormDescription> 126 - <FormMessage /> 127 - </FormItem> 128 - )} 129 - /> 130 - <FormField 131 - control={form.control} 132 - name="description" 133 - render={({ field }) => ( 134 - <FormItem> 135 - <FormLabel>Description</FormLabel> 136 - <FormControl> 137 - <Input placeholder="" {...field} /> 138 - </FormControl> 129 + <FormField 130 + control={form.control} 131 + name="title" 132 + render={({ field }) => ( 133 + <FormItem className="sm:col-span-4"> 134 + <FormLabel>Title</FormLabel> 135 + <FormControl> 136 + <Input placeholder="" {...field} /> 137 + </FormControl> 138 + <FormDescription>The title of your page.</FormDescription> 139 + <FormMessage /> 140 + </FormItem> 141 + )} 142 + /> 143 + <FormField 144 + control={form.control} 145 + name="description" 146 + render={({ field }) => ( 147 + <FormItem className="sm:col-span-5"> 148 + <FormLabel>Description</FormLabel> 149 + <FormControl> 150 + <Input placeholder="" {...field} /> 151 + </FormControl> 152 + <FormDescription> 153 + Give your user some information about it. 154 + </FormDescription> 155 + <FormMessage /> 156 + </FormItem> 157 + )} 158 + /> 159 + <FormField 160 + control={form.control} 161 + name="slug" 162 + render={({ field }) => ( 163 + <FormItem className="sm:col-span-3"> 164 + <FormLabel>Slug</FormLabel> 165 + <FormControl> 166 + <Input placeholder="" {...field} /> 167 + </FormControl> 168 + <FormDescription> 169 + The subdomain for your status page. At least 3 chars. 170 + </FormDescription> 171 + <FormMessage /> 172 + </FormItem> 173 + )} 174 + /> 175 + <FormField 176 + control={form.control} 177 + name="monitors" 178 + render={() => ( 179 + <FormItem className="sm:col-span-full"> 180 + <div className="mb-4"> 181 + <FormLabel className="text-base">Monitor</FormLabel> 139 182 <FormDescription> 140 - Give your user some information about it. 183 + Select the monitors you want to display. 141 184 </FormDescription> 142 - <FormMessage /> 143 - </FormItem> 144 - )} 145 - /> 146 - <FormField 147 - control={form.control} 148 - name="monitors" 149 - render={() => ( 150 - <FormItem> 151 - <div className="mb-4"> 152 - <FormLabel className="text-base">Monitor</FormLabel> 153 - <FormDescription> 154 - Select the monitors you want to display. 155 - </FormDescription> 156 - </div> 157 - {allMonitors?.map((item) => ( 158 - <FormField 159 - key={item.id} 160 - control={form.control} 161 - name="monitors" 162 - render={({ field }) => { 163 - return ( 164 - <FormItem 165 - key={item.id} 166 - className="flex flex-row items-start space-x-3 space-y-0" 167 - > 168 - <FormControl> 169 - <Checkbox 170 - checked={field.value?.includes(item.id)} 171 - onCheckedChange={(checked) => { 172 - return checked 173 - ? field.onChange([ 174 - ...(field.value || []), 175 - item.id, 176 - ]) 177 - : field.onChange( 178 - field.value?.filter( 179 - (value) => value !== item.id, 180 - ), 181 - ); 182 - }} 183 - /> 184 - </FormControl> 185 + </div> 186 + {allMonitors?.map((item) => ( 187 + <FormField 188 + key={item.id} 189 + control={form.control} 190 + name="monitors" 191 + render={({ field }) => { 192 + return ( 193 + <FormItem 194 + key={item.id} 195 + className="flex flex-row items-start space-x-3 space-y-0" 196 + > 197 + <FormControl> 198 + <Checkbox 199 + checked={field.value?.includes(item.id)} 200 + onCheckedChange={(checked) => { 201 + return checked 202 + ? field.onChange([ 203 + ...(field.value || []), 204 + item.id, 205 + ]) 206 + : field.onChange( 207 + field.value?.filter( 208 + (value) => value !== item.id, 209 + ), 210 + ); 211 + }} 212 + /> 213 + </FormControl> 214 + <div className="space-y-1 leading-none"> 185 215 <FormLabel className="font-normal"> 186 216 {item.name} 187 217 </FormLabel> 188 - </FormItem> 189 - ); 190 - }} 191 - /> 192 - ))} 193 - <FormMessage /> 194 - </FormItem> 195 - )} 196 - /> 218 + <FormDescription>{item.description}</FormDescription> 219 + </div> 220 + </FormItem> 221 + ); 222 + }} 223 + /> 224 + ))} 225 + <FormMessage /> 226 + </FormItem> 227 + )} 228 + /> 229 + <div className="sm:col-span-full"> 230 + <Button className="w-full sm:w-auto"> 231 + {!isPending ? "Confirm" : <LoadingAnimation />} 232 + </Button> 197 233 </div> 198 234 </form> 199 235 </Form>
+3 -2
packages/api/src/router/monitor.ts
··· 7 7 allMonitorsSchema, 8 8 insertMonitorSchema, 9 9 monitor, 10 + selectMonitorSchema, 10 11 user, 11 12 usersToWorkspaces, 12 13 workspace, ··· 110 111 .get(); 111 112 112 113 if (!result || !result.users_to_workspaces) return; 113 - 114 - return currentMonitor; 114 + const _monitor = selectMonitorSchema.parse(currentMonitor); 115 + return _monitor; 115 116 }), 116 117 117 118 updateMonitorDescription: protectedProcedure