Openstatus www.openstatus.dev

chore: max status banner updates (#1775)

* chore: read more event timeline link

* fix: id

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* ci: apply automated fixes

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>

authored by

Maximilian Kaske
Copilot
autofix-ci[bot]
and committed by
GitHub
9dccd46e 7e74aa1d

+98 -39
+4 -1
apps/status-page/src/app/(public)/client.tsx
··· 352 352 ))} 353 353 </StatusEventAffected> 354 354 ) : null} 355 - <StatusEventTimelineReport updates={report.statusReportUpdates} /> 355 + <StatusEventTimelineReport 356 + updates={report.statusReportUpdates} 357 + reportId={report.id} 358 + /> 356 359 </StatusEventContent> 357 360 </StatusEvent> 358 361 </Status>
+1
apps/status-page/src/app/(status-page)/[domain]/(public)/events/(list)/page.tsx
··· 88 88 ) : null} 89 89 <StatusEventTimelineReport 90 90 updates={report.statusReportUpdates} 91 + reportId={report.id} 91 92 /> 92 93 </StatusEventContent> 93 94 </Link>
+4 -1
apps/status-page/src/app/(status-page)/[domain]/(public)/events/(view)/report/[id]/page.tsx
··· 68 68 ))} 69 69 </StatusEventAffected> 70 70 ) : null} 71 - <StatusEventTimelineReport updates={report.statusReportUpdates} /> 71 + <StatusEventTimelineReport 72 + updates={report.statusReportUpdates} 73 + reportId={report.id} 74 + /> 72 75 </StatusEventContent> 73 76 </StatusEvent> 74 77 </div>
+2
apps/status-page/src/app/(status-page)/[domain]/(public)/page.tsx
··· 144 144 <StatusBannerContainer status={e.status}> 145 145 <StatusBannerContent> 146 146 <StatusEventTimelineReport 147 + reportId={report.id} 147 148 updates={report.statusReportUpdates} 148 149 withDot={false} 150 + maxUpdates={3} 149 151 /> 150 152 </StatusBannerContent> 151 153 </StatusBannerContainer>
+81 -30
apps/status-page/src/components/status-page/status-events.tsx
··· 8 8 TooltipProvider, 9 9 TooltipTrigger, 10 10 } from "@/components/ui/tooltip"; 11 + import { usePathnamePrefix } from "@/hooks/use-pathname-prefix"; 11 12 import { formatDate, formatDateRange, formatDateTime } from "@/lib/formatter"; 12 13 import { cn } from "@/lib/utils"; 13 14 import { formatDistanceStrict } from "date-fns"; 14 - import { Check } from "lucide-react"; 15 + import { Check, ChevronRight } from "lucide-react"; 16 + import Link from "next/link"; 15 17 import { status } from "./messages"; 16 18 17 19 export function StatusEventGroup({ ··· 171 173 className, 172 174 updates, 173 175 withDot = true, 176 + maxUpdates, 177 + reportId, 174 178 ...props 175 179 }: React.ComponentProps<"div"> & { 176 180 // TODO: remove unused props 181 + reportId: number; 177 182 updates: { 178 183 date: Date; 179 184 message: string; 180 185 status: "investigating" | "identified" | "monitoring" | "resolved"; 181 186 }[]; 182 187 withDot?: boolean; 188 + maxUpdates?: number; 183 189 }) { 190 + const prefix = usePathnamePrefix(); 191 + const sortedUpdates = [...updates].sort( 192 + (a, b) => b.date.getTime() - a.date.getTime(), 193 + ); 194 + const hasMoreUpdates = maxUpdates && sortedUpdates.length > maxUpdates; 195 + const displayedUpdates = maxUpdates 196 + ? sortedUpdates.slice(0, maxUpdates) 197 + : sortedUpdates; 198 + 184 199 return ( 185 200 <div className={cn("text-muted-foreground text-sm", className)} {...props}> 186 201 {/* NOTE: make sure they are sorted by date */} 187 - {updates 188 - .sort((a, b) => b.date.getTime() - a.date.getTime()) 189 - .map((update, index) => { 190 - const updateDate = new Date(update.date); 191 - let durationText: string | undefined; 202 + {displayedUpdates.map((update, index) => { 203 + const updateDate = new Date(update.date); 204 + let durationText: string | undefined; 192 205 193 - if (index === 0) { 194 - const startedAt = new Date(updates[updates.length - 1].date); 195 - const duration = formatDistanceStrict(startedAt, updateDate); 206 + if (index === 0) { 207 + const startedAt = new Date( 208 + sortedUpdates[sortedUpdates.length - 1].date, 209 + ); 210 + const duration = formatDistanceStrict(startedAt, updateDate); 196 211 197 - if (duration !== "0 seconds" && update.status === "resolved") { 198 - durationText = `(in ${duration})`; 199 - } 200 - } else { 201 - const lastUpdateDate = new Date(updates[index - 1].date); 202 - const timeFromLast = formatDistanceStrict( 203 - updateDate, 204 - lastUpdateDate, 205 - ); 206 - durationText = `(${timeFromLast} earlier)`; 212 + if (duration !== "0 seconds" && update.status === "resolved") { 213 + durationText = `(in ${duration})`; 207 214 } 215 + } else { 216 + const lastUpdateDate = new Date(displayedUpdates[index - 1].date); 217 + const timeFromLast = formatDistanceStrict(updateDate, lastUpdateDate); 218 + durationText = `(${timeFromLast} earlier)`; 219 + } 208 220 209 - return ( 210 - <StatusEventTimelineReportUpdate 211 - key={index} 212 - report={update} 213 - duration={durationText} 214 - withSeparator={index !== updates.length - 1} 215 - withDot={withDot} 216 - isLast={index === updates.length - 1} 217 - /> 218 - ); 219 - })} 221 + return ( 222 + <StatusEventTimelineReportUpdate 223 + key={index} 224 + report={update} 225 + duration={durationText} 226 + withSeparator={ 227 + index !== displayedUpdates.length - 1 && !hasMoreUpdates 228 + } 229 + withDot={withDot} 230 + isLast={index === displayedUpdates.length - 1 && !hasMoreUpdates} 231 + /> 232 + ); 233 + })} 234 + {hasMoreUpdates && ( 235 + <StatusEventTimelineReadMore 236 + href={`${prefix ? `/${prefix}` : ""}/events/report/${reportId}`} 237 + withDot={withDot} 238 + /> 239 + )} 220 240 </div> 221 241 ); 222 242 } ··· 272 292 )} 273 293 </StatusEventTimelineMessage> 274 294 </div> 295 + </div> 296 + </div> 297 + </div> 298 + ); 299 + } 300 + 301 + function StatusEventTimelineReadMore({ 302 + href, 303 + withDot = true, 304 + }: { 305 + href: string; 306 + withDot?: boolean; 307 + }) { 308 + return ( 309 + <div className="group"> 310 + <div className="flex flex-row items-center justify-between gap-2"> 311 + <div className="flex flex-row gap-4"> 312 + {withDot ? ( 313 + <div className="flex flex-col"> 314 + <div className="flex h-5 flex-col items-center justify-center"> 315 + <div className="size-2.5 shrink-0 rounded-full bg-muted" /> 316 + </div> 317 + </div> 318 + ) : null} 319 + <Link 320 + href={href} 321 + className="flex items-center gap-1 font-medium text-muted-foreground text-sm hover:text-foreground hover:underline" 322 + > 323 + View full report 324 + <ChevronRight className="size-4" /> 325 + </Link> 275 326 </div> 276 327 </div> 277 328 </div>
+4 -1
apps/status-page/src/components/status-page/status-feed.tsx
··· 126 126 ))} 127 127 </StatusEventAffected> 128 128 )} 129 - <StatusEventTimelineReport updates={report.updates} /> 129 + <StatusEventTimelineReport 130 + updates={report.updates} 131 + reportId={report.id} 132 + /> 130 133 </StatusEventContent> 131 134 </Link> 132 135 </StatusEvent>
+2 -6
apps/workflows/src/incident/index.ts
··· 1 1 import { schema } from "@openstatus/db"; 2 - import { and, eq, inArray, isNotNull, isNull, ne } from "drizzle-orm"; 2 + import { and, eq, inArray, isNull, ne } from "drizzle-orm"; 3 3 import { Hono } from "hono"; 4 4 import { env } from "../env"; 5 5 import { db } from "../lib/db"; ··· 19 19 const unresolvedIncidentMonitorIds = db 20 20 .select({ monitorId: schema.incidentTable.monitorId }) 21 21 .from(schema.incidentTable) 22 - .where( 23 - and( 24 - isNull(schema.incidentTable.resolvedAt), 25 - ), 26 - ); 22 + .where(and(isNull(schema.incidentTable.resolvedAt))); 27 23 28 24 const activeMonitorsWithUnresolvedIncidents = await db 29 25 .select({ id: schema.monitor.id })