Openstatus www.openstatus.dev

chore: feedback probo (#1525)

* chore: improved tz details and default value

* fix: feed

* fix: checker regions

authored by

Maximilian Kaske and committed by
GitHub
7aefd432 675b4919

+202 -33
+2 -12
apps/dashboard/src/components/data-table/maintenances/columns.tsx
··· 1 1 "use client"; 2 2 3 3 import { ProcessMessage } from "@/components/content/process-message"; 4 + import { TableCellDate } from "@/components/data-table/table-cell-date"; 4 5 import { TableCellNumber } from "@/components/data-table/table-cell-number"; 5 6 import { DataTableColumnHeader } from "@/components/ui/data-table/data-table-column-header"; 6 7 import type { RouterOutputs } from "@openstatus/api"; ··· 39 40 header: ({ column }) => ( 40 41 <DataTableColumnHeader column={column} title="Start Date" /> 41 42 ), 42 - cell: ({ row }) => { 43 - const value = row.getValue("from"); 44 - if (value instanceof Date) { 45 - return ( 46 - <div className="text-muted-foreground">{value.toLocaleString()}</div> 47 - ); 48 - } 49 - if (typeof value === "string") { 50 - return <div className="text-muted-foreground">{value}</div>; 51 - } 52 - return <div className="text-muted-foreground">-</div>; 53 - }, 43 + cell: ({ row }) => <TableCellDate value={row.getValue("from")} />, 54 44 enableHiding: false, 55 45 }, 56 46 {
+2 -1
apps/dashboard/src/components/data-table/status-report-updates/data-table.tsx
··· 1 1 "use client"; 2 2 3 3 import { ProcessMessage } from "@/components/content/process-message"; 4 + import { TableCellDate } from "@/components/data-table/table-cell-date"; 4 5 import { FormSheetStatusReportUpdate } from "@/components/forms/status-report-update/sheet"; 5 6 import { Button } from "@/components/ui/button"; 6 7 import { ··· 116 117 </div> 117 118 </TableCell> 118 119 <TableCell className="w-[170px] text-muted-foreground"> 119 - {update.date.toLocaleString()} 120 + <TableCellDate value={update.date} /> 120 121 </TableCell> 121 122 <TableCell className="w-8"> 122 123 <DataTableRowActions row={update} />
+6 -3
apps/dashboard/src/components/data-table/table-cell-date.tsx
··· 1 + import { HoverCardTimestamp } from "@/components/common/hover-card-timestamp"; 1 2 import { cn } from "@/lib/utils"; 2 3 import { format } from "date-fns"; 3 4 ··· 9 10 }: React.ComponentProps<"div"> & { value: unknown; formatStr?: string }) { 10 11 if (value instanceof Date) { 11 12 return ( 12 - <div className={cn("text-muted-foreground", className)} {...props}> 13 - {format(value, formatStr)} 14 - </div> 13 + <HoverCardTimestamp date={value}> 14 + <div className={cn("text-muted-foreground", className)} {...props}> 15 + {format(value, formatStr)} 16 + </div> 17 + </HoverCardTimestamp> 15 18 ); 16 19 } 17 20 if (typeof value === "string") {
+33 -6
apps/dashboard/src/components/forms/maintenance/form.tsx
··· 71 71 onSubmit: (values: FormValues) => Promise<void>; 72 72 }) { 73 73 const mobile = useIsMobile(); 74 + const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; 74 75 const form = useForm<FormValues>({ 75 76 resolver: zodResolver(schema), 76 77 defaultValues: defaultValues ?? { ··· 81 82 monitors: [], 82 83 }, 83 84 }); 85 + const watchEndDate = form.watch("endDate"); 84 86 const watchMessage = form.watch("message"); 85 87 const [isPending, startTransition] = useTransition(); 86 88 const { setIsDirty } = useFormSheetDirty(); ··· 184 186 field.value.getMilliseconds(), 185 187 ); 186 188 field.onChange(newDate); 189 + 190 + // NOTE: if end date is before start date, set it to the same day as the start date 191 + if (watchEndDate && newDate > watchEndDate) { 192 + form.setValue("endDate", newDate); 193 + } 187 194 }} 188 195 initialFocus 189 196 /> 190 197 <div className="border-t p-3"> 191 198 <div className="flex items-center gap-3"> 192 - <Label htmlFor="time" className="text-xs"> 199 + <Label htmlFor="time-start" className="text-xs"> 193 200 Enter time 194 201 </Label> 195 202 <div className="relative grow"> 196 203 <Input 197 - id="time" 204 + id="time-start" 198 205 type="time" 199 206 step="1" 200 207 value={ ··· 234 241 </div> 235 242 </PopoverContent> 236 243 </Popover> 237 - <FormDescription>When the maintenance starts.</FormDescription> 244 + <FormDescription> 245 + When the maintenance starts. Shown in your timezone ( 246 + <code className="font-commit-mono text-foreground/70"> 247 + {timezone} 248 + </code> 249 + ) and saved as Unix time ( 250 + <code className="font-commit-mono text-foreground/70"> 251 + UTC 252 + </code> 253 + ). 254 + </FormDescription> 238 255 <FormMessage /> 239 256 </FormItem> 240 257 )} ··· 293 310 /> 294 311 <div className="border-t p-3"> 295 312 <div className="flex items-center gap-3"> 296 - <Label htmlFor="time" className="text-xs"> 313 + <Label htmlFor="time-end" className="text-xs"> 297 314 Enter time 298 315 </Label> 299 316 <div className="relative grow"> 300 317 <Input 301 - id="time" 318 + id="time-end" 302 319 type="time" 303 320 step="1" 304 321 value={ ··· 338 355 </div> 339 356 </PopoverContent> 340 357 </Popover> 341 - <FormDescription>When the maintenance ends.</FormDescription> 358 + <FormDescription> 359 + When the maintenance ends. Shown in your timezone ( 360 + <code className="font-commit-mono text-foreground/70"> 361 + {timezone} 362 + </code> 363 + ) and saved as Unix time ( 364 + <code className="font-commit-mono text-foreground/70"> 365 + UTC 366 + </code> 367 + ). 368 + </FormDescription> 342 369 <FormMessage /> 343 370 </FormItem> 344 371 )}
+10 -1
apps/dashboard/src/components/forms/status-report-update/form.tsx
··· 63 63 onSubmit: (values: FormValues) => Promise<void>; 64 64 }) { 65 65 const mobile = useIsMobile(); 66 + const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; 66 67 const form = useForm<FormValues>({ 67 68 resolver: zodResolver(schema), 68 69 defaultValues: defaultValues ?? { ··· 245 246 </PopoverContent> 246 247 </Popover> 247 248 <FormDescription> 248 - When the status report was created. 249 + When the status report was created. Shown in your timezone ( 250 + <code className="font-commit-mono text-foreground/70"> 251 + {timezone} 252 + </code> 253 + ) and saved as Unix time ( 254 + <code className="font-commit-mono text-foreground/70"> 255 + UTC 256 + </code> 257 + ). 249 258 </FormDescription> 250 259 <FormMessage /> 251 260 </FormItem>
+11 -1
apps/dashboard/src/components/forms/status-report/form.tsx
··· 80 80 monitors: { id: number; name: string }[]; 81 81 }) { 82 82 const mobile = useIsMobile(); 83 + const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; 83 84 const form = useForm<FormValues>({ 84 85 resolver: zodResolver(defaultValues ? updateSchema : schema), 85 86 defaultValues: defaultValues ?? { ··· 284 285 </PopoverContent> 285 286 </Popover> 286 287 <FormDescription> 287 - When the status report was created. 288 + When the status report was created. Shown in your timezone 289 + ( 290 + <code className="font-commit-mono text-foreground/70"> 291 + {timezone} 292 + </code> 293 + ) and saved as Unix time ( 294 + <code className="font-commit-mono text-foreground/70"> 295 + UTC 296 + </code> 297 + ). 288 298 </FormDescription> 289 299 <FormMessage /> 290 300 </FormItem>
+1
apps/status-page/package.json
··· 75 75 "clsx": "2.1.1", 76 76 "cmdk": "1.1.1", 77 77 "date-fns": "4.1.0", 78 + "feed": "4.2.2", 78 79 "lucide-react": "0.525.0", 79 80 "next": "15.5.3", 80 81 "next-auth": "5.0.0-beta.29",
+102
apps/status-page/src/app/(status-page)/[domain]/(public)/feed/[type]/route.ts
··· 1 + import { getBaseUrl } from "@/lib/base-url"; 2 + import { getQueryClient, trpc } from "@/lib/trpc/server"; 3 + import { Feed } from "feed"; 4 + import { notFound, unauthorized } from "next/navigation"; 5 + 6 + const STATUS_LABELS = { 7 + investigating: "Investigating", 8 + identified: "Identified", 9 + monitoring: "Monitoring", 10 + resolved: "Resolved", 11 + maintenance: "Maintenance", 12 + } as const; 13 + 14 + export const revalidate = 60; 15 + 16 + export async function GET( 17 + _request: Request, 18 + props: { params: Promise<{ domain: string; type: string }> }, 19 + ) { 20 + const queryClient = getQueryClient(); 21 + const { domain, type } = await props.params; 22 + if (!["rss", "atom"].includes(type)) return notFound(); 23 + 24 + const page = await queryClient.fetchQuery( 25 + trpc.page.getPageBySlug.queryOptions({ slug: domain }), 26 + ); 27 + if (!page) return notFound(); 28 + if (page.passwordProtected) return unauthorized(); 29 + 30 + const baseUrl = getBaseUrl({ 31 + slug: page.slug, 32 + customDomain: page.customDomain, 33 + }); 34 + 35 + const feed = new Feed({ 36 + id: `${baseUrl}/feed/${type}`, 37 + title: page.title, 38 + description: page.description, 39 + generator: "OpenStatus - Status Page Updates", 40 + feedLinks: { 41 + rss: `${baseUrl}/feed/rss`, 42 + atom: `${baseUrl}/feed/atom`, 43 + }, 44 + link: baseUrl, 45 + author: { 46 + name: page.title, 47 + email: 48 + page.contactUrl?.startsWith("mailto:") && page.contactUrl !== null 49 + ? page.contactUrl.slice(7) 50 + : undefined, 51 + link: page.homepageUrl || baseUrl, 52 + }, 53 + copyright: `Copyright ${new Date() 54 + .getFullYear() 55 + .toString()} openstatus.dev`, 56 + language: "en-US", 57 + updated: new Date(), 58 + ttl: 60, 59 + }); 60 + 61 + for (const maintenance of page.maintenances) { 62 + const maintenanceUrl = `${baseUrl}/events/maintenance/${maintenance.id}`; 63 + feed.addItem({ 64 + id: maintenanceUrl, 65 + title: `Maintenance - ${maintenance.title}`, 66 + link: maintenanceUrl, 67 + description: maintenance.message, 68 + date: maintenance.updatedAt ?? maintenance.createdAt ?? new Date(), 69 + }); 70 + } 71 + 72 + for (const statusReport of page.statusReports) { 73 + const statusReportUrl = `${baseUrl}/events/report/${statusReport.id}`; 74 + const status = STATUS_LABELS[statusReport.status]; 75 + const statusReportUpdates = statusReport.statusReportUpdates 76 + .map((update) => { 77 + const updateStatus = STATUS_LABELS[update.status]; 78 + return `${updateStatus}: ${update.message}.`; 79 + }) 80 + .join("\n\n"); 81 + 82 + feed.addItem({ 83 + id: statusReportUrl, 84 + title: `${status} - ${statusReport.title}`, 85 + link: statusReportUrl, 86 + description: statusReportUpdates, 87 + date: statusReport.updatedAt ?? statusReport.createdAt ?? new Date(), 88 + }); 89 + } 90 + 91 + feed.items.sort( 92 + (a, b) => (a.date as Date).getTime() - (b.date as Date).getTime(), 93 + ); 94 + 95 + const res = type === "atom" ? feed.atom1() : feed.rss2(); 96 + 97 + return new Response(res, { 98 + headers: { 99 + "Content-Type": "application/xml; charset=utf-8", 100 + }, 101 + }); 102 + }
+1 -1
apps/status-page/src/components/nav/header.tsx
··· 118 118 onSubscribe={async (email) => { 119 119 await subscribeMutation.mutateAsync({ slug: domain, email }); 120 120 }} 121 - slug={page?.slug} 121 + page={page} 122 122 /> 123 123 <NavMobile className="md:hidden" /> 124 124 </div>
+13 -5
apps/status-page/src/components/status-page/status-updates.tsx
··· 11 11 import { Separator } from "@/components/ui/separator"; 12 12 import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; 13 13 import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard"; 14 + import { getBaseUrl } from "@/lib/base-url"; 14 15 import { cn } from "@/lib/utils"; 16 + import type { RouterOutputs } from "@openstatus/api"; 15 17 import { Check, Copy, Inbox } from "lucide-react"; 16 18 import { useState } from "react"; 17 19 18 20 type StatusUpdateType = "email" | "rss" | "ssh"; 19 21 22 + type Page = NonNullable<RouterOutputs["statusPage"]["get"]>; 23 + 20 24 // TODO: use domain instead of openstatus subdomain if available 21 25 22 26 interface StatusUpdatesProps extends React.ComponentProps<typeof Button> { 23 27 types?: StatusUpdateType[]; 24 - slug?: string; 28 + page?: Page | null; 25 29 onSubscribe?: (value: string) => Promise<void> | void; 26 30 } 27 31 28 32 export function StatusUpdates({ 29 33 className, 30 34 types = ["rss", "ssh"], 31 - slug, 35 + page, 32 36 onSubscribe, 33 37 ...props 34 38 }: StatusUpdatesProps) { 35 39 const [success, setSuccess] = useState(false); 40 + const baseUrl = getBaseUrl({ 41 + slug: page?.slug, 42 + customDomain: page?.customDomain, 43 + }); 36 44 37 45 return ( 38 46 <Popover> ··· 91 99 <CopyInputButton 92 100 className="w-full" 93 101 id="rss" 94 - value={`https://${slug}.openstatus.dev/feed/rss`} 102 + value={`${baseUrl}/feed/rss`} 95 103 /> 96 104 </div> 97 105 <Separator /> ··· 100 108 <CopyInputButton 101 109 className="w-full" 102 110 id="atom" 103 - value={`https://${slug}.openstatus.dev/feed/atom`} 111 + value={`${baseUrl}/feed/atom`} 104 112 /> 105 113 </div> 106 114 </TabsContent> ··· 110 118 <CopyInputButton 111 119 className="w-full" 112 120 id="ssh" 113 - value={`ssh ${slug}@ssh.openstatus.dev`} 121 + value={`ssh ${page?.slug}@ssh.openstatus.dev`} 114 122 /> 115 123 </div> 116 124 </TabsContent>
+15
apps/status-page/src/lib/base-url.ts
··· 1 + export function getBaseUrl({ 2 + slug, 3 + customDomain, 4 + }: { 5 + slug?: string; 6 + customDomain?: string; 7 + }) { 8 + if (process.env.NODE_ENV === "development") { 9 + return `http://localhost:3000/${slug}`; 10 + } 11 + if (customDomain) { 12 + return `https://${customDomain}`; 13 + } 14 + return `https://${slug}.openstatus.dev`; 15 + }
+3 -3
apps/web/src/app/(pages)/(content)/play/checker/_components/checker-form.tsx
··· 45 45 } from "@/components/ping-response-analysis/utils"; 46 46 import { toast } from "@/lib/toast"; 47 47 import { notEmpty } from "@/lib/utils"; 48 - import { monitorRegions } from "@openstatus/db/src/schema/constants"; 48 + import { AVAILABLE_REGIONS } from "@openstatus/regions"; 49 49 import { regionDict } from "@openstatus/regions"; 50 50 import { ArrowRight, ChevronRight, Gauge, Info, Loader } from "lucide-react"; 51 51 import dynamic from "next/dynamic"; ··· 311 311 <p className="w-[95px]"> 312 312 Region{" "} 313 313 <span className="font-normal text-xs tabular-nums"> 314 - ({result.length}/{monitorRegions.length}) 314 + ({result.length}/{AVAILABLE_REGIONS.length}) 315 315 </span> 316 316 </p> 317 317 {loading ? ( ··· 322 322 {id && 323 323 !loading && 324 324 result.length > 0 && 325 - result.length !== monitorRegions.length ? ( 325 + result.length !== AVAILABLE_REGIONS.length ? ( 326 326 <TooltipProvider> 327 327 <Tooltip> 328 328 <TooltipTrigger>
+3
pnpm-lock.yaml
··· 702 702 date-fns: 703 703 specifier: 4.1.0 704 704 version: 4.1.0 705 + feed: 706 + specifier: 4.2.2 707 + version: 4.2.2 705 708 lucide-react: 706 709 specifier: 0.525.0 707 710 version: 0.525.0(react@19.2.0)