Openstatus www.openstatus.dev
6
fork

Configure Feed

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

wip: lots of stuff (#71)

authored by

Maximilian Kaske and committed by
GitHub
af407f4d e43a30b0

+1100 -233
+4
apps/web/package.json
··· 22 22 "@radix-ui/react-dropdown-menu": "^2.0.5", 23 23 "@radix-ui/react-hover-card": "^1.0.6", 24 24 "@radix-ui/react-label": "^2.0.2", 25 + "@radix-ui/react-popover": "^1.0.6", 25 26 "@radix-ui/react-select": "^1.2.2", 27 + "@radix-ui/react-separator": "^1.0.3", 26 28 "@radix-ui/react-slot": "^1.0.2", 29 + "@radix-ui/react-switch": "^1.0.3", 27 30 "@radix-ui/react-toast": "^1.1.4", 28 31 "@radix-ui/react-tooltip": "^1.0.6", 29 32 "@t3-oss/env-nextjs": "0.4.1", ··· 43 46 "next": "13.4.8", 44 47 "next-plausible": "3.7.2", 45 48 "react": "18.2.0", 49 + "react-day-picker": "^8.8.0", 46 50 "react-dom": "18.2.0", 47 51 "react-hook-form": "^7.45.1", 48 52 "resend": "^0.15.3",
+23
apps/web/src/app/app/(dashboard)/[workspaceId]/dashboard/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 + import Loading from "../loading"; 7 + 8 + export default async function DashboardPage() { 9 + const workspace = await api.workspace.getUserWithWorkspace.query(); 10 + if (!workspace) { 11 + return <Loading />; 12 + } 13 + 14 + return ( 15 + <div className="grid gap-6 md:grid-cols-2 md:gap-8"> 16 + <div className="col-span-full flex w-full justify-between"> 17 + <Header title="Dashboard" description="Overview of all your websites" /> 18 + </div> 19 + <Container title="Hello"></Container> 20 + <Container title="World"></Container> 21 + </div> 22 + ); 23 + }
-13
apps/web/src/app/app/(dashboard)/[workspaceId]/incidents/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 - }
+18
apps/web/src/app/app/(dashboard)/[workspaceId]/monitors/[id]/data/loading.tsx
··· 1 + import { Container } from "@/components/dashboard/container"; 2 + import { Header } from "@/components/dashboard/header"; 3 + import { Skeleton } from "@/components/ui/skeleton"; 4 + 5 + export default function Loading() { 6 + return ( 7 + <div className="grid gap-6 md:gap-8"> 8 + <Header.Skeleton /> 9 + <div className="grid gap-3"> 10 + <div className="flex items-center justify-between gap-3"> 11 + <Skeleton className="h-8 w-64" /> 12 + <Skeleton className="h-8 w-64" /> 13 + </div> 14 + <Container.Skeleton /> 15 + </div> 16 + </div> 17 + ); 18 + }
+54
apps/web/src/app/app/(dashboard)/[workspaceId]/monitors/[id]/data/page.tsx
··· 1 + import * as React from "react"; 2 + import { notFound } from "next/navigation"; 3 + import * as z from "zod"; 4 + 5 + import { availableRegions } from "@openstatus/tinybird"; 6 + 7 + import { Header } from "@/components/dashboard/header"; 8 + import { columns } from "@/components/data-table/columns"; 9 + import { DataTable } from "@/components/data-table/data-table"; 10 + import { getResponseListData } from "@/lib/tb"; 11 + import { api } from "@/trpc/server"; 12 + 13 + /** 14 + * allowed URL search params 15 + */ 16 + const searchParamsSchema = z.object({ 17 + statusCode: z.coerce.number().optional(), 18 + region: z.enum(availableRegions).optional(), 19 + cronTimestamp: z.coerce.number().optional(), 20 + fromDate: z.coerce.number().optional(), 21 + toDate: z.coerce.number().optional(), 22 + }); 23 + 24 + export default async function Page({ 25 + params, 26 + searchParams, 27 + }: { 28 + params: { workspaceId: string; id: string }; 29 + searchParams: { [key: string]: string | string[] | undefined }; 30 + }) { 31 + const workspaceId = Number(params.workspaceId); 32 + const id = Number(params.id); 33 + const search = searchParamsSchema.safeParse(searchParams); 34 + 35 + const monitor = await api.monitor.getMonitorById.query({ 36 + id, 37 + }); 38 + 39 + if (!monitor || !search.success) { 40 + return notFound(); 41 + } 42 + 43 + const data = await getResponseListData({ 44 + siteId: "openstatus", // TODO: use monitorId 45 + ...search.data, 46 + }); 47 + 48 + return ( 49 + <div className="grid gap-6 md:gap-8"> 50 + <Header title={monitor.name} description={monitor.description}></Header> 51 + {data && <DataTable columns={columns} data={data} />} 52 + </div> 53 + ); 54 + }
+11 -2
apps/web/src/app/app/(dashboard)/[workspaceId]/monitors/_components/action-button.tsx
··· 1 1 "use client"; 2 2 3 3 import * as React from "react"; 4 - import { useRouter } from "next/navigation"; 4 + import Link from "next/link"; 5 + import { useParams, usePathname, useRouter } from "next/navigation"; 5 6 import { MoreVertical } from "lucide-react"; 6 7 import type * as z from "zod"; 7 8 ··· 43 44 44 45 export function ActionButton(props: Schema) { 45 46 const router = useRouter(); 47 + const pathname = usePathname(); 46 48 const [dialogOpen, setDialogOpen] = React.useState(false); 47 49 const [alertOpen, setAlertOpen] = React.useState(false); 48 50 const [saving, setSaving] = React.useState(false); ··· 73 75 <DropdownMenuTrigger asChild> 74 76 <Button 75 77 variant="ghost" 76 - className="absolute right-6 top-6 h-8 w-8 p-0" 78 + className="data-[state=open]:bg-accent absolute right-6 top-6 h-8 w-8 p-0" 77 79 > 78 80 <span className="sr-only">Open menu</span> 79 81 <MoreVertical className="h-4 w-4" /> ··· 83 85 <DialogTrigger asChild> 84 86 <DropdownMenuItem>Edit</DropdownMenuItem> 85 87 </DialogTrigger> 88 + <DropdownMenuItem asChild> 89 + <Link 90 + href={`/app/${props.workspaceId}/monitors/${props.id}/data`} 91 + > 92 + View data 93 + </Link> 94 + </DropdownMenuItem> 86 95 <AlertDialogTrigger asChild> 87 96 <DropdownMenuItem className="text-destructive focus:bg-destructive focus:text-background"> 88 97 Delete
-18
apps/web/src/app/app/(dashboard)/[workspaceId]/monitors/loading.tsx
··· 1 - import { Container } from "@/components/dashboard/container"; 2 - import { Header } from "@/components/dashboard/header"; 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 - <Container.Skeleton /> 14 - <Container.Skeleton /> 15 - <Container.Skeleton /> 16 - </div> 17 - ); 18 - }
+38 -1
apps/web/src/app/app/(dashboard)/[workspaceId]/monitors/page.tsx
··· 2 2 3 3 import { Container } from "@/components/dashboard/container"; 4 4 import { Header } from "@/components/dashboard/header"; 5 + import { Badge } from "@/components/ui/badge"; 6 + import { cn } from "@/lib/utils"; 5 7 import { api } from "@/trpc/server"; 6 8 import { ActionButton } from "./_components/action-button"; 7 9 import { CreateForm } from "./_components/create-form"; ··· 22 24 <CreateForm {...{ workspaceId }} /> 23 25 </Header> 24 26 {monitors.map((monitor, index) => ( 25 - <Container key={index} title={monitor.name} description={monitor.url}> 27 + <Container 28 + key={index} 29 + title={monitor.name} 30 + description={monitor.description} 31 + > 26 32 <ActionButton {...monitor} /> 33 + <dl className="[&_dt]:text-muted-foreground grid gap-2 [&>*]:text-sm [&_dt]:font-light"> 34 + <div className="flex min-w-0 items-center justify-between gap-3"> 35 + <dt>Status</dt> 36 + <dd> 37 + <Badge 38 + variant={monitor.status === "active" ? "default" : "outline"} 39 + className="capitalize" 40 + > 41 + {monitor.status} 42 + <span 43 + className={cn( 44 + "ml-1 h-1.5 w-1.5 rounded-full", 45 + monitor.status === "active" 46 + ? "bg-green-500" 47 + : "bg-red-500", 48 + )} 49 + /> 50 + </Badge> 51 + </dd> 52 + </div> 53 + <div className="flex min-w-0 items-center justify-between gap-3"> 54 + <dt>Periodicity</dt> 55 + <dd className="font-mono">{monitor.periodicity}</dd> 56 + </div> 57 + <div className="flex min-w-0 items-center justify-between gap-3"> 58 + <dt>URL</dt> 59 + <dd className="overflow-hidden text-ellipsis font-semibold"> 60 + {monitor.url} 61 + </dd> 62 + </div> 63 + </dl> 27 64 </Container> 28 65 ))} 29 66 </div>
+7 -22
apps/web/src/app/app/(dashboard)/[workspaceId]/page.tsx
··· 1 - import * as React from "react"; 1 + import { redirect } from "next/navigation"; 2 2 3 - import { Container } from "@/components/dashboard/container"; 4 - import { Header } from "@/components/dashboard/header"; 5 - import { StatusPageCreateForm } from "@/components/forms/status-page-form"; 6 - import { api } from "@/trpc/server"; 7 - import Loading from "./loading"; 8 - 9 - export default async function DashboardPage() { 10 - const workspace = await api.workspace.getUserWithWorkspace.query(); 11 - if (!workspace) { 12 - return <Loading />; 13 - } 14 - 15 - return ( 16 - <div className="grid gap-6 md:grid-cols-2 md:gap-8"> 17 - <div className="col-span-full flex w-full justify-between"> 18 - <Header title="Dashboard" description="Overview of all your websites" /> 19 - </div> 20 - <Container title="Hello"></Container> 21 - <Container title="World"></Container> 22 - </div> 23 - ); 3 + export default function DiscordRedirect({ 4 + params, 5 + }: { 6 + params: { workspaceId: string }; 7 + }) { 8 + return redirect(`/app/${params.workspaceId}/dashboard`); 24 9 }
+144
apps/web/src/app/app/(dashboard)/[workspaceId]/status-pages/_components/action-button.tsx
··· 1 + "use client"; 2 + 3 + import * as React from "react"; 4 + import Link from "next/link"; 5 + import { useParams, usePathname, useRouter } from "next/navigation"; 6 + import { MoreVertical } from "lucide-react"; 7 + import type * as z from "zod"; 8 + 9 + import type { insertPageSchema } from "@openstatus/db/src/schema"; 10 + 11 + import { StatusPageForm } from "@/components/forms/status-page-form"; 12 + import { LoadingAnimation } from "@/components/loading-animation"; 13 + import { 14 + AlertDialog, 15 + AlertDialogAction, 16 + AlertDialogCancel, 17 + AlertDialogContent, 18 + AlertDialogDescription, 19 + AlertDialogFooter, 20 + AlertDialogHeader, 21 + AlertDialogTitle, 22 + AlertDialogTrigger, 23 + } from "@/components/ui/alert-dialog"; 24 + 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 + import { 35 + DropdownMenu, 36 + DropdownMenuContent, 37 + DropdownMenuItem, 38 + DropdownMenuTrigger, 39 + } from "@/components/ui/dropdown-menu"; 40 + import { wait } from "@/lib/utils"; 41 + import { api } from "@/trpc/client"; 42 + 43 + type Schema = z.infer<typeof insertPageSchema>; 44 + 45 + export function ActionButton(props: Schema) { 46 + const router = useRouter(); 47 + const [dialogOpen, setDialogOpen] = React.useState(false); 48 + const [alertOpen, setAlertOpen] = React.useState(false); 49 + const [saving, setSaving] = React.useState(false); 50 + 51 + async function onUpdate(values: Schema) { 52 + setSaving(true); 53 + // await api.monitor.updateMonitor.mutate({ id: props.id, ...values }); 54 + await wait(1000); 55 + router.refresh(); 56 + setSaving(false); 57 + setDialogOpen(false); 58 + } 59 + 60 + async function onDelete() { 61 + setSaving(true); 62 + // await api.monitor.deleteMonitor.mutate({ monitorId: Number(props.id) }); 63 + await wait(1000); 64 + router.refresh(); 65 + setSaving(false); 66 + setAlertOpen(false); 67 + } 68 + 69 + return ( 70 + <Dialog open={dialogOpen} onOpenChange={(value) => setDialogOpen(value)}> 71 + <AlertDialog 72 + open={alertOpen} 73 + onOpenChange={(value) => setAlertOpen(value)} 74 + > 75 + <DropdownMenu> 76 + <DropdownMenuTrigger asChild> 77 + <Button 78 + variant="ghost" 79 + className="data-[state=open]:bg-accent absolute right-6 top-6 h-8 w-8 p-0" 80 + > 81 + <span className="sr-only">Open menu</span> 82 + <MoreVertical className="h-4 w-4" /> 83 + </Button> 84 + </DropdownMenuTrigger> 85 + <DropdownMenuContent align="end"> 86 + <DialogTrigger asChild> 87 + <DropdownMenuItem>Edit</DropdownMenuItem> 88 + </DialogTrigger> 89 + <AlertDialogTrigger asChild> 90 + <DropdownMenuItem className="text-destructive focus:bg-destructive focus:text-background"> 91 + Delete 92 + </DropdownMenuItem> 93 + </AlertDialogTrigger> 94 + </DropdownMenuContent> 95 + </DropdownMenu> 96 + <AlertDialogContent> 97 + <AlertDialogHeader> 98 + <AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle> 99 + <AlertDialogDescription> 100 + This action cannot be undone. This will permanently delete the 101 + monitor. 102 + </AlertDialogDescription> 103 + </AlertDialogHeader> 104 + <AlertDialogFooter> 105 + <AlertDialogCancel>Cancel</AlertDialogCancel> 106 + <AlertDialogAction 107 + onClick={(e) => { 108 + e.preventDefault(); 109 + onDelete(); 110 + }} 111 + disabled={saving} 112 + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" 113 + > 114 + {!saving ? "Delete" : <LoadingAnimation />} 115 + </AlertDialogAction> 116 + </AlertDialogFooter> 117 + </AlertDialogContent> 118 + </AlertDialog> 119 + <DialogContent> 120 + <DialogHeader> 121 + <DialogTitle>Update Page</DialogTitle> 122 + <DialogDescription>Change your settings.</DialogDescription> 123 + </DialogHeader> 124 + <StatusPageForm 125 + id="status-page-update" 126 + onSubmit={onUpdate} 127 + defaultValues={props} 128 + /> 129 + <DialogFooter> 130 + <Button 131 + type="submit" 132 + form="status-page-update" 133 + disabled={saving} 134 + onSubmit={(e) => { 135 + e.preventDefault(); 136 + }} 137 + > 138 + {!saving ? "Confirm" : <LoadingAnimation />} 139 + </Button> 140 + </DialogFooter> 141 + </DialogContent> 142 + </Dialog> 143 + ); 144 + }
+60
apps/web/src/app/app/(dashboard)/[workspaceId]/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 { insertPageSchema } from "@openstatus/db/src/schema"; 8 + 9 + import { StatusPageForm } from "@/components/forms/status-page-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 { api } from "@/trpc/client"; 22 + 23 + interface Props { 24 + workspaceId: number; 25 + } 26 + 27 + export function CreateForm({ workspaceId }: Props) { 28 + const router = useRouter(); 29 + const [open, setOpen] = React.useState(false); 30 + const [saving, setSaving] = React.useState(false); 31 + 32 + async function onCreate(values: z.infer<typeof insertPageSchema>) { 33 + setSaving(true); 34 + // await api.monitor.getMonitorsByWorkspace.revalidate(); 35 + await api.page.createPage.mutate({ ...values, workspaceId }); 36 + router.refresh(); 37 + setSaving(false); 38 + setOpen(false); 39 + } 40 + 41 + return ( 42 + <Dialog open={open} onOpenChange={(value) => setOpen(value)}> 43 + <DialogTrigger asChild> 44 + <Button>Create</Button> 45 + </DialogTrigger> 46 + <DialogContent> 47 + <DialogHeader> 48 + <DialogTitle>Create Status Page</DialogTitle> 49 + <DialogDescription>Choose the settings.</DialogDescription> 50 + </DialogHeader> 51 + <StatusPageForm id="status-page-create" onSubmit={onCreate} /> 52 + <DialogFooter> 53 + <Button type="submit" form="status-page-create" disabled={saving}> 54 + {!saving ? "Confirm" : <LoadingAnimation />} 55 + </Button> 56 + </DialogFooter> 57 + </DialogContent> 58 + </Dialog> 59 + ); 60 + }
-18
apps/web/src/app/app/(dashboard)/[workspaceId]/status-pages/loading.tsx
··· 1 - import { Container } from "@/components/dashboard/container"; 2 - import { Header } from "@/components/dashboard/header"; 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 - <Container.Skeleton /> 14 - <Container.Skeleton /> 15 - <Container.Skeleton /> 16 - </div> 17 - ); 18 - }
+18 -4
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 { StatusPageCreateForm } from "@/components/forms/status-page-form"; 6 5 import { api } from "@/trpc/server"; 6 + import { ActionButton } from "./_components/action-button"; 7 + import { CreateForm } from "./_components/create-form"; 7 8 8 9 export default async function Page({ 9 10 params, 10 11 }: { 11 12 params: { workspaceId: string }; 12 13 }) { 14 + const workspaceId = Number(params.workspaceId); 13 15 const pages = await api.page.getPageByWorkspace.query({ 14 - workspaceId: Number(params.workspaceId), 16 + workspaceId, 15 17 }); 16 18 // iterate over pages 17 19 return ( ··· 20 22 title="Status Page" 21 23 description="Overview of all your status page." 22 24 > 23 - <StatusPageCreateForm /> 25 + <CreateForm {...{ workspaceId }} /> 24 26 </Header> 25 27 {pages.map((page, index) => ( 26 - <Container key={index} title={page.title}></Container> 28 + <Container 29 + key={index} 30 + title={page.title} 31 + description={page.description} 32 + > 33 + <ActionButton {...page} /> 34 + <dl className="[&_dt]:text-muted-foreground grid gap-2 [&>*]:text-sm [&_dt]:font-light"> 35 + <div className="flex min-w-0 items-center justify-between gap-3"> 36 + <dt>Slug</dt> 37 + <dd className="font-mono">{page.slug}</dd> 38 + </div> 39 + </dl> 40 + </Container> 27 41 ))} 28 42 </div> 29 43 );
+5 -7
apps/web/src/app/app/(dashboard)/layout.tsx
··· 8 8 9 9 export default function AppLayout({ children }: { children: React.ReactNode }) { 10 10 return ( 11 - <div className="container mx-auto flex min-h-screen w-full flex-col items-center justify-center space-y-6 p-4 md:p-8"> 11 + <div className="container relative mx-auto flex min-h-screen w-full flex-col items-center justify-center gap-6 p-4 lg:p-8"> 12 12 <AppHeader /> 13 - <div className="flex w-full flex-1 gap-6 md:gap-8"> 14 - <Shell className="hidden max-w-min md:block"> 15 - <nav> 16 - <AppSidebar /> 17 - </nav> 13 + <div className="flex w-full flex-1 gap-6 lg:gap-8"> 14 + <Shell className="hidden max-h-[calc(100vh-9rem)] max-w-min shrink-0 lg:sticky lg:top-20 lg:block"> 15 + <AppSidebar /> 18 16 </Shell> 19 17 <main className="z-10 flex w-full flex-1 flex-col items-start justify-center"> 20 18 <Shell className="relative flex-1"> 21 19 {/* The `top-4` is represented in Shell with a `py-4` class */} 22 - <nav className="absolute right-4 top-4 block md:hidden"> 20 + <nav className="absolute right-4 top-4 block md:right-6 md:top-6 lg:hidden"> 23 21 <AppMenu /> 24 22 </nav> 25 23 {children}
+2
apps/web/src/app/layout.tsx
··· 6 6 import { ClerkProvider } from "@clerk/nextjs"; 7 7 import PlausibleProvider from "next-plausible"; 8 8 9 + import { TailwindIndicator } from "@/components/tailwind-indicator"; 9 10 import { Toaster } from "@/components/ui/toaster"; 10 11 import Background from "./_components/background"; 11 12 ··· 51 52 <body className={`${inter.className} ${calSans.variable}`}> 52 53 <Background>{children}</Background> 53 54 <Toaster /> 55 + <TailwindIndicator /> 54 56 </body> 55 57 </PlausibleProvider> 56 58 </html>
+3 -3
apps/web/src/app/monitor/[id]/page.tsx
··· 2 2 3 3 import { availableRegions } from "@openstatus/tinybird"; 4 4 5 - import { columns } from "@/components/monitor/columns"; 6 - import { DataTable } from "@/components/monitor/data-table"; 5 + import { columns } from "@/components/data-table/columns"; 6 + import { DataTable } from "@/components/data-table/data-table"; 7 7 import { getResponseListData } from "@/lib/tb"; 8 8 9 9 // ··· 30 30 const data = search.success 31 31 ? await getResponseListData({ siteId: params.id, ...search.data }) 32 32 : await getResponseListData({ siteId: params.id }); 33 - if (!data) return <div>Something went wrong</div>; 33 + if (!data || !search.success) return <div>Something went wrong</div>; 34 34 return <DataTable columns={columns} data={data} />; 35 35 }
+1 -1
apps/web/src/app/page.tsx
··· 1 1 import Link from "next/link"; 2 2 3 3 import { Footer } from "@/components/layout/footer"; 4 - import { Tracker } from "@/components/monitor/tracker"; 4 + import { Tracker } from "@/components/tracker"; 5 5 import { Badge } from "@/components/ui/badge"; 6 6 import { Button } from "@/components/ui/button"; 7 7 import { getMonitorListData } from "@/lib/tb";
+1 -1
apps/web/src/app/play/@modal/(..)monitor/[id]/modal.tsx
··· 16 16 return ( 17 17 <Dialog open onOpenChange={handleOpenChange}> 18 18 {/* overflow-auto should happen inside content table */} 19 - <DialogContent className="max-h-screen w-full overflow-auto sm:max-w-3xl"> 19 + <DialogContent className="max-h-screen w-full overflow-auto sm:max-w-3xl sm:p-8"> 20 20 {children} 21 21 </DialogContent> 22 22 </Dialog>
+2 -2
apps/web/src/app/play/@modal/(..)monitor/[id]/page.tsx
··· 2 2 3 3 import { availableRegions } from "@openstatus/tinybird"; 4 4 5 - import { columns } from "@/components/monitor/columns"; 6 - import { DataTable } from "@/components/monitor/data-table"; 5 + import { columns } from "@/components/data-table/columns"; 6 + import { DataTable } from "@/components/data-table/data-table"; 7 7 import { getResponseListData } from "@/lib/tb"; 8 8 import { Modal } from "./modal"; 9 9
+1 -1
apps/web/src/app/play/page.tsx
··· 2 2 3 3 import { groupByRange } from "@openstatus/tinybird"; 4 4 5 - import { Tracker } from "@/components/monitor/tracker"; 5 + import { Tracker } from "@/components/tracker"; 6 6 import { getMonitorListData } from "@/lib/tb"; 7 7 import { ToggleButton } from "./toggle-button"; 8 8
+2 -2
apps/web/src/components/dashboard/header.tsx
··· 13 13 return ( 14 14 <div 15 15 className={cn( 16 - "col-span-full mr-12 flex justify-between md:mr-0", 16 + "col-span-full mr-12 flex justify-between lg:mr-0", 17 17 className, 18 18 )} 19 19 > ··· 30 30 31 31 function HeaderSkeleton({ children }: { children?: React.ReactNode }) { 32 32 return ( 33 - <div className="col-span-full mr-12 flex w-full justify-between md:mr-0"> 33 + <div className="col-span-full mr-12 flex w-full justify-between lg:mr-0"> 34 34 <div className="grid w-full gap-3"> 35 35 <Skeleton className="h-8 w-full max-w-[200px]" /> 36 36 <Skeleton className="h-4 w-full max-w-[300px]" />
+90
apps/web/src/components/data-table/data-table-date-ranger-picker.tsx
··· 1 + "use client"; 2 + 3 + import * as React from "react"; 4 + import { usePathname, useRouter, useSearchParams } from "next/navigation"; 5 + import { format } from "date-fns"; 6 + import { Calendar as CalendarIcon } from "lucide-react"; 7 + import type { DateRange } from "react-day-picker"; 8 + 9 + import { Button } from "@/components/ui/button"; 10 + import { Calendar } from "@/components/ui/calendar"; 11 + import { 12 + Popover, 13 + PopoverContent, 14 + PopoverTrigger, 15 + } from "@/components/ui/popover"; 16 + import useUpdateSearchParams from "@/hooks/use-update-search-params"; 17 + import { cn } from "@/lib/utils"; 18 + 19 + type DataTableDateRangePicker = React.HTMLAttributes<HTMLDivElement>; 20 + 21 + export function DataTableDateRangePicker({ 22 + className, 23 + }: DataTableDateRangePicker) { 24 + const router = useRouter(); 25 + const pathname = usePathname(); 26 + const searchParams = useSearchParams(); 27 + const updateSearchParams = useUpdateSearchParams(); 28 + const [date, setDate] = React.useState<DateRange | undefined>(); 29 + 30 + React.useEffect(() => { 31 + if (searchParams) { 32 + const from = 33 + (searchParams.has("fromDate") && searchParams.get("fromDate")) || 34 + undefined; 35 + const to = 36 + (searchParams.has("toDate") && searchParams.get("toDate")) || undefined; 37 + setDate({ 38 + from: from ? new Date(Number(from)) : undefined, 39 + to: to ? new Date(Number(to)) : undefined, 40 + }); 41 + } 42 + }, [searchParams]); 43 + 44 + return ( 45 + <div className={cn("grid gap-2", className)}> 46 + <Popover> 47 + <PopoverTrigger asChild> 48 + <Button 49 + id="date" 50 + variant="outline" 51 + className={cn( 52 + "w-[250px] justify-start text-left font-normal", 53 + !date && "text-muted-foreground", 54 + )} 55 + > 56 + <CalendarIcon className="mr-2 h-4 w-4" /> 57 + {date?.from ? ( 58 + date.to ? ( 59 + <> 60 + {format(date.from, "LLL dd, y")} -{" "} 61 + {format(date.to, "LLL dd, y")} 62 + </> 63 + ) : ( 64 + format(date.from, "LLL dd, y") 65 + ) 66 + ) : ( 67 + <span>Pick a date</span> 68 + )} 69 + </Button> 70 + </PopoverTrigger> 71 + <PopoverContent className="w-auto p-0" align="start"> 72 + <Calendar 73 + initialFocus 74 + mode="range" 75 + defaultMonth={date?.from} 76 + selected={date} 77 + onSelect={(e) => { 78 + setDate(e); 79 + const searchParams = updateSearchParams({ 80 + fromDate: e?.from?.getTime() || null, 81 + toDate: e?.to?.getTime() || null, 82 + }); 83 + router.replace(`${pathname}?${searchParams}`); 84 + }} 85 + /> 86 + </PopoverContent> 87 + </Popover> 88 + </div> 89 + ); 90 + }
+147
apps/web/src/components/data-table/data-table-faceted-filter.tsx
··· 1 + import * as React from "react"; 2 + import type { Column } from "@tanstack/react-table"; 3 + import { Check, PlusCircle } from "lucide-react"; 4 + 5 + import { Badge } from "@/components/ui/badge"; 6 + import { Button } from "@/components/ui/button"; 7 + import { 8 + Command, 9 + CommandEmpty, 10 + CommandGroup, 11 + CommandInput, 12 + CommandItem, 13 + CommandList, 14 + CommandSeparator, 15 + } from "@/components/ui/command"; 16 + import { 17 + Popover, 18 + PopoverContent, 19 + PopoverTrigger, 20 + } from "@/components/ui/popover"; 21 + import { Separator } from "@/components/ui/separator"; 22 + import { cn } from "@/lib/utils"; 23 + 24 + interface DataTableFacetedFilter<TData, TValue> { 25 + column?: Column<TData, TValue>; 26 + title?: string; 27 + options: { 28 + label: string; 29 + value: string; 30 + icon?: React.ComponentType<{ className?: string }>; 31 + }[]; 32 + } 33 + 34 + export function DataTableFacetedFilter<TData, TValue>({ 35 + column, 36 + title, 37 + options, 38 + }: DataTableFacetedFilter<TData, TValue>) { 39 + const facets = column?.getFacetedUniqueValues(); 40 + const selectedValues = new Set(column?.getFilterValue() as string[]); 41 + 42 + return ( 43 + <Popover> 44 + <PopoverTrigger asChild> 45 + <Button variant="outline" size="sm" className="h-8 border-dashed"> 46 + <PlusCircle className="mr-2 h-4 w-4" /> 47 + {title} 48 + {selectedValues?.size > 0 && ( 49 + <> 50 + <Separator orientation="vertical" className="mx-2 h-4" /> 51 + <Badge 52 + variant="secondary" 53 + className="rounded-sm px-1 font-normal lg:hidden" 54 + > 55 + {selectedValues.size} 56 + </Badge> 57 + <div className="hidden space-x-1 lg:flex"> 58 + {selectedValues.size > 2 ? ( 59 + <Badge 60 + variant="secondary" 61 + className="rounded-sm px-1 font-normal" 62 + > 63 + {selectedValues.size} selected 64 + </Badge> 65 + ) : ( 66 + options 67 + .filter((option) => selectedValues.has(option.value)) 68 + .map((option) => ( 69 + <Badge 70 + variant="secondary" 71 + key={option.value} 72 + className="rounded-sm px-1 font-normal" 73 + > 74 + {option.label} 75 + </Badge> 76 + )) 77 + )} 78 + </div> 79 + </> 80 + )} 81 + </Button> 82 + </PopoverTrigger> 83 + <PopoverContent className="w-[200px] p-0" align="start"> 84 + <Command> 85 + <CommandInput placeholder={title} /> 86 + <CommandList> 87 + <CommandEmpty>No results found.</CommandEmpty> 88 + <CommandGroup> 89 + {options.map((option) => { 90 + const isSelected = selectedValues.has(option.value); 91 + return ( 92 + <CommandItem 93 + key={option.value} 94 + onSelect={() => { 95 + if (isSelected) { 96 + selectedValues.delete(option.value); 97 + } else { 98 + selectedValues.add(option.value); 99 + } 100 + const filterValues = Array.from(selectedValues); 101 + column?.setFilterValue( 102 + filterValues.length ? filterValues : undefined, 103 + ); 104 + }} 105 + > 106 + <div 107 + className={cn( 108 + "border-primary mr-2 flex h-4 w-4 items-center justify-center rounded-sm border", 109 + isSelected 110 + ? "bg-primary text-primary-foreground" 111 + : "opacity-50 [&_svg]:invisible", 112 + )} 113 + > 114 + <Check className={cn("h-4 w-4")} /> 115 + </div> 116 + {option.icon && ( 117 + <option.icon className="text-muted-foreground mr-2 h-4 w-4" /> 118 + )} 119 + <span>{option.label}</span> 120 + {facets?.get(option.value) && ( 121 + <span className="ml-auto flex h-4 w-4 items-center justify-center font-mono text-xs"> 122 + {facets.get(option.value)} 123 + </span> 124 + )} 125 + </CommandItem> 126 + ); 127 + })} 128 + </CommandGroup> 129 + {selectedValues.size > 0 && ( 130 + <> 131 + <CommandSeparator /> 132 + <CommandGroup> 133 + <CommandItem 134 + onSelect={() => column?.setFilterValue(undefined)} 135 + className="justify-center text-center" 136 + > 137 + Clear filters 138 + </CommandItem> 139 + </CommandGroup> 140 + </> 141 + )} 142 + </CommandList> 143 + </Command> 144 + </PopoverContent> 145 + </Popover> 146 + ); 147 + }
+3
apps/web/src/components/data-table/data-table-region-select.tsx
··· 1 + export default function DataTableRegionSelect() { 2 + return <div></div>; 3 + }
+49
apps/web/src/components/data-table/data-table-toolbar.tsx
··· 1 + "use client"; 2 + 3 + import type { Table } from "@tanstack/react-table"; 4 + import { X } from "lucide-react"; 5 + 6 + import { Button } from "@/components/ui/button"; 7 + import { regionsDict } from "@/data/regions-dictionary"; 8 + import { DataTableDateRangePicker } from "./data-table-date-ranger-picker"; 9 + import { DataTableFacetedFilter } from "./data-table-faceted-filter"; 10 + import { DataTableFilterInput } from "./data-table-filter-input"; 11 + 12 + interface DataTableToolbarProps<TData> { 13 + table: Table<TData>; 14 + } 15 + 16 + export function DataTableToolbar<TData>({ 17 + table, 18 + }: DataTableToolbarProps<TData>) { 19 + const isFiltered = table.getState().columnFilters.length > 0; 20 + 21 + return ( 22 + <div className="flex flex-wrap items-center justify-between gap-3"> 23 + <div className="flex flex-1 items-center gap-2"> 24 + <DataTableFilterInput table={table} /> 25 + {table.getColumn("region") && ( 26 + <DataTableFacetedFilter 27 + column={table.getColumn("region")} 28 + title="Region" 29 + options={Object.keys(regionsDict).map((key) => ({ 30 + label: regionsDict[key].location, 31 + value: regionsDict[key].code, 32 + }))} 33 + /> 34 + )} 35 + {isFiltered && ( 36 + <Button 37 + variant="ghost" 38 + onClick={() => table.resetColumnFilters()} 39 + className="h-8 px-2 lg:px-3" 40 + > 41 + Reset 42 + <X className="ml-2 h-4 w-4" /> 43 + </Button> 44 + )} 45 + </div> 46 + <DataTableDateRangePicker /> 47 + </div> 48 + ); 49 + }
+30 -2
apps/web/src/components/forms/montitor-form.tsx
··· 26 26 SelectItem, 27 27 SelectTrigger, 28 28 SelectValue, 29 - } from "../ui/select"; 29 + } from "@/components/ui/select"; 30 + import { Switch } from "@/components/ui/switch"; 30 31 31 32 type Schema = z.infer<typeof insertMonitorSchema>; 32 33 ··· 44 45 name: defaultValues?.name || "", 45 46 description: defaultValues?.description || "", 46 47 periodicity: defaultValues?.periodicity || undefined, 48 + status: defaultValues?.status || "inactive", 47 49 }, 48 50 }); 49 51 ··· 101 103 /> 102 104 <FormField 103 105 control={form.control} 106 + name="status" 107 + render={({ field }) => ( 108 + <FormItem className="flex flex-row items-center justify-between"> 109 + <div className="space-y-0.5"> 110 + <FormLabel>Active</FormLabel> 111 + <FormDescription> 112 + This will start ping your endpoint on based on the selected 113 + frequence. 114 + </FormDescription> 115 + </div> 116 + <FormControl> 117 + {/* TODO: make the monitor.active a boolean value */} 118 + <Switch 119 + checked={field.value === "active" ? true : false} 120 + onCheckedChange={(value) => 121 + field.onChange(value ? "active" : "inactive") 122 + } 123 + disabled 124 + /> 125 + </FormControl> 126 + <FormMessage /> 127 + </FormItem> 128 + )} 129 + /> 130 + <FormField 131 + control={form.control} 104 132 name="periodicity" 105 133 render={({ field }) => ( 106 134 <FormItem> ··· 133 161 </SelectContent> 134 162 </Select> 135 163 <FormDescription> 136 - How often your endpoint will be checked 164 + Frequency of how often your endpoint will be pinged. 137 165 </FormDescription> 138 166 <FormMessage /> 139 167 </FormItem>
+65 -112
apps/web/src/components/forms/status-page-form.tsx
··· 1 1 "use client"; 2 2 3 - import { useState } from "react"; 4 - import { useParams, useRouter } from "next/navigation"; 5 3 import { zodResolver } from "@hookform/resolvers/zod"; 6 - import { Loader2 } from "lucide-react"; 7 4 import { useForm } from "react-hook-form"; 8 5 import type * as z from "zod"; 9 6 10 - import type { insertMonitorSchema } from "@openstatus/db/src/schema"; 11 - import { insertPageSchema, periodicityEnum } from "@openstatus/db/src/schema"; 7 + import { insertPageSchema } from "@openstatus/db/src/schema"; 12 8 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 9 import { 24 10 Form, 25 11 FormControl, ··· 30 16 FormMessage, 31 17 } from "@/components/ui/form"; 32 18 import { Input } from "@/components/ui/input"; 33 - import { api } from "@/trpc/client"; 34 19 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(); 20 + type Schema = z.infer<typeof insertPageSchema>; 21 + 22 + interface Props { 23 + id: string; 24 + defaultValues?: Schema; 25 + onSubmit: (values: Schema) => Promise<void>; 26 + } 41 27 42 - const form = useForm<z.infer<typeof insertPageSchema>>({ 28 + export function StatusPageForm({ id, defaultValues, onSubmit }: Props) { 29 + const form = useForm<Schema>({ 43 30 resolver: zodResolver(insertPageSchema), 44 31 defaultValues: { 45 - title: "", 46 - slug: "", 47 - description: "", 48 - workspaceId: Number(params.workspaceId), 32 + title: defaultValues?.title || "", 33 + slug: defaultValues?.slug || "", 34 + description: defaultValues?.description || "", 49 35 }, 50 36 }); 51 37 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 38 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> 39 + <Form {...form}> 40 + <form onSubmit={form.handleSubmit(onSubmit)} id={id}> 41 + <div className="grid w-full items-center space-y-6"> 42 + <FormField 43 + control={form.control} 44 + name="title" 45 + render={({ field }) => ( 46 + <FormItem> 47 + <FormLabel>Title</FormLabel> 48 + <FormControl> 49 + <Input placeholder="" {...field} /> 50 + </FormControl> 51 + <FormDescription>This is title of your page.</FormDescription> 52 + <FormMessage /> 53 + </FormItem> 54 + )} 55 + /> 56 + <FormField 57 + control={form.control} 58 + name="slug" 59 + render={({ field }) => ( 60 + <FormItem> 61 + <FormLabel>Slug</FormLabel> 62 + <FormControl> 63 + <Input placeholder="" {...field} /> 64 + </FormControl> 65 + <FormDescription> 66 + This is your url of your page. 67 + </FormDescription> 68 + <FormMessage /> 69 + </FormItem> 70 + )} 71 + /> 72 + <FormField 73 + control={form.control} 74 + name="description" 75 + render={({ field }) => ( 76 + <FormItem> 77 + <FormLabel>Description</FormLabel> 78 + <FormControl> 79 + <Input placeholder="" {...field} /> 80 + </FormControl> 81 + <FormDescription> 82 + Give your user some information about it. 83 + </FormDescription> 84 + <FormMessage /> 85 + </FormItem> 86 + )} 87 + /> 88 + </div> 89 + </form> 90 + </Form> 138 91 ); 139 92 }
+9 -1
apps/web/src/components/icons.tsx
··· 1 - import { Activity, LayoutDashboard, Link, PanelTop, Siren } from "lucide-react"; 1 + import { 2 + Activity, 3 + LayoutDashboard, 4 + Link, 5 + PanelTop, 6 + Siren, 7 + Table, 8 + } from "lucide-react"; 2 9 import type { Icon as LucideIcon, LucideProps } from "lucide-react"; 3 10 4 11 export type Icon = LucideIcon; ··· 10 17 link: Link, 11 18 siren: Siren, 12 19 "panel-top": PanelTop, 20 + table: Table, 13 21 } as const;
+15 -13
apps/web/src/components/layout/app-header.tsx
··· 3 3 import Link from "next/link"; 4 4 import { UserButton, useUser } from "@clerk/nextjs"; 5 5 6 + import { Shell } from "../dashboard/shell"; 6 7 import { Skeleton } from "../ui/skeleton"; 7 8 8 9 export function AppHeader() { 9 10 const { isLoaded, isSignedIn } = useUser(); 10 11 11 12 return ( 12 - // use `h-8` to avoid header layout shift on load 13 - <header className="z-10 flex h-8 w-full items-center justify-between"> 14 - <Link 15 - href="/" 16 - className="font-cal text-muted-foreground hover:text-foreground text-lg" 17 - > 18 - openstatus 19 - </Link> 20 - {!isLoaded && !isSignedIn ? ( 21 - <Skeleton className="h-8 w-8 rounded-full" /> 22 - ) : ( 23 - <UserButton /> 24 - )} 13 + <header className="border-border sticky top-3 z-50 w-full"> 14 + <Shell className="bg-background/70 flex w-full items-center justify-between px-3 py-3 backdrop-blur-lg md:px-6 md:py-3"> 15 + <Link 16 + href="/app" 17 + className="font-cal text-muted-foreground hover:text-foreground text-lg" 18 + > 19 + openstatus 20 + </Link> 21 + {!isLoaded && !isSignedIn ? ( 22 + <Skeleton className="h-8 w-8 rounded-full" /> 23 + ) : ( 24 + <UserButton /> 25 + )} 26 + </Shell> 25 27 </header> 26 28 ); 27 29 }
+1 -1
apps/web/src/components/layout/app-sidebar.tsx
··· 21 21 href={link} 22 22 className={cn( 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 - pathname === link && 24 + pathname.startsWith(link) && 25 25 "bg-muted/50 border-border text-foreground", 26 26 disabled && "pointer-events-none opacity-60", 27 27 )}
+10 -2
apps/web/src/components/monitor/columns.tsx apps/web/src/components/data-table/columns.tsx
··· 6 6 import type { Ping } from "@openstatus/tinybird"; 7 7 8 8 import { Badge } from "@/components/ui/badge"; 9 + import { regionsDict } from "@/data/regions-dictionary"; 9 10 import { cn } from "@/lib/utils"; 10 11 import { DataTableColumnHeader } from "./data-table-column-header"; 11 12 import { DataTableRowActions } from "./data-table-row-action"; ··· 19 20 cell: ({ row }) => { 20 21 return ( 21 22 <div> 22 - {format(new Date(row.getValue("timestamp")), "dd/MM/yy HH:mm")} 23 + {format(new Date(row.getValue("timestamp")), "LLL dd, y HH:mm")} 23 24 </div> 24 25 ); 25 26 }, ··· 58 59 { 59 60 accessorKey: "latency", 60 61 header: ({ column }) => ( 61 - <DataTableColumnHeader column={column} title="Latency" /> 62 + <DataTableColumnHeader column={column} title="Latency (ms)" /> 62 63 ), 63 64 }, 64 65 { ··· 66 67 header: ({ column }) => ( 67 68 <DataTableColumnHeader column={column} title="Region" /> 68 69 ), 70 + cell: ({ row }) => { 71 + const region = String(row.getValue("region")); 72 + return <div>{regionsDict[region]?.location}</div>; 73 + }, 74 + filterFn: (row, id, value) => { 75 + return value.includes(row.getValue(id)); 76 + }, 69 77 }, 70 78 { 71 79 accessorKey: "url",
apps/web/src/components/monitor/data-table-column-header.tsx apps/web/src/components/data-table/data-table-column-header.tsx
apps/web/src/components/monitor/data-table-filter-input.tsx apps/web/src/components/data-table/data-table-filter-input.tsx
apps/web/src/components/monitor/data-table-pagination.tsx apps/web/src/components/data-table/data-table-pagination.tsx
apps/web/src/components/monitor/data-table-row-action.tsx apps/web/src/components/data-table/data-table-row-action.tsx
+2 -2
apps/web/src/components/monitor/data-table.tsx apps/web/src/components/data-table/data-table.tsx
··· 23 23 TableHeader, 24 24 TableRow, 25 25 } from "@/components/ui/table"; 26 - import { DataTableFilterInput } from "./data-table-filter-input"; 27 26 import { DataTablePagination } from "./data-table-pagination"; 27 + import { DataTableToolbar } from "./data-table-toolbar"; 28 28 29 29 interface DataTableProps<TData, TValue> { 30 30 columns: ColumnDef<TData, TValue>[]; ··· 58 58 59 59 return ( 60 60 <div className="space-y-3"> 61 - <DataTableFilterInput table={table} /> 61 + <DataTableToolbar table={table} /> 62 62 <div className="rounded-md border"> 63 63 <Table> 64 64 <TableHeader>
apps/web/src/components/monitor/tracker.tsx apps/web/src/components/tracker.tsx
+1 -1
apps/web/src/components/status-page/monitor.tsx
··· 3 3 import type { selectMonitorSchema } from "@openstatus/db/src/schema"; 4 4 5 5 import { getMonitorListData } from "@/lib/tb"; 6 - import { Tracker } from "../monitor/tracker"; 6 + import { Tracker } from "../tracker"; 7 7 8 8 export const Monitor = async ({ 9 9 monitor,
+14
apps/web/src/components/tailwind-indicator.tsx
··· 1 + export function TailwindIndicator() { 2 + if (process.env.NODE_ENV === "production") return null; 3 + 4 + return ( 5 + <div className="fixed bottom-1 left-1 z-50 flex h-6 w-6 items-center justify-center rounded-full bg-gray-800 p-3 font-mono text-xs text-white"> 6 + <div className="block sm:hidden">xs</div> 7 + <div className="hidden sm:block md:hidden">sm</div> 8 + <div className="hidden md:block lg:hidden">md</div> 9 + <div className="hidden lg:block xl:hidden">lg</div> 10 + <div className="hidden xl:block 2xl:hidden">xl</div> 11 + <div className="hidden 2xl:block">2xl</div> 12 + </div> 13 + ); 14 + }
+64
apps/web/src/components/ui/calendar.tsx
··· 1 + "use client"; 2 + 3 + import * as React from "react"; 4 + import { ChevronLeft, ChevronRight } from "lucide-react"; 5 + import { DayPicker } from "react-day-picker"; 6 + 7 + import { buttonVariants } from "@/components/ui/button"; 8 + import { cn } from "@/lib/utils"; 9 + 10 + export type CalendarProps = React.ComponentProps<typeof DayPicker>; 11 + 12 + function Calendar({ 13 + className, 14 + classNames, 15 + showOutsideDays = true, 16 + ...props 17 + }: CalendarProps) { 18 + return ( 19 + <DayPicker 20 + showOutsideDays={showOutsideDays} 21 + className={cn("p-3", className)} 22 + classNames={{ 23 + months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0", 24 + month: "space-y-4", 25 + caption: "flex justify-center pt-1 relative items-center", 26 + caption_label: "text-sm font-medium", 27 + nav: "space-x-1 flex items-center", 28 + nav_button: cn( 29 + buttonVariants({ variant: "outline" }), 30 + "h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100", 31 + ), 32 + nav_button_previous: "absolute left-1", 33 + nav_button_next: "absolute right-1", 34 + table: "w-full border-collapse space-y-1", 35 + head_row: "flex", 36 + head_cell: 37 + "text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]", 38 + row: "flex w-full mt-2", 39 + cell: "text-center text-sm p-0 relative [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20", 40 + day: cn( 41 + buttonVariants({ variant: "ghost" }), 42 + "h-8 w-8 p-0 font-normal aria-selected:opacity-100", 43 + ), 44 + day_selected: 45 + "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground", 46 + day_today: "bg-accent text-accent-foreground", 47 + day_outside: "text-muted-foreground opacity-50", 48 + day_disabled: "text-muted-foreground opacity-50", 49 + day_range_middle: 50 + "aria-selected:bg-accent aria-selected:text-accent-foreground", 51 + day_hidden: "invisible", 52 + ...classNames, 53 + }} 54 + components={{ 55 + IconLeft: ({ ...props }) => <ChevronLeft className="h-4 w-4" />, 56 + IconRight: ({ ...props }) => <ChevronRight className="h-4 w-4" />, 57 + }} 58 + {...props} 59 + /> 60 + ); 61 + } 62 + Calendar.displayName = "Calendar"; 63 + 64 + export { Calendar };
+31
apps/web/src/components/ui/popover.tsx
··· 1 + "use client"; 2 + 3 + import * as React from "react"; 4 + import * as PopoverPrimitive from "@radix-ui/react-popover"; 5 + 6 + import { cn } from "@/lib/utils"; 7 + 8 + const Popover = PopoverPrimitive.Root; 9 + 10 + const PopoverTrigger = PopoverPrimitive.Trigger; 11 + 12 + const PopoverContent = React.forwardRef< 13 + React.ElementRef<typeof PopoverPrimitive.Content>, 14 + React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> 15 + >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 16 + <PopoverPrimitive.Portal> 17 + <PopoverPrimitive.Content 18 + ref={ref} 19 + align={align} 20 + sideOffset={sideOffset} 21 + className={cn( 22 + "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 rounded-md border p-4 shadow-md outline-none", 23 + className, 24 + )} 25 + {...props} 26 + /> 27 + </PopoverPrimitive.Portal> 28 + )); 29 + PopoverContent.displayName = PopoverPrimitive.Content.displayName; 30 + 31 + export { Popover, PopoverTrigger, PopoverContent };
+31
apps/web/src/components/ui/separator.tsx
··· 1 + "use client"; 2 + 3 + import * as React from "react"; 4 + import * as SeparatorPrimitive from "@radix-ui/react-separator"; 5 + 6 + import { cn } from "@/lib/utils"; 7 + 8 + const Separator = React.forwardRef< 9 + React.ElementRef<typeof SeparatorPrimitive.Root>, 10 + React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root> 11 + >( 12 + ( 13 + { className, orientation = "horizontal", decorative = true, ...props }, 14 + ref, 15 + ) => ( 16 + <SeparatorPrimitive.Root 17 + ref={ref} 18 + decorative={decorative} 19 + orientation={orientation} 20 + className={cn( 21 + "bg-border shrink-0", 22 + orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]", 23 + className, 24 + )} 25 + {...props} 26 + /> 27 + ), 28 + ); 29 + Separator.displayName = SeparatorPrimitive.Root.displayName; 30 + 31 + export { Separator };
+29
apps/web/src/components/ui/switch.tsx
··· 1 + "use client"; 2 + 3 + import * as React from "react"; 4 + import * as SwitchPrimitives from "@radix-ui/react-switch"; 5 + 6 + import { cn } from "@/lib/utils"; 7 + 8 + const Switch = React.forwardRef< 9 + React.ElementRef<typeof SwitchPrimitives.Root>, 10 + React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root> 11 + >(({ className, ...props }, ref) => ( 12 + <SwitchPrimitives.Root 13 + className={cn( 14 + "focus-visible:ring-ring focus-visible:ring-offset-background data-[state=checked]:bg-primary data-[state=unchecked]:bg-input peer inline-flex h-[24px] w-[44px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", 15 + className, 16 + )} 17 + {...props} 18 + ref={ref} 19 + > 20 + <SwitchPrimitives.Thumb 21 + className={cn( 22 + "bg-background pointer-events-none block h-5 w-5 rounded-full shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0", 23 + )} 24 + /> 25 + </SwitchPrimitives.Root> 26 + )); 27 + Switch.displayName = SwitchPrimitives.Root.displayName; 28 + 29 + export { Switch };
+2 -2
apps/web/src/config/pages.ts
··· 8 8 disabled?: boolean; 9 9 }; 10 10 11 + // TODO: add to <Header id="dashboard" /> to easily access title and description - ideally allow both 11 12 export const pagesConfig: Page[] = [ 12 13 { 13 14 title: "Dashboard", 14 15 description: "Get an overview of what's hot.", 15 - href: "", 16 + href: "/dashboard", 16 17 icon: "layout-dashboard", 17 18 }, 18 - 19 19 { 20 20 title: "Monitors", 21 21 description: "Check all the responses in one place.",
+1 -1
apps/web/src/lib/utils.ts
··· 12 12 } 13 13 14 14 export function formatDate(date: Date) { 15 - return format(date, "dd/MM/yyyy"); 15 + return format(date, "LLL dd, y"); 16 16 }
+1 -1
apps/web/src/middleware.ts
··· 85 85 86 86 if (result.length) { 87 87 const orgSelection = new URL( 88 - `/app/${result[0].users_to_workspaces.workspaceId}`, 88 + `/app/${result[0].users_to_workspaces.workspaceId}/dashboard`, 89 89 req.url, 90 90 ); 91 91 return NextResponse.redirect(orgSelection);
+1
packages/api/src/router/page.ts
··· 5 5 6 6 import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc"; 7 7 8 + // TODO: deletePageById - updatePageById 8 9 export const pageRouter = createTRPCRouter({ 9 10 createPage: protectedProcedure 10 11 .input(insertPageSchema)
+5
packages/db/src/schema/monitor.ts
··· 1 1 import { relations } from "drizzle-orm"; 2 2 import { 3 + boolean, 3 4 int, 4 5 mysqlEnum, 5 6 mysqlTable, ··· 28 29 ]) 29 30 .default("other") 30 31 .notNull(), 32 + 33 + // TBD: if we keep or not?!? 31 34 status: mysqlEnum("status", ["active", "inactive"]) 32 35 .default("inactive") 33 36 .notNull(), 37 + 38 + active: boolean("active").default(false), 34 39 35 40 url: varchar("url", { length: 512 }).notNull(), 36 41
+105
pnpm-lock.yaml
··· 74 74 '@radix-ui/react-label': 75 75 specifier: ^2.0.2 76 76 version: 2.0.2(@types/react-dom@18.2.5)(@types/react@18.2.12)(react-dom@18.2.0)(react@18.2.0) 77 + '@radix-ui/react-popover': 78 + specifier: ^1.0.6 79 + version: 1.0.6(@types/react-dom@18.2.5)(@types/react@18.2.12)(react-dom@18.2.0)(react@18.2.0) 77 80 '@radix-ui/react-select': 78 81 specifier: ^1.2.2 79 82 version: 1.2.2(@types/react-dom@18.2.5)(@types/react@18.2.12)(react-dom@18.2.0)(react@18.2.0) 83 + '@radix-ui/react-separator': 84 + specifier: ^1.0.3 85 + version: 1.0.3(@types/react-dom@18.2.5)(@types/react@18.2.12)(react-dom@18.2.0)(react@18.2.0) 80 86 '@radix-ui/react-slot': 81 87 specifier: ^1.0.2 82 88 version: 1.0.2(@types/react@18.2.12)(react@18.2.0) 89 + '@radix-ui/react-switch': 90 + specifier: ^1.0.3 91 + version: 1.0.3(@types/react-dom@18.2.5)(@types/react@18.2.12)(react-dom@18.2.0)(react@18.2.0) 83 92 '@radix-ui/react-toast': 84 93 specifier: ^1.1.4 85 94 version: 1.1.4(@types/react-dom@18.2.5)(@types/react@18.2.12)(react-dom@18.2.0)(react@18.2.0) ··· 137 146 react: 138 147 specifier: 18.2.0 139 148 version: 18.2.0 149 + react-day-picker: 150 + specifier: ^8.8.0 151 + version: 8.8.0(date-fns@2.30.0)(react@18.2.0) 140 152 react-dom: 141 153 specifier: 18.2.0 142 154 version: 18.2.0(react@18.2.0) ··· 2518 2530 react-remove-scroll: 2.5.5(@types/react@18.2.12)(react@18.2.0) 2519 2531 dev: false 2520 2532 2533 + /@radix-ui/react-popover@1.0.6(@types/react-dom@18.2.5)(@types/react@18.2.12)(react-dom@18.2.0)(react@18.2.0): 2534 + resolution: {integrity: sha512-cZ4defGpkZ0qTRtlIBzJLSzL6ht7ofhhW4i1+pkemjV1IKXm0wgCRnee154qlV6r9Ttunmh2TNZhMfV2bavUyA==} 2535 + peerDependencies: 2536 + '@types/react': '*' 2537 + '@types/react-dom': '*' 2538 + react: ^16.8 || ^17.0 || ^18.0 2539 + react-dom: ^16.8 || ^17.0 || ^18.0 2540 + peerDependenciesMeta: 2541 + '@types/react': 2542 + optional: true 2543 + '@types/react-dom': 2544 + optional: true 2545 + dependencies: 2546 + '@babel/runtime': 7.22.5 2547 + '@radix-ui/primitive': 1.0.1 2548 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.12)(react@18.2.0) 2549 + '@radix-ui/react-context': 1.0.1(@types/react@18.2.12)(react@18.2.0) 2550 + '@radix-ui/react-dismissable-layer': 1.0.4(@types/react-dom@18.2.5)(@types/react@18.2.12)(react-dom@18.2.0)(react@18.2.0) 2551 + '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.12)(react@18.2.0) 2552 + '@radix-ui/react-focus-scope': 1.0.3(@types/react-dom@18.2.5)(@types/react@18.2.12)(react-dom@18.2.0)(react@18.2.0) 2553 + '@radix-ui/react-id': 1.0.1(@types/react@18.2.12)(react@18.2.0) 2554 + '@radix-ui/react-popper': 1.1.2(@types/react-dom@18.2.5)(@types/react@18.2.12)(react-dom@18.2.0)(react@18.2.0) 2555 + '@radix-ui/react-portal': 1.0.3(@types/react-dom@18.2.5)(@types/react@18.2.12)(react-dom@18.2.0)(react@18.2.0) 2556 + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.5)(@types/react@18.2.12)(react-dom@18.2.0)(react@18.2.0) 2557 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.5)(@types/react@18.2.12)(react-dom@18.2.0)(react@18.2.0) 2558 + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.12)(react@18.2.0) 2559 + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.12)(react@18.2.0) 2560 + '@types/react': 18.2.12 2561 + '@types/react-dom': 18.2.5 2562 + aria-hidden: 1.2.3 2563 + react: 18.2.0 2564 + react-dom: 18.2.0(react@18.2.0) 2565 + react-remove-scroll: 2.5.5(@types/react@18.2.12)(react@18.2.0) 2566 + dev: false 2567 + 2521 2568 /@radix-ui/react-popper@1.1.2(@types/react-dom@18.2.5)(@types/react@18.2.12)(react-dom@18.2.0)(react@18.2.0): 2522 2569 resolution: {integrity: sha512-1CnGGfFi/bbqtJZZ0P/NQY20xdG3E0LALJaLUEoKwPLwl6PPPfbeiCqMVQnhoFRAxjJj4RpBRJzDmUgsex2tSg==} 2523 2570 peerDependencies: ··· 2719 2766 react-remove-scroll: 2.5.5(@types/react@18.2.12)(react@18.2.0) 2720 2767 dev: false 2721 2768 2769 + /@radix-ui/react-separator@1.0.3(@types/react-dom@18.2.5)(@types/react@18.2.12)(react-dom@18.2.0)(react@18.2.0): 2770 + resolution: {integrity: sha512-itYmTy/kokS21aiV5+Z56MZB54KrhPgn6eHDKkFeOLR34HMN2s8PaN47qZZAGnvupcjxHaFZnW4pQEh0BvvVuw==} 2771 + peerDependencies: 2772 + '@types/react': '*' 2773 + '@types/react-dom': '*' 2774 + react: ^16.8 || ^17.0 || ^18.0 2775 + react-dom: ^16.8 || ^17.0 || ^18.0 2776 + peerDependenciesMeta: 2777 + '@types/react': 2778 + optional: true 2779 + '@types/react-dom': 2780 + optional: true 2781 + dependencies: 2782 + '@babel/runtime': 7.22.5 2783 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.5)(@types/react@18.2.12)(react-dom@18.2.0)(react@18.2.0) 2784 + '@types/react': 18.2.12 2785 + '@types/react-dom': 18.2.5 2786 + react: 18.2.0 2787 + react-dom: 18.2.0(react@18.2.0) 2788 + dev: false 2789 + 2722 2790 /@radix-ui/react-slot@1.0.0(react@18.2.0): 2723 2791 resolution: {integrity: sha512-3mrKauI/tWXo1Ll+gN5dHcxDPdm/Df1ufcDLCecn+pnCIVcdWE7CujXo8QaXOWRJyZyQWWbpB8eFwHzWXlv5mQ==} 2724 2792 peerDependencies: ··· 2742 2810 '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.12)(react@18.2.0) 2743 2811 '@types/react': 18.2.12 2744 2812 react: 18.2.0 2813 + dev: false 2814 + 2815 + /@radix-ui/react-switch@1.0.3(@types/react-dom@18.2.5)(@types/react@18.2.12)(react-dom@18.2.0)(react@18.2.0): 2816 + resolution: {integrity: sha512-mxm87F88HyHztsI7N+ZUmEoARGkC22YVW5CaC+Byc+HRpuvCrOBPTAnXgf+tZ/7i0Sg/eOePGdMhUKhPaQEqow==} 2817 + peerDependencies: 2818 + '@types/react': '*' 2819 + '@types/react-dom': '*' 2820 + react: ^16.8 || ^17.0 || ^18.0 2821 + react-dom: ^16.8 || ^17.0 || ^18.0 2822 + peerDependenciesMeta: 2823 + '@types/react': 2824 + optional: true 2825 + '@types/react-dom': 2826 + optional: true 2827 + dependencies: 2828 + '@babel/runtime': 7.22.5 2829 + '@radix-ui/primitive': 1.0.1 2830 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.12)(react@18.2.0) 2831 + '@radix-ui/react-context': 1.0.1(@types/react@18.2.12)(react@18.2.0) 2832 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.5)(@types/react@18.2.12)(react-dom@18.2.0)(react@18.2.0) 2833 + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.12)(react@18.2.0) 2834 + '@radix-ui/react-use-previous': 1.0.1(@types/react@18.2.12)(react@18.2.0) 2835 + '@radix-ui/react-use-size': 1.0.1(@types/react@18.2.12)(react@18.2.0) 2836 + '@types/react': 18.2.12 2837 + '@types/react-dom': 18.2.5 2838 + react: 18.2.0 2839 + react-dom: 18.2.0(react@18.2.0) 2745 2840 dev: false 2746 2841 2747 2842 /@radix-ui/react-toast@1.1.4(@types/react-dom@18.2.5)(@types/react@18.2.12)(react-dom@18.2.0)(react@18.2.0): ··· 7215 7310 minimist: 1.2.8 7216 7311 strip-json-comments: 2.0.1 7217 7312 dev: true 7313 + 7314 + /react-day-picker@8.8.0(date-fns@2.30.0)(react@18.2.0): 7315 + resolution: {integrity: sha512-QIC3uOuyGGbtypbd5QEggsCSqVaPNu8kzUWquZ7JjW9fuWB9yv7WyixKmnaFelTLXFdq7h7zU6n/aBleBqe/dA==} 7316 + peerDependencies: 7317 + date-fns: ^2.28.0 7318 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 7319 + dependencies: 7320 + date-fns: 2.30.0 7321 + react: 18.2.0 7322 + dev: false 7218 7323 7219 7324 /react-dom@18.2.0(react@18.2.0): 7220 7325 resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==}