Openstatus www.openstatus.dev

fix: utc time status page (#904)

* fix: utc time tracker

* feat: utc timezone tooltip

* chore: add tooltip with timezone

* chore: datetime tooltip on status reports

authored by

Maximilian Kaske and committed by
GitHub
d592d22f f4f018cb

+91 -31
+43
apps/web/src/components/status-page/datetime-tooltip.tsx
··· 1 + "use client"; 2 + 3 + import { cn } from "@/lib/utils"; 4 + import { 5 + Tooltip, 6 + TooltipContent, 7 + TooltipProvider, 8 + TooltipTrigger, 9 + } from "@openstatus/ui"; 10 + import { format } from "date-fns"; 11 + import { formatInTimeZone } from "date-fns-tz"; 12 + import { useState } from "react"; 13 + 14 + export function DateTimeTooltip({ 15 + date = new Date(), 16 + className, 17 + }: { 18 + date?: Date; 19 + className?: string; 20 + }) { 21 + const [open, setOpen] = useState(false); 22 + return ( 23 + <TooltipProvider> 24 + <Tooltip open={open} onOpenChange={setOpen}> 25 + <TooltipTrigger 26 + onClick={() => setOpen(false)} 27 + className={cn( 28 + "text-muted-foreground underline decoration-muted-foreground/30 decoration-dashed underline-offset-4", 29 + className 30 + )} 31 + asChild 32 + > 33 + <span>{formatInTimeZone(date, "UTC", "LLL dd, y HH:mm (z)")}</span> 34 + </TooltipTrigger> 35 + <TooltipContent> 36 + <p className="text-muted-foreground text-xs"> 37 + {format(date, "LLL dd, y HH:mm (z)")} 38 + </p> 39 + </TooltipContent> 40 + </Tooltip> 41 + </TooltipProvider> 42 + ); 43 + }
+8 -7
apps/web/src/components/status-page/status-check.tsx
··· 6 6 import type { StatusVariant } from "@openstatus/tracker"; 7 7 import { Tracker } from "@openstatus/tracker"; 8 8 9 - import { getServerTimezoneFormat } from "@/lib/timezone"; 10 9 import { cn } from "@/lib/utils"; 11 10 import { Icons } from "../icons"; 11 + import { DateTimeTooltip } from "./datetime-tooltip"; 12 12 13 13 export async function StatusCheck({ 14 14 statusReports, ··· 22 22 const tracker = new Tracker({ statusReports, incidents, maintenances }); 23 23 const className = tracker.currentClassName; 24 24 const details = tracker.currentDetails; 25 - 26 - const formattedServerDate = getServerTimezoneFormat(); 27 25 28 26 return ( 29 27 <div className="flex flex-col items-center gap-3"> ··· 33 31 <StatusIcon variant={details.variant} /> 34 32 </span> 35 33 </div> 36 - <p className="text-muted-foreground text-xs"> 37 - Status Check <span className="text-muted-foreground/50 text-xs">•</span>{" "} 38 - {formattedServerDate} 39 - </p> 34 + <div className="flex flex-wrap gap-2"> 35 + <p className="text-muted-foreground text-xs">Status Check</p> 36 + <span className="text-muted-foreground/50 text-xs">•</span>{" "} 37 + <p className="text-xs"> 38 + <DateTimeTooltip date={new Date()} /> 39 + </p> 40 + </div> 40 41 </div> 41 42 ); 42 43 }
+10 -7
apps/web/src/components/status-page/status-report.tsx
··· 1 1 "use client"; 2 2 3 - import { format } from "date-fns"; 4 3 import { ChevronRight } from "lucide-react"; 5 4 import Link from "next/link"; 6 5 import { useParams } from "next/navigation"; ··· 16 15 import { cn } from "@/lib/utils"; 17 16 import { StatusBadge } from "../status-update/status-badge"; 18 17 import { ProcessMessage } from "./process-message"; 18 + import { DateTimeTooltip } from "./datetime-tooltip"; 19 + import { format } from "date-fns"; 20 + import { formatInTimeZone } from "date-fns-tz"; 19 21 20 22 function StatusReport({ 21 23 report, ··· 65 67 }) { 66 68 const firstReport = 67 69 report.statusReportUpdates[report.statusReportUpdates.length - 1]; 70 + 68 71 return ( 69 72 <div className={cn("flex flex-wrap items-center gap-2", className)}> 70 - <p className="text-muted-foreground"> 71 - {format(firstReport.date || new Date(), "LLL dd, y HH:mm")} 73 + <p className="text-muted-foreground text-sm"> 74 + Started at <DateTimeTooltip date={firstReport.date} /> 72 75 </p> 73 76 <span className="text-muted-foreground/50 text-xs">•</span> 74 77 <StatusBadge status={report.status} /> ··· 89 92 // reports are already `orderBy: desc(report.date)` within the query itself 90 93 function StatusReportUpdates({ report }: { report: StatusReportWithUpdates }) { 91 94 return ( 92 - <div className="grid gap-4 md:grid-cols-4"> 95 + <div className="grid gap-2 md:grid-cols-10 md:gap-4"> 93 96 {report.statusReportUpdates.map((update) => { 94 97 return ( 95 98 <Fragment key={update.id}> 96 - <div className="flex items-center gap-2 md:col-span-1 md:flex-col md:items-start md:gap-1"> 99 + <div className="flex items-center gap-2 md:col-span-3 md:flex-col md:items-start md:gap-1"> 97 100 <p className="font-medium capitalize">{update.status}</p> 98 101 <p className="font-mono text-muted-foreground text-sm md:text-xs"> 99 - {format(update.date, "LLL dd, y HH:mm")} 102 + <DateTimeTooltip date={update.date} /> 100 103 </p> 101 104 </div> 102 - <div className="prose dark:prose-invert md:col-span-3"> 105 + <div className="prose dark:prose-invert md:col-span-7"> 103 106 <ProcessMessage value={update.message} /> 104 107 </div> 105 108 </Fragment>
+9 -4
apps/web/src/components/tracker/tracker.tsx
··· 1 1 "use client"; 2 2 3 3 import { cva } from "class-variance-authority"; 4 - import { endOfDay, format, formatDuration, startOfDay } from "date-fns"; 4 + import { format, formatDuration } from "date-fns"; 5 5 import { ChevronRight, Info } from "lucide-react"; 6 6 import Link from "next/link"; 7 7 import * as React from "react"; ··· 13 13 StatusReportUpdate, 14 14 } from "@openstatus/db/src/schema"; 15 15 import type { Monitor } from "@openstatus/tinybird"; 16 - import { Tracker as OSTracker, classNames } from "@openstatus/tracker"; 16 + import { 17 + Tracker as OSTracker, 18 + classNames, 19 + endOfDay, 20 + startOfDay, 21 + } from "@openstatus/tracker"; 17 22 import { 18 23 HoverCard, 19 24 HoverCardContent, ··· 150 155 <div 151 156 className={cn( 152 157 rootClassName, 153 - "h-auto w-1 flex-none rounded-full", 158 + "h-auto w-1 flex-none rounded-full" 154 159 )} 155 160 /> 156 161 <div className="grid flex-1 gap-1"> ··· 245 250 Down for{" "} 246 251 {formatDuration( 247 252 { minutes, hours, days }, 248 - { format: ["days", "hours", "minutes", "seconds"], zero: false }, 253 + { format: ["days", "hours", "minutes", "seconds"], zero: false } 249 254 )} 250 255 </p> 251 256 );
+8 -1
apps/web/src/lib/timezone.ts
··· 34 34 return `Etc/GMT${offset}` as const; 35 35 } 36 36 37 + export function getServerTimezoneUTCFormat() { 38 + const now = new Date(); 39 + const now_utc = new Date(now.toUTCString().slice(0, -4)); // remove the GMT end 40 + 41 + return format(now_utc, "LLL dd, y HH:mm:ss (z)", { timeZone: "UTC" }); 42 + } 43 + 37 44 export function getServerTimezoneFormat() { 38 - return format(new Date(), "LLL dd, y HH:mm:ss zzz", { timeZone: "UTC" }); 45 + return format(new Date(), "LLL dd, y HH:mm:ss (z)"); 39 46 } 40 47 41 48 export function formatDate(date: Date) {
+1
packages/tracker/src/index.ts
··· 1 1 export * from "./tracker"; 2 2 export * from "./types"; 3 3 export * from "./config"; 4 + export * from "./utils";
+12 -12
packages/tracker/src/utils.ts
··· 1 1 export function endOfDay(date: Date): Date { 2 2 // Create a new Date object to avoid mutating the original date 3 - const newDate = new Date(date); 3 + // const newDate = new Date(date); 4 + const newDate = new Date( 5 + Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()), 6 + ); 4 7 5 8 // Set hours, minutes, seconds, and milliseconds to end of day 6 - newDate.setHours(23, 59, 59, 999); 9 + newDate.setUTCHours(23, 59, 59, 999); 7 10 8 11 return newDate; 9 12 } 10 13 11 14 export function startOfDay(date: Date): Date { 12 15 // Create a new Date object to avoid mutating the original date 13 - const newDate = new Date(date); 16 + // const newDate = new Date(date); 17 + const newDate = new Date( 18 + Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()), 19 + ); 14 20 15 21 // Set hours, minutes, seconds, and milliseconds to start of day 16 - newDate.setHours(0, 0, 0, 0); 22 + newDate.setUTCHours(0, 0, 0, 0); 17 23 18 24 return newDate; 19 25 } 20 26 21 27 export function isSameDay(date1: Date, date2: Date) { 22 - const newDate1 = new Date(date1); 23 - const newDate2 = new Date(date2); 24 - 25 - newDate1.setDate(newDate1.getDate()); 26 - newDate1.setHours(0, 0, 0, 0); 27 - 28 - newDate2.setDate(newDate2.getDate()); 29 - newDate2.setHours(0, 0, 0, 0); 28 + const newDate1 = startOfDay(date1); 29 + const newDate2 = startOfDay(date2); 30 30 31 31 return newDate1.toUTCString() === newDate2.toUTCString(); 32 32 }