"use client"; import { useStatusBlocksLabels } from "@openstatus/ui/components/blocks/status-i18n"; import { StatusTimestamp } from "@openstatus/ui/components/blocks/status-timestamp"; import type { StatusReportImpact, StatusReportUpdate, } from "@openstatus/ui/components/blocks/status.types"; import { worstStatusReportImpact } from "@openstatus/ui/components/blocks/status.utils"; import { Badge } from "@openstatus/ui/components/ui/badge"; import { HoverCard, HoverCardContent, HoverCardTrigger, } from "@openstatus/ui/components/ui/hover-card"; import { Separator } from "@openstatus/ui/components/ui/separator"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@openstatus/ui/components/ui/tooltip"; import { cn } from "@openstatus/ui/lib/utils"; import { Slot } from "@radix-ui/react-slot"; import { formatDistanceStrict } from "date-fns"; import { Check } from "lucide-react"; // ============================================================================ // Container Components // ============================================================================ /** * StatusEventGroup - Root container for status events and incident reports * * Provides a vertical flex container with consistent spacing (gap-4) for * displaying a feed of status events, incident reports, and maintenance notices. * The component includes ARIA role="feed" for accessibility. * * @example * ```tsx * * * // First incident... * * * // Second incident... * * * ``` * * @see StatusEvent - For individual event items */ export function StatusEventGroup({ className, children, ...props }: React.ComponentProps<"div">) { return (
{children}
); } /** * StatusEvent - Individual event container within StatusEventGroup * * Container for a single event (incident, report, or maintenance) with relative * positioning to support absolutely positioned date aside elements. * * @example * ```tsx * * * * * * API Outage * // Event details... * * * ``` */ export function StatusEvent({ className, children, ...props }: React.ComponentProps<"div">) { return (
{children}
); } // ============================================================================ // Content Components // ============================================================================ /** * StatusEventContent - Main content container for event details * * Provides a hoverable container with rounded borders and muted background on hover. * The hoverable behavior can be disabled for non-interactive events. * * @param hoverable - Whether to show hover effects (default: true) * * @example * ```tsx * * Database Maintenance *

Scheduled maintenance from 2-4 AM UTC

