Openstatus www.openstatus.dev

feat: add quicklinks for status reports (#1148)

authored by

Maximilian Kaske and committed by
GitHub
de559508 18b1ce86

+124 -4
+53
apps/web/src/app/app/[workspaceSlug]/(dashboard)/status-pages/(overview)/_components/status-report-button.tsx
··· 1 + import { cn } from "@/lib/utils"; 2 + import type { Page } from "@openstatus/db/src/schema"; 3 + import { 4 + Alert, 5 + AlertDescription, 6 + AlertTitle, 7 + } from "@openstatus/ui/src/components/alert"; 8 + import { buttonVariants } from "@openstatus/ui/src/components/button"; 9 + import { 10 + DropdownMenu, 11 + DropdownMenuContent, 12 + DropdownMenuItem, 13 + DropdownMenuLabel, 14 + DropdownMenuSeparator, 15 + DropdownMenuTrigger, 16 + } from "@openstatus/ui/src/components/dropdown-menu"; 17 + import { ChevronDown, Megaphone } from "lucide-react"; 18 + import Link from "next/link"; 19 + 20 + export function StatusReportButton({ pages }: { pages: Page[] }) { 21 + return ( 22 + <Alert className="max-w-xl"> 23 + <AlertTitle>Status Reports</AlertTitle> 24 + <AlertDescription> 25 + Start a new report. If you want to update your users about a current 26 + report, please hover the <em>Last Report</em> column and click on{" "} 27 + <em>Go to report</em>. 28 + </AlertDescription> 29 + <DropdownMenu> 30 + <DropdownMenuTrigger 31 + className={cn( 32 + buttonVariants({ size: "sm", variant: "secondary" }), 33 + "mt-2", 34 + )} 35 + > 36 + <Megaphone className="h-4 w-4 mr-1 pb-0.5" /> 37 + New Status Report 38 + <span className="h-8 w-px bg-background mx-2" /> 39 + <ChevronDown className="h-4 w-4" /> 40 + </DropdownMenuTrigger> 41 + <DropdownMenuContent align="end"> 42 + <DropdownMenuLabel>Select Page</DropdownMenuLabel> 43 + <DropdownMenuSeparator /> 44 + {pages.map((page) => ( 45 + <Link key={page.id} href={`./status-pages/${page.id}/reports/new`}> 46 + <DropdownMenuItem>{page.title}</DropdownMenuItem> 47 + </Link> 48 + ))} 49 + </DropdownMenuContent> 50 + </DropdownMenu> 51 + </Alert> 52 + ); 53 + }
+3
apps/web/src/app/app/[workspaceSlug]/(dashboard)/status-pages/(overview)/page.tsx
··· 8 8 import { columns } from "@/components/data-table/status-page/columns"; 9 9 import { DataTable } from "@/components/data-table/status-page/data-table"; 10 10 import { api } from "@/trpc/server"; 11 + import { StatusReportButton } from "./_components/status-report-button"; 11 12 12 13 export default async function MonitorPage() { 13 14 const pages = await api.page.getPagesByWorkspace.query(); ··· 27 28 ); 28 29 } 29 30 const isLimitReached = await api.page.isPageLimitReached.query(); 31 + 30 32 return ( 31 33 <> 32 34 <DataTable columns={columns} data={pages} /> 33 35 {isLimitReached ? <Limit /> : null} 36 + <StatusReportButton pages={pages} /> 34 37 </> 35 38 ); 36 39 }
+2
apps/web/src/components/data-table/notification/data-table-row-actions.tsx
··· 21 21 DropdownMenu, 22 22 DropdownMenuContent, 23 23 DropdownMenuItem, 24 + DropdownMenuSeparator, 24 25 DropdownMenuTrigger, 25 26 } from "@openstatus/ui"; 26 27 ··· 72 73 <Link href={`./notifications/${notification.id}/edit`}> 73 74 <DropdownMenuItem>Edit</DropdownMenuItem> 74 75 </Link> 76 + <DropdownMenuSeparator /> 75 77 <AlertDialogTrigger asChild> 76 78 <DropdownMenuItem className="text-destructive focus:bg-destructive focus:text-background"> 77 79 Delete
+40 -1
apps/web/src/components/data-table/status-page/columns.tsx
··· 5 5 import Link from "next/link"; 6 6 import * as z from "zod"; 7 7 8 - import type { Maintenance, Page } from "@openstatus/db/src/schema"; 8 + import type { 9 + Maintenance, 10 + Page, 11 + StatusReport, 12 + StatusReportUpdate, 13 + } from "@openstatus/db/src/schema"; 9 14 import { 10 15 Tooltip, 11 16 TooltipContent, ··· 13 18 TooltipTrigger, 14 19 } from "@openstatus/ui"; 15 20 21 + import { formatDate } from "@/lib/utils"; 16 22 import { ArrowUpRight, Check } from "lucide-react"; 17 23 import { DataTableBadges } from "../data-table-badges"; 18 24 import { DataTableRowActions } from "./data-table-row-actions"; ··· 21 27 Page & { 22 28 monitorsToPages: { monitor: { name: string } }[]; 23 29 maintenancesToPages: Maintenance[]; // we get only the active maintenances! 30 + statusReports: (StatusReport & { 31 + statusReportUpdates: StatusReportUpdate[]; 32 + })[]; 24 33 } 25 34 >[] = [ 26 35 { ··· 82 91 <DataTableBadges 83 92 names={monitors.map((monitor) => monitor.monitor.name)} 84 93 /> 94 + ); 95 + }, 96 + }, 97 + { 98 + accessorKey: "statusReports", 99 + header: "Last Report", 100 + cell: ({ row }) => { 101 + const lastReport = row.original.statusReports?.[0]; 102 + 103 + if (!lastReport) { 104 + return <span className="text-muted-foreground/50">-</span>; 105 + } 106 + 107 + const date = 108 + lastReport.statusReportUpdates?.[0].date || lastReport.updatedAt; 109 + 110 + return ( 111 + <div className="group relative"> 112 + <span className="group-hover:text-muted-foreground/70"> 113 + {formatDate(date)} 114 + </span> 115 + <div className="absolute -inset-x-2 -inset-y-1 invisible group-hover:visible backdrop-blur-sm flex items-center px-2 py-1"> 116 + <Link 117 + href={`./status-pages/${row.original.id}/reports/${lastReport.id}`} 118 + className="hover:underline" 119 + > 120 + Go to report 121 + </Link> 122 + </div> 123 + </div> 85 124 ); 86 125 }, 87 126 },
+6
apps/web/src/components/data-table/status-page/data-table-row-actions.tsx
··· 21 21 DropdownMenu, 22 22 DropdownMenuContent, 23 23 DropdownMenuItem, 24 + DropdownMenuSeparator, 24 25 DropdownMenuTrigger, 25 26 } from "@openstatus/ui"; 26 27 ··· 80 81 > 81 82 <DropdownMenuItem>Visit</DropdownMenuItem> 82 83 </Link> 84 + <DropdownMenuSeparator /> 85 + <Link href={`./status-pages/${page.id}/reports/new`}> 86 + <DropdownMenuItem>Create Report</DropdownMenuItem> 87 + </Link> 88 + <DropdownMenuSeparator /> 83 89 <AlertDialogTrigger asChild> 84 90 <DropdownMenuItem className="text-destructive focus:bg-destructive focus:text-background"> 85 91 Delete
+9
packages/api/src/router/page.ts
··· 224 224 gte(maintenance.to, new Date()), 225 225 ), 226 226 }, 227 + statusReports: { 228 + orderBy: (reports, { desc }) => desc(reports.updatedAt), 229 + with: { 230 + statusReportUpdates: { 231 + orderBy: (updates, { desc }) => desc(updates.date), 232 + }, 233 + }, 234 + }, 227 235 }, 228 236 }); 237 + console.log(allPages.map((page) => page.statusReports)); 229 238 return z.array(selectPageSchemaWithMonitorsRelation).parse(allPages); 230 239 }), 231 240
+2
packages/db/src/schema/pages/page.ts
··· 3 3 4 4 import { maintenance } from "../maintenances"; 5 5 import { monitorsToPages } from "../monitors"; 6 + import { statusReport } from "../status_reports"; 6 7 import { workspace } from "../workspaces"; 7 8 8 9 export const page = sqliteTable("page", { ··· 43 44 export const pageRelations = relations(page, ({ many, one }) => ({ 44 45 monitorsToPages: many(monitorsToPages), 45 46 maintenancesToPages: many(maintenance), 47 + statusReports: many(statusReport), 46 48 workspace: one(workspace, { 47 49 fields: [page.workspaceId], 48 50 references: [workspace.id],
+4
packages/db/src/schema/shared.ts
··· 59 59 }), 60 60 ), 61 61 maintenancesToPages: selectMaintenanceSchema.array().default([]), 62 + statusReports: selectStatusReportSchema 63 + .extend({ statusReportUpdates: selectStatusReportUpdateSchema.array() }) 64 + .array() 65 + .default([]), 62 66 }); 63 67 64 68 export const selectPublicPageSchemaWithRelation = selectPageSchema
+4 -2
packages/db/src/schema/status_reports/validation.ts
··· 14 14 { 15 15 status: statusReportStatusSchema, 16 16 }, 17 - ); 17 + ).extend({ 18 + date: z.coerce.date().optional().default(new Date()), 19 + }); 18 20 19 21 export const insertStatusReportSchema = createInsertSchema(statusReport, { 20 22 status: statusReportStatusSchema, 21 23 }) 22 24 .extend({ 23 - date: z.date().optional().default(new Date()), 25 + date: z.coerce.date().optional().default(new Date()), 24 26 /** 25 27 * relationship to monitors and pages 26 28 */
+1 -1
packages/emails/src/client.tsx
··· 14 14 public async sendFollowUp(req: { to: string }) { 15 15 if (process.env.NODE_ENV === "development") return; 16 16 17 - const html = await render(<FollowUpEmail />); 18 17 try { 18 + const html = await render(<FollowUpEmail />); 19 19 const result = await this.client.emails.send({ 20 20 from: "Thibault Le Ouay Ducasse <thibault@openstatus.dev>", 21 21 subject: "How's it going with OpenStatus?",