Openstatus www.openstatus.dev

chore: apply feedback (#1297)

* chore: apply feedback

* fix: end date before start date

authored by

Maximilian Kaske and committed by
GitHub
ab17c980 02248bcd

+223 -25
+6 -6
apps/dashboard/src/app/(dashboard)/monitors/[id]/overview/client.tsx
··· 146 146 </Section> 147 147 <Section> 148 148 <SectionHeader> 149 - <SectionTitle>Timeline</SectionTitle> 149 + <SectionTitle>Regions</SectionTitle> 150 150 <SectionDescription> 151 - What happened to your monitor over the last {TIMELINE_INTERVAL} days 151 + Every region&apos;s latency over the last 24 hours 152 152 </SectionDescription> 153 153 </SectionHeader> 154 - <AuditLogsWrapper monitorId={id} interval={TIMELINE_INTERVAL} /> 154 + <DataTable data={regionMetrics} columns={regionColumns} /> 155 155 </Section> 156 156 <Section> 157 157 <SectionHeader> 158 - <SectionTitle>Regions</SectionTitle> 158 + <SectionTitle>Timeline</SectionTitle> 159 159 <SectionDescription> 160 - Every region&apos;s latency over the last 24 hours 160 + What happened to your monitor over the last {TIMELINE_INTERVAL} days 161 161 </SectionDescription> 162 162 </SectionHeader> 163 - <DataTable data={regionMetrics} columns={regionColumns} /> 163 + <AuditLogsWrapper monitorId={id} interval={TIMELINE_INTERVAL} /> 164 164 </Section> 165 165 </SectionGroup> 166 166 );
+8 -3
apps/dashboard/src/app/(dashboard)/status-pages/[id]/maintenances/page.tsx
··· 1 1 "use client"; 2 2 3 + import { Link } from "@/components/common/link"; 3 4 import { 4 5 Section, 5 6 SectionDescription, ··· 41 42 <Section> 42 43 <SectionHeaderRow> 43 44 <SectionHeader> 44 - <SectionTitle>Maintenances</SectionTitle> 45 + <SectionTitle>{statusPage.title}</SectionTitle> 45 46 <SectionDescription> 46 - See our maintenances and scheduled downtimes. 47 + List of all maintenances. Looking for{" "} 48 + <Link href={`/status-pages/${id}/status-reports`}> 49 + status reports 50 + </Link> 51 + ? 47 52 </SectionDescription> 48 53 </SectionHeader> 49 54 <div> ··· 60 65 }); 61 66 }} 62 67 > 63 - <Button data-section="action" size="sm" variant="ghost"> 68 + <Button data-section="action" size="sm"> 64 69 <Plus /> 65 70 Create Maintenance 66 71 </Button>
+6 -1
apps/dashboard/src/app/(dashboard)/status-pages/[id]/status-reports/page.tsx
··· 1 1 "use client"; 2 2 3 + import { Link } from "@/components/common/link"; 3 4 import { 4 5 Section, 5 6 SectionDescription, ··· 59 60 <SectionHeader> 60 61 <SectionTitle>{page.title}</SectionTitle> 61 62 <SectionDescription> 62 - See our uptime history and status reports. 63 + List of all status reports. Looking for{" "} 64 + <Link href={`/status-pages/${id}/maintenances`}> 65 + maintenances 66 + </Link> 67 + ? 63 68 </SectionDescription> 64 69 </SectionHeader> 65 70 <div>
+1 -3
apps/dashboard/src/app/(dashboard)/status-pages/[id]/subscribers/page.tsx
··· 68 68 <Section> 69 69 <SectionHeader> 70 70 <SectionTitle>{page?.title}</SectionTitle> 71 - <SectionDescription> 72 - Allow your users to subscribe to status page updates. 73 - </SectionDescription> 71 + <SectionDescription>List of all subscribers.</SectionDescription> 74 72 </SectionHeader> 75 73 </Section> 76 74 <Section>
+3 -1
apps/dashboard/src/components/data-table/settings/members/data-table.tsx
··· 45 45 </TableCell> 46 46 <TableCell>{item.user.email}</TableCell> 47 47 <TableCell>{item.role}</TableCell> 48 - <TableCell>{formatDate(item.createdAt)}</TableCell> 48 + <TableCell> 49 + {formatDate(item.user.createdAt ?? item.createdAt)} 50 + </TableCell> 49 51 <TableCell> 50 52 <div className="flex justify-end"> 51 53 <QuickActions
+1 -1
apps/dashboard/src/components/data-table/status-reports/data-table-row-actions.tsx
··· 26 26 "view-report": () => { 27 27 if (typeof window !== "undefined") { 28 28 window.open( 29 - `https://${row.original.page.customDomain || `${row.original.page.slug}.openstatus.dev/events/report/${row.original.id}`}`, 29 + `https://${row.original.page.customDomain || `${row.original.page.slug}.openstatus.dev`}/events/report/${row.original.id}`, 30 30 "_blank", 31 31 ); 32 32 }
+15 -10
apps/dashboard/src/components/forms/maintenance/form.tsx
··· 34 34 import { cn } from "@/lib/utils"; 35 35 import { zodResolver } from "@hookform/resolvers/zod"; 36 36 import { isTRPCClientError } from "@trpc/client"; 37 - import { format } from "date-fns"; 37 + import { addDays, format } from "date-fns"; 38 38 import { CalendarIcon, ClockIcon } from "lucide-react"; 39 39 import type React from "react"; 40 40 import { useTransition } from "react"; ··· 42 42 import { toast } from "sonner"; 43 43 import { z } from "zod"; 44 44 45 - const schema = z.object({ 46 - title: z.string(), 47 - message: z.string(), 48 - startDate: z.date(), 49 - endDate: z.date(), 50 - monitors: z.array(z.number()), 51 - }); 45 + const schema = z 46 + .object({ 47 + title: z.string().min(1, "Title is required"), 48 + message: z.string(), 49 + startDate: z.date(), 50 + endDate: z.date(), 51 + monitors: z.array(z.number()), 52 + }) 53 + .refine((data) => data.endDate > data.startDate, { 54 + message: "End date cannot be earlier than start date.", 55 + path: ["endDate"], 56 + }); 52 57 53 58 export type FormValues = z.infer<typeof schema>; 54 59 ··· 69 74 title: "", 70 75 message: "", 71 76 startDate: new Date(), 72 - endDate: new Date(), 77 + endDate: addDays(new Date(), 1), 73 78 monitors: [], 74 79 }, 75 80 }); ··· 276 281 </div> 277 282 </PopoverContent> 278 283 </Popover> 279 - <FormDescription>When the maintenance ends</FormDescription> 284 + <FormDescription>When the maintenance ends.</FormDescription> 280 285 <FormMessage /> 281 286 </FormItem> 282 287 )}
+144
apps/dashboard/src/components/forms/status-page/form-appearance.tsx
··· 1 + import { useTransition } from "react"; 2 + import { z } from "zod"; 3 + 4 + import { 5 + FormCard, 6 + FormCardContent, 7 + FormCardDescription, 8 + FormCardFooter, 9 + FormCardFooterInfo, 10 + FormCardHeader, 11 + FormCardTitle, 12 + } from "@/components/forms/form-card"; 13 + import { Button } from "@/components/ui/button"; 14 + import { 15 + Form, 16 + FormControl, 17 + FormField, 18 + FormItem, 19 + FormLabel, 20 + FormMessage, 21 + } from "@/components/ui/form"; 22 + import { 23 + Select, 24 + SelectContent, 25 + SelectItem, 26 + SelectTrigger, 27 + SelectValue, 28 + } from "@/components/ui/select"; 29 + import { zodResolver } from "@hookform/resolvers/zod"; 30 + import { isTRPCClientError } from "@trpc/client"; 31 + import { Laptop, Moon, Sun } from "lucide-react"; 32 + import { useForm } from "react-hook-form"; 33 + import { toast } from "sonner"; 34 + 35 + const schema = z.object({ 36 + forceTheme: z.enum(["light", "dark", "system"]), 37 + }); 38 + 39 + type FormValues = z.infer<typeof schema>; 40 + 41 + export function FormAppearance({ 42 + defaultValues, 43 + onSubmit, 44 + }: { 45 + defaultValues?: FormValues; 46 + onSubmit: (values: FormValues) => Promise<void>; 47 + }) { 48 + const [isPending, startTransition] = useTransition(); 49 + const form = useForm<FormValues>({ 50 + resolver: zodResolver(schema), 51 + defaultValues: defaultValues ?? { 52 + forceTheme: "system", 53 + }, 54 + }); 55 + 56 + function submitAction(values: FormValues) { 57 + if (isPending) return; 58 + 59 + startTransition(async () => { 60 + try { 61 + const promise = onSubmit(values); 62 + toast.promise(promise, { 63 + loading: "Saving...", 64 + success: "Saved", 65 + error: (error) => { 66 + if (isTRPCClientError(error)) { 67 + return error.message; 68 + } 69 + return "Failed to save"; 70 + }, 71 + }); 72 + await promise; 73 + } catch (error) { 74 + console.error(error); 75 + } 76 + }); 77 + } 78 + 79 + return ( 80 + <Form {...form}> 81 + <form onSubmit={form.handleSubmit(submitAction)}> 82 + <FormCard> 83 + <FormCardHeader> 84 + <FormCardTitle>Appearance</FormCardTitle> 85 + <FormCardDescription> 86 + Forced theme will override the user&apos;s preference. 87 + </FormCardDescription> 88 + </FormCardHeader> 89 + <FormCardContent> 90 + <FormField 91 + control={form.control} 92 + name="forceTheme" 93 + render={({ field }) => ( 94 + <FormItem> 95 + <FormLabel className="sr-only">Theme</FormLabel> 96 + <Select 97 + onValueChange={field.onChange} 98 + defaultValue={field.value} 99 + > 100 + <FormControl> 101 + <SelectTrigger> 102 + <SelectValue placeholder="Select a theme" /> 103 + </SelectTrigger> 104 + </FormControl> 105 + <SelectContent> 106 + <SelectItem value="light"> 107 + <div className="flex items-center gap-2"> 108 + <Sun className="h-4 w-4" /> 109 + <span>Light</span> 110 + </div> 111 + </SelectItem> 112 + <SelectItem value="dark"> 113 + <div className="flex items-center gap-2"> 114 + <Moon className="h-4 w-4" /> 115 + <span>Dark</span> 116 + </div> 117 + </SelectItem> 118 + <SelectItem value="system"> 119 + <div className="flex items-center gap-2"> 120 + <Laptop className="h-4 w-4" /> 121 + <span>System</span> 122 + </div> 123 + </SelectItem> 124 + </SelectContent> 125 + </Select> 126 + <FormMessage /> 127 + </FormItem> 128 + )} 129 + /> 130 + </FormCardContent> 131 + <FormCardFooter> 132 + <FormCardFooterInfo> 133 + Your user will still be able to change the theme via the theme 134 + toggle. 135 + </FormCardFooterInfo> 136 + <Button type="submit" disabled={isPending}> 137 + {isPending ? "Submitting..." : "Submit"} 138 + </Button> 139 + </FormCardFooter> 140 + </FormCard> 141 + </form> 142 + </Form> 143 + ); 144 + }
+18
apps/dashboard/src/components/forms/status-page/update.tsx
··· 2 2 import { useTRPC } from "@/lib/trpc/client"; 3 3 import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; 4 4 import { useParams, useRouter } from "next/navigation"; 5 + import { FormAppearance } from "./form-appearance"; 5 6 import { FormCustomDomain } from "./form-custom-domain"; 6 7 import { FormDangerZone } from "./form-danger-zone"; 7 8 import { FormGeneral } from "./form-general"; ··· 44 45 45 46 const updateCustomDomainMutation = useMutation( 46 47 trpc.page.updateCustomDomain.mutationOptions({ 48 + onSuccess: () => refetch(), 49 + }), 50 + ); 51 + 52 + const updatePageAppearanceMutation = useMutation( 53 + trpc.page.updateAppearance.mutationOptions({ 47 54 onSuccess: () => refetch(), 48 55 }), 49 56 ); ··· 111 118 await updateCustomDomainMutation.mutateAsync({ 112 119 id: Number.parseInt(id), 113 120 customDomain: values.domain, 121 + }); 122 + }} 123 + /> 124 + <FormAppearance 125 + defaultValues={{ 126 + forceTheme: statusPage.forceTheme ?? "system", 127 + }} 128 + onSubmit={async (values) => { 129 + await updatePageAppearanceMutation.mutateAsync({ 130 + id: Number.parseInt(id), 131 + forceTheme: values.forceTheme, 114 132 }); 115 133 }} 116 134 />
+21
packages/api/src/router/page.ts
··· 733 733 .run(); 734 734 }), 735 735 736 + updateAppearance: protectedProcedure 737 + .meta({ track: Events.UpdatePage }) 738 + .input( 739 + z.object({ 740 + id: z.number(), 741 + forceTheme: z.enum(["light", "dark", "system"]), 742 + }), 743 + ) 744 + .mutation(async (opts) => { 745 + const whereConditions: SQL[] = [ 746 + eq(page.workspaceId, opts.ctx.workspace.id), 747 + eq(page.id, opts.input.id), 748 + ]; 749 + 750 + await opts.ctx.db 751 + .update(page) 752 + .set({ forceTheme: opts.input.forceTheme, updatedAt: new Date() }) 753 + .where(and(...whereConditions)) 754 + .run(); 755 + }), 756 + 736 757 updateMonitors: protectedProcedure 737 758 .meta({ track: Events.UpdatePage }) 738 759 .input(