*
* ``` */ export function StatusEventContent({ className, hoverable = true, children, ...props }: React.ComponentProps<"div"> & { hoverable?: boolean; }) { return (
{children}
); } /** * StatusEventTitle - Title for status events * * Displays the event title in medium-weight font, typically used for incident * names or maintenance titles. * * @example * ```tsx * API Gateway Outage * ``` */ export function StatusEventTitle({ className, children, ...props }: React.ComponentProps<"div">) { return (
{children}
); } /** * StatusEventTitleCheck - Resolved status indicator with tooltip * * Displays a green check icon in a circular badge with a tooltip explaining * that the report has been resolved. Typically displayed next to event titles. * * @example * ```tsx *
* API Outage * *
* ``` */ export function StatusEventTitleCheck({ className, children, ...props }: React.ComponentProps<"div">) { const labels = useStatusBlocksLabels(); return (

{labels.reportResolved}

); } // ============================================================================ // Affected Services Components // ============================================================================ /** * StatusEventAffected - Container for affected service badges * * Displays a wrapping flex container for StatusEventAffectedBadge components, * showing which services were impacted by an incident. * * @example * ```tsx * * API * Database * CDN * * ``` */ export function StatusEventAffected({ className, children, ...props }: React.ComponentProps<"div">) { return (
{children}
); } /** * StatusEventAffectedBadge - Badge for individual affected service * * Displays a small secondary-style badge representing a single affected service. * Uses a smaller font size (text-[10px]) for compact display. * * @example * ```tsx * REST API * ``` */ export function StatusEventAffectedBadge({ className, children, ...props }: React.ComponentProps<"div">) { return ( {children} ); } // ============================================================================ // Date/Time Components // ============================================================================ /** * StatusEventDate - Date display with relative time badge * * Displays a formatted date with a relative time badge (e.g., "2 days ago"). * For future dates, the badge is highlighted in info color. The layout is * responsive: horizontal on mobile (gap-2), vertical on desktop (flex-col). * * @param date - The event date to display * * @example * ```tsx * * // Displays: "Jan 15, 2024" with "2 days ago" badge * ``` */ export function StatusEventDate({ className, date, ...props }: React.ComponentProps<"div"> & { date: Date; }) { const labels = useStatusBlocksLabels(); const isFuture = date > new Date(); const distance = formatDistanceStrict(date, new Date(), { addSuffix: true }); return (
{labels.formatDateShort(date)}
{" "} {distance}
); } /** * StatusEventAside - Sidebar date container (desktop only) * * Positions the date to the left of event content on desktop screens (lg breakpoint). * On mobile, it appears inline. Uses sticky positioning on desktop to keep dates * visible while scrolling through long events. * * @example * ```tsx * * * * * * // Event content... * * * ``` */ export function StatusEventAside({ className, children, ...props }: React.ComponentProps<"div">) { return (
{children}
); } const impactTextClasses: Record = { operational: "text-success", degraded_performance: "text-warning", partial_outage: "text-warning", major_outage: "text-destructive", }; /** * StatusEventTimelineImpact - Worst impact label for a timeline update * * Shows the most severe impact among the update's component changes. The label * is a hover card trigger listing each component's explicit impact. */ export function StatusEventTimelineImpact({ changes, className, ...props }: React.ComponentProps<"button"> & { changes: { name: string; impact: StatusReportImpact }[]; }) { const labels = useStatusBlocksLabels(); const worst = worstStatusReportImpact(changes.map((c) => c.impact)); return (
{changes.map((change, i) => (
{change.name} {labels.componentImpact[change.impact]}
))}
); } // ============================================================================ // Timeline Components // ============================================================================ /** * StatusEventTimelineReport - Timeline of incident report updates * * Displays a chronological timeline of incident updates, sorted from newest to * oldest. Each update shows the status (investigating → identified → monitoring → resolved), * timestamp, message, and time elapsed between updates. * * **Automatic Duration Calculation**: * - First update (most recent): Shows total time from start to resolution (if resolved) * - Other updates: Shows time elapsed since the previous update * * @param updates - Array of report updates to display * @param withDot - Whether to show colored status dots (default: true) * @param maxUpdates - Maximum number of updates to display (optional, shows all if not specified) * * @example * ```tsx * * // Displays timeline with: "Resolved (in 1 hour)" → "Monitoring (15 minutes earlier)" → etc. * ``` * * @see StatusEventTimelineReportUpdate - For individual update rendering */ export function StatusEventTimelineReport({ className, updates, withDot = true, maxUpdates, renderMessage, ...props }: React.ComponentProps<"div"> & { updates: StatusReportUpdate[]; withDot?: boolean; maxUpdates?: number; renderMessage?: (message: string) => React.ReactNode; }) { const labels = useStatusBlocksLabels(); const sortedUpdates = [...updates].sort( (a, b) => b.date.getTime() - a.date.getTime(), ); const displayedUpdates = maxUpdates ? sortedUpdates.slice(0, maxUpdates) : sortedUpdates; return (
{/* NOTE: make sure they are sorted by date */} {displayedUpdates.map((update, index) => { const updateDate = new Date(update.date); let durationText: string | undefined; if (index === 0) { const startedAt = new Date( sortedUpdates[sortedUpdates.length - 1].date, ); const duration = formatDistanceStrict(startedAt, updateDate); if (duration !== "0 seconds" && update.status === "resolved") { durationText = labels.durationIn(duration); } } else { const lastUpdateDate = new Date(displayedUpdates[index - 1].date); const timeFromLast = formatDistanceStrict(updateDate, lastUpdateDate); durationText = labels.durationEarlier(timeFromLast); } return ( ); })}
); } /** * StatusEventTimelineReportUpdate - Single update entry in incident timeline * * Displays one update in the incident timeline with: * - Colored dot indicator (red=investigating, yellow=identified, blue=monitoring, green=resolved) * - Status label and timestamp (with StatusTimestamp for rich hover details) * - Duration text (e.g., "in 1 hour" or "15 minutes earlier") * - Update message * - Optional vertical separator line connecting to next update * * @param report - The update data (status, message, date) * @param duration - Optional duration text to display * @param withSeparator - Whether to show separator line to next update (default: true) * @param withDot - Whether to show colored status dot (default: true) * @param isLast - Whether this is the last update (affects bottom margin) * * @example * ```tsx * * ``` * * @see StatusEventTimelineDot - For the colored dot indicator * @see StatusEventTimelineSeparator - For the connecting line */ export function StatusEventTimelineReportUpdate({ report, duration, withSeparator = true, withDot = true, isLast = false, renderMessage, }: { report: StatusReportUpdate; withSeparator?: boolean; duration?: string; withDot?: boolean; isLast?: boolean; renderMessage?: (message: string) => React.ReactNode; }) { const labels = useStatusBlocksLabels(); return (
{withDot ? (
{withSeparator ? : null}
) : null}
{labels.incidentStatus[report.status]}{" "} {report.impactChanges?.length ? ( <> ·{" "} {" "} ) : null} ·{" "} {labels.formatDateTime(report.date)} {" "} {duration ? ( {duration} ) : null} {report.message.trim() === "" ? ( - ) : renderMessage ? ( renderMessage(report.message) ) : ( {report.message} )}
); } interface StatusMaintenanceUpdate { title: string; message: string; from: Date; to: Date; } /** * StatusEventTimelineMaintenance - Timeline entry for maintenance windows * * Displays a maintenance window with title, date range, duration, and message. * Uses a blue dot indicator to distinguish from incident updates. * * The date range is formatted and split to allow individual StatusTimestamp * components for each date, providing rich hover details. * * @param maintenance - The maintenance data (title, message, from, to dates) * @param withDot - Whether to show the blue maintenance dot (default: true) * * @example * ```tsx * * // Displays: [●] Database Upgrade · Jan 20, 2:00 AM - 4:00 AM (for 2 hours) * // Upgrading to PostgreSQL 15 * ``` * * @see StatusEventTimelineDot - For the colored indicator * @see StatusTimestamp - For rich timestamp hover cards */ export function StatusEventTimelineMaintenance({ maintenance, withDot = true, renderMessage, }: { maintenance: StatusMaintenanceUpdate; withDot?: boolean; renderMessage?: (message: string) => React.ReactNode; }) { const labels = useStatusBlocksLabels(); const duration = formatDistanceStrict(maintenance.from, maintenance.to); const { from, to } = labels.formatDateRangeParts( maintenance.from, maintenance.to, ); return (
{withDot ? (
) : null} {/* NOTE: is always last, no need for className="mb-2" */}
{maintenance.title}{" "} ·{" "} {from} {" - "} {to} {" "} {duration ? ( {labels.durationFor(duration)} ) : null} {maintenance.message.trim() === "" ? ( - ) : renderMessage ? ( renderMessage(maintenance.message) ) : ( maintenance.message )}
); } /** * StatusEventTimelineTitle - Title line for timeline entries * * Displays the title line of timeline entries with medium font weight, * typically containing status label, timestamp, and duration. * * @example * ```tsx * * Resolved · Jan 15, 10:30 AM * * ``` */ export function StatusEventTimelineTitle({ className, children, asChild, ...props }: React.ComponentProps<"div"> & { asChild?: boolean }) { const Comp = asChild ? Slot : "div"; return ( {children} ); } /** * StatusEventTimelineMessage - Message content for timeline entries * * Displays the update message in monospace font with muted color and * consistent padding. * * @example * ```tsx * * We have identified the root cause and deployed a fix * * ``` */ export function StatusEventTimelineMessage({ className, children, ...props }: React.ComponentProps<"div">) { return (
{children}
); } /** * StatusEventTimelineDot - Colored status indicator dot * * Displays a small circular dot with color based on the parent's data-variant: * - investigating: Red (destructive) * - identified: Yellow (warning) * - monitoring: Blue (info) * - resolved: Green (success) * - maintenance: Blue (info) * * @example * ```tsx *
* * // Displays green dot *
* ``` */ export function StatusEventTimelineDot({ className, ...props }: React.ComponentProps<"div">) { return (
); } /** * StatusEventTimelineSeparator - Vertical line connecting timeline entries * * Displays a vertical separator line between timeline updates, colored to match * the status of the update it's connected to. Uses the same color scheme as * StatusEventTimelineDot. * * @example * ```tsx *
* * * // Displays blue connecting line *
* ``` */ export function StatusEventTimelineSeparator({ className, ...props }: React.ComponentProps) { return ( ); }