Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/openstatusHQ/openstatus. ๐ซ Status page with uptime monitoring & API monitoring as code ๐ซ openstatus.dev
bun drizzle-orm monitoring monitoring-as-code nextjs observability on-call open-source shadcn-ui status-page statuspage synthetic-monitoring tinybird turso uptime uptime-checker uptime-monitor
Something went wrong. Try again.
9.6 kB ยท 304 lines
TSX
at incident-api
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305"use client";import { StatusBlankEvents } from "@openstatus/ui/components/blocks/status-blank";import { StatusEvent, StatusEventAffected, StatusEventAffectedBadge, StatusEventAside, StatusEventContent, StatusEventDate, StatusEventGroup, StatusEventTimelineMaintenance, StatusEventTimelineReport, StatusEventTitle,} from "@openstatus/ui/components/blocks/status-events";import { useStatusBlocksLabels } from "@openstatus/ui/components/blocks/status-i18n";import type { StatusReport, Maintenance,} from "@openstatus/ui/components/blocks/status.types";import { useMemo } from "react";
type UnifiedEvent = { id: number; title: string; startDate: Date;} & ( | { type: "report"; data: StatusReport } | { type: "maintenance"; data: Maintenance });
/** * isStatusReport - Type guard for discriminating status reports * * Type guard function that narrows a UnifiedEvent to a status report event. * Used internally by StatusFeed to distinguish between reports and maintenance. * * @param event - The unified event to check * @returns True if the event is a status report * * @example * ```tsx * if (isStatusReport(event)) { * // TypeScript knows event.data is StatusReport * console.log(event.data.updates); * } * ``` */function isStatusReport( event: UnifiedEvent,): event is UnifiedEvent & { type: "report"; data: StatusReport } { return event.type === "report";}
/** * isMaintenance - Type guard for discriminating maintenance events * * Type guard function that narrows a UnifiedEvent to a maintenance event. * Used internally by StatusFeed to distinguish between reports and maintenance. * * @param event - The unified event to check * @returns True if the event is a maintenance * * @example * ```tsx * if (isMaintenance(event)) { * // TypeScript knows event.data is Maintenance * console.log(event.data.from, event.data.to); * } * ``` */function isMaintenance( event: UnifiedEvent,): event is UnifiedEvent & { type: "maintenance"; data: Maintenance } { return event.type === "maintenance";}
/** * StatusFeed - Unified feed of incident reports and maintenance events * * Displays a chronological feed combining both status reports (incidents) and * scheduled maintenance, sorted by date from newest to oldest. The component * intelligently merges the two event types and renders them using a discriminated * union pattern for type safety. * * **Key Features**: * - **Unified Timeline**: Combines reports and maintenance in chronological order * - **Discriminated Union**: Type-safe rendering using TypeScript discriminated unions * - **Memoization**: Uses `useMemo` to prevent flicker on re-renders * - **Empty State**: Shows a styled empty state when no events are present * - **Automatic Rendering**: Each event type is rendered with appropriate components * * **Data Structure**: * - **StatusReport**: Incident reports with updates timeline * - id, title, affected services * - updates array with status, message, date * - **Maintenance**: Scheduled maintenance windows * - id, title, message, affected services * - from/to date range * * The component creates a unified event array internally, using stable keys * (`${type}-${id}`) to prevent React flicker when data updates. * * @param statusReports - Array of incident reports to display * @param maintenances - Array of maintenance events to display * * @example * // Feed with both reports and maintenance * ```tsx * <StatusFeed * statusReports={[ * { * id: 1, * title: "API Outage", * affected: ["API", "Database"], * updates: [ * { * status: "resolved", * message: "All systems operational", * date: new Date("2024-01-15T12:00:00Z") * }, * { * status: "investigating", * message: "Investigating API timeouts", * date: new Date("2024-01-15T11:00:00Z") * } * ] * } * ]} * maintenances={[ * { * id: 2, * title: "Database Upgrade", * message: "Upgrading to PostgreSQL 15", * affected: ["Database"], * from: new Date("2024-01-20T02:00:00Z"), * to: new Date("2024-01-20T04:00:00Z") * } * ]} * /> * ``` * * @example * // Empty state (no events) * ```tsx * <StatusFeed statusReports={[]} maintenances={[]} /> * // Displays: "No recent notifications" empty state * ``` * * @example * // Only incident reports * ```tsx * <StatusFeed * statusReports={incidentReports} * maintenances={[]} * /> * ``` * * @see StatusEventGroup - For the feed container * @see StatusEventTimelineReport - For incident rendering * @see StatusEventTimelineMaintenance - For maintenance rendering * @see isStatusReport - For type guard discrimination * @see isMaintenance - For type guard discrimination */export function StatusFeed({ statusReports = [], maintenances = [], footer, emptyAction, renderReportMessage, renderMaintenanceMessage, renderEvent, ...props}: React.ComponentProps<"div"> & { statusReports?: StatusReport[]; maintenances?: Maintenance[]; footer?: React.ReactNode; emptyAction?: React.ReactNode; renderReportMessage?: (message: string) => React.ReactNode; renderMaintenanceMessage?: (message: string) => React.ReactNode; renderEvent?: ( event: | { type: "report"; data: StatusReport } | { type: "maintenance"; data: Maintenance }, children: React.ReactNode, ) => React.ReactNode;}) { const labels = useStatusBlocksLabels(); // Memoize the unified events array to prevent flicker on re-renders const unifiedEvents = useMemo<UnifiedEvent[]>(() => { return [ ...statusReports.map((report): UnifiedEvent => ({ id: report.id, title: report.title, type: "report", startDate: report.updates[report.updates.length - 1]?.date || new Date(), data: report, })), ...maintenances.map((maintenance): UnifiedEvent => ({ id: maintenance.id, title: maintenance.title, type: "maintenance", startDate: maintenance.from, data: maintenance, })), ].sort((a, b) => b.startDate.getTime() - a.startDate.getTime()); }, [statusReports, maintenances]);
if (unifiedEvents.length === 0) { return ( <StatusBlankEvents title={labels.noRecentNotifications} description={labels.noRecentNotificationsDescription} action={emptyAction} /> ); }
return ( <> <StatusEventGroup data-slot="status-feed" {...props}> {unifiedEvents.map((event) => { // Use stable key to prevent flicker const key = `${event.type}-${event.id}`;
if (isStatusReport(event)) { const report = event.data; const node = ( <StatusEventContent> <StatusEventTitle>{report.title}</StatusEventTitle> {report.affected.length > 0 && ( <StatusEventAffected> {report.affected.map((affected, index) => ( <StatusEventAffectedBadge key={index}> {affected} </StatusEventAffectedBadge> ))} </StatusEventAffected> )} <StatusEventTimelineReport updates={report.updates} renderMessage={renderReportMessage} /> </StatusEventContent> ); return ( <StatusEvent key={key}> <StatusEventAside> <StatusEventDate date={event.startDate} /> </StatusEventAside> {renderEvent ? renderEvent({ type: "report", data: report }, node) : node} </StatusEvent> ); }
if (isMaintenance(event)) { const maintenance = event.data; const node = ( <StatusEventContent> <StatusEventTitle>{maintenance.title}</StatusEventTitle> {maintenance.affected.length > 0 && ( <StatusEventAffected> {maintenance.affected.map((affected, index) => ( <StatusEventAffectedBadge key={index}> {affected} </StatusEventAffectedBadge> ))} </StatusEventAffected> )} <StatusEventTimelineMaintenance maintenance={{ title: maintenance.title, message: maintenance.message, from: maintenance.from, to: maintenance.to, }} renderMessage={renderMaintenanceMessage} /> </StatusEventContent> ); return ( <StatusEvent key={key}> <StatusEventAside> <StatusEventDate date={event.startDate} /> </StatusEventAside> {renderEvent ? renderEvent( { type: "maintenance", data: maintenance }, node, ) : node} </StatusEvent> ); } return null; })} </StatusEventGroup> {footer} </> );}StatusFeed.displayName = "StatusFeed";