From ecbca3208facde68254ddd46021f5001ad72c113 Mon Sep 17 00:00:00 2001 From: Nawaz Dhandala Date: Wed, 1 Apr 2026 14:58:58 +0100 Subject: [PATCH] feat: add onIncidentClick handler to various components for incident navigation; enhance Tooltip with animation support --- .../src/Components/Metrics/MetricCharts.tsx | 2 +- .../src/Pages/Monitor/View/Index.tsx | 20 ++ .../Components/Monitor/MonitorOverview.tsx | 5 + .../src/Pages/Overview/Overview.tsx | 22 ++ .../UI/Components/Graphs/DayUptimeGraph.tsx | 4 + .../UI/Components/Graphs/UptimeBarTooltip.tsx | 320 ++++++++++++------ Common/UI/Components/MonitorGraphs/Uptime.tsx | 4 + .../MonitorGraphs/UptimeBarDayModal.tsx | 174 +++++++++- Common/UI/Components/Tooltip/Tooltip.tsx | 22 +- 9 files changed, 441 insertions(+), 132 deletions(-) diff --git a/App/FeatureSet/Dashboard/src/Components/Metrics/MetricCharts.tsx b/App/FeatureSet/Dashboard/src/Components/Metrics/MetricCharts.tsx index 608b0322fa..8f993c0aa0 100644 --- a/App/FeatureSet/Dashboard/src/Components/Metrics/MetricCharts.tsx +++ b/App/FeatureSet/Dashboard/src/Components/Metrics/MetricCharts.tsx @@ -228,7 +228,7 @@ const MetricCharts: FunctionComponent = ( ? metricAttributes : undefined, groupByAttribute: - queryConfig.metricQueryData.filterData.groupByAttribute, + queryConfig.metricQueryData.filterData.groupByAttribute?.toString(), unit, }; diff --git a/App/FeatureSet/Dashboard/src/Pages/Monitor/View/Index.tsx b/App/FeatureSet/Dashboard/src/Pages/Monitor/View/Index.tsx index 5e7b66059a..69b4e754b4 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Monitor/View/Index.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Monitor/View/Index.tsx @@ -689,6 +689,16 @@ const MonitorView: FunctionComponent = (): ReactElement => { defaultBarColor={Green} downtimeMonitorStatuses={downTimeMonitorStatues} incidents={timelineIncidents} + onIncidentClick={(incidentId: string) => { + Navigation.navigate( + RouteUtil.populateRouteParams( + RouteMap[PageMap.INCIDENT_VIEW]!, + { + modelId: new ObjectID(incidentId), + }, + ), + ); + }} onBarClick={( date: Date, incidents: Array, @@ -703,6 +713,16 @@ const MonitorView: FunctionComponent = (): ReactElement => { { + Navigation.navigate( + RouteUtil.populateRouteParams( + RouteMap[PageMap.INCIDENT_VIEW]!, + { + modelId: new ObjectID(incidentId), + }, + ), + ); + }} onClose={() => { setSelectedDay(null); setSelectedDayIncidents([]); diff --git a/App/FeatureSet/StatusPage/src/Components/Monitor/MonitorOverview.tsx b/App/FeatureSet/StatusPage/src/Components/Monitor/MonitorOverview.tsx index 3cf3c361c2..8d9dffeb43 100644 --- a/App/FeatureSet/StatusPage/src/Components/Monitor/MonitorOverview.tsx +++ b/App/FeatureSet/StatusPage/src/Components/Monitor/MonitorOverview.tsx @@ -34,6 +34,9 @@ export interface ComponentProps { defaultBarColor: Color; uptimeHistoryDays?: number | undefined; incidents?: Array | undefined; + onIncidentClick?: + | ((incidentId: string) => void) + | undefined; } const MonitorOverview: FunctionComponent = ( @@ -146,6 +149,7 @@ const MonitorOverview: FunctionComponent = ( isLoading={false} height={props.uptimeGraphHeight} incidents={props.incidents} + onIncidentClick={props.onIncidentClick} onBarClick={( date: Date, incidents: Array, @@ -170,6 +174,7 @@ const MonitorOverview: FunctionComponent = ( { setSelectedDay(null); setSelectedDayIncidents([]); diff --git a/App/FeatureSet/StatusPage/src/Pages/Overview/Overview.tsx b/App/FeatureSet/StatusPage/src/Pages/Overview/Overview.tsx index 00b1f4e635..5adec3d35d 100644 --- a/App/FeatureSet/StatusPage/src/Pages/Overview/Overview.tsx +++ b/App/FeatureSet/StatusPage/src/Pages/Overview/Overview.tsx @@ -6,6 +6,8 @@ import ScheduledMaintenanceGroup from "../../Types/ScheduledMaintenanceGroup"; import API from "../../Utils/API"; import { STATUS_PAGE_API_URL } from "../../Utils/Config"; import StatusPageUtil from "../../Utils/StatusPage"; +import RouteMap, { RouteUtil } from "../../Utils/RouteMap"; +import PageMap from "../../Utils/PageMap"; import { getAnnouncementEventItem } from "../Announcement/Detail"; import { getIncidentEventItem, getEpisodeEventItem } from "../Incidents/Detail"; import PageComponentProps from "../PageComponentProps"; @@ -548,6 +550,16 @@ const Overview: FunctionComponent = ( defaultBarColor={statusPage?.defaultBarColor || Green} uptimeHistoryDays={uptimeHistoryDays} incidents={monitorIncidents} + onIncidentClick={(incidentId: string) => { + Navigation.navigate( + RouteUtil.populateRouteParams( + StatusPageUtil.isPreviewPage() + ? (RouteMap[PageMap.PREVIEW_INCIDENT_DETAIL] as Route) + : (RouteMap[PageMap.INCIDENT_DETAIL] as Route), + new ObjectID(incidentId), + ), + ); + }} />, ); } @@ -621,6 +633,16 @@ const Overview: FunctionComponent = ( defaultBarColor={statusPage?.defaultBarColor || Green} uptimeHistoryDays={uptimeHistoryDays} incidents={groupIncidents} + onIncidentClick={(incidentId: string) => { + Navigation.navigate( + RouteUtil.populateRouteParams( + StatusPageUtil.isPreviewPage() + ? (RouteMap[PageMap.PREVIEW_INCIDENT_DETAIL] as Route) + : (RouteMap[PageMap.INCIDENT_DETAIL] as Route), + new ObjectID(incidentId), + ), + ); + }} />, ); } diff --git a/Common/UI/Components/Graphs/DayUptimeGraph.tsx b/Common/UI/Components/Graphs/DayUptimeGraph.tsx index 70ef0d3c2d..6ee0702fd6 100644 --- a/Common/UI/Components/Graphs/DayUptimeGraph.tsx +++ b/Common/UI/Components/Graphs/DayUptimeGraph.tsx @@ -33,6 +33,9 @@ export interface ComponentProps { onBarClick?: | ((date: Date, incidents: Array) => void) | undefined; + onIncidentClick?: + | ((incidentId: string) => void) + | undefined; } const DayUptimeGraph: FunctionComponent = ( @@ -252,6 +255,7 @@ const DayUptimeGraph: FunctionComponent = ( hasEvents={hasEvents} statusDurations={statusDurations} incidents={dayIncidents} + onIncidentClick={props.onIncidentClick} /> } > diff --git a/Common/UI/Components/Graphs/UptimeBarTooltip.tsx b/Common/UI/Components/Graphs/UptimeBarTooltip.tsx index 3120115cd0..4dd2b16800 100644 --- a/Common/UI/Components/Graphs/UptimeBarTooltip.tsx +++ b/Common/UI/Components/Graphs/UptimeBarTooltip.tsx @@ -16,6 +16,9 @@ export interface ComponentProps { hasEvents: boolean; statusDurations: Array; incidents: Array; + onIncidentClick?: + | ((incidentId: string) => void) + | undefined; } const UptimeBarTooltip: FunctionComponent = ( @@ -45,35 +48,60 @@ const UptimeBarTooltip: FunctionComponent = ( ? "#fef9c3" : "#fee2e2"; + // Sort: downtime statuses first so they're prominent + const sortedDurations: Array = [ + ...props.statusDurations, + ].sort((a: StatusDuration, b: StatusDuration) => { + if (a.isDowntime && !b.isDowntime) { + return -1; + } + if (!a.isDowntime && b.isDowntime) { + return 1; + } + return b.seconds - a.seconds; + }); + return ( -
- {/* Date header */} +
+ {/* ── Header ── */}
-
+ {dateStr} -
+ + {props.hasEvents && props.incidents.length === 0 && ( + + {props.uptimePercent >= 100 ? "100%" : props.uptimePercent.toFixed(2) + "%"} + + )}
- {/* Uptime card */} + {/* ── Uptime meter ── */} {props.hasEvents && (
0 || props.incidents.length > 0 ? "12px" : "0", }} >
= ( display: "flex", justifyContent: "space-between", alignItems: "baseline", - marginBottom: "6px", + marginBottom: "8px", }} > Uptime - {props.uptimePercent.toFixed(2)}% + {props.uptimePercent >= 100 + ? "100" + : props.uptimePercent.toFixed(2)} + %
= ( width: "100%", height: "6px", backgroundColor: uptimeTrackColor, - borderRadius: "3px", + borderRadius: "100px", overflow: "hidden", }} > @@ -115,34 +152,43 @@ const UptimeBarTooltip: FunctionComponent = ( width: `${Math.min(props.uptimePercent, 100)}%`, height: "100%", backgroundColor: uptimeColor, - borderRadius: "3px", + borderRadius: "100px", }} />
)} + {/* ── No data ── */} {!props.hasEvents && (
-
- No data available for this day +
+ No monitoring data for this day
)} - {/* Status breakdown */} - {props.statusDurations.length > 0 && ( + {/* ── Status breakdown ── */} + {sortedDurations.length > 0 && (
0 ? "10px" : "0", + marginBottom: props.incidents.length > 0 ? "0" : "0", + paddingBottom: props.incidents.length > 0 ? "10px" : "0", + borderBottom: + props.incidents.length > 0 ? "1px solid #e5e7eb" : "none", }} >
= ( textTransform: "uppercase", letterSpacing: "0.06em", fontWeight: 600, - marginBottom: "4px", + marginBottom: "6px", }} > Status Breakdown
- {props.statusDurations.map( - (status: StatusDuration, index: number) => { - return ( + {sortedDurations.map((status: StatusDuration, index: number) => { + return ( +
-
- - - {status.label} - -
+ /> - {OneUptimeDate.secondsToFormattedFriendlyTimeString( - status.seconds, - )} + {status.label}
- ); - }, - )} + + {OneUptimeDate.secondsToFormattedFriendlyTimeString( + status.seconds, + )} + +
+ ); + })}
)} - {/* Incidents section */} + {/* ── Incidents ── */} {props.incidents.length > 0 && ( -
+
= (
{props.incidents.length}
+ {props.incidents.slice(0, 3).map( (incident: UptimeBarTooltipIncident) => { + const isClickable: boolean = Boolean(props.onIncidentClick); + return (
{ + e.stopPropagation(); + props.onIncidentClick!(incident.id); + } + : undefined + } style={{ backgroundColor: "#f9fafb", - border: "1px solid #f3f4f6", + border: "1px solid #e5e7eb", borderRadius: "8px", padding: "8px 10px", marginBottom: "6px", + cursor: isClickable ? "pointer" : "default", + transition: "all 0.15s ease", + }} + onMouseEnter={(e: React.MouseEvent) => { + if (isClickable) { + (e.currentTarget as HTMLDivElement).style.backgroundColor = + "#f3f4f6"; + (e.currentTarget as HTMLDivElement).style.borderColor = + "#d1d5db"; + } + }} + onMouseLeave={(e: React.MouseEvent) => { + if (isClickable) { + (e.currentTarget as HTMLDivElement).style.backgroundColor = + "#f9fafb"; + (e.currentTarget as HTMLDivElement).style.borderColor = + "#e5e7eb"; + } }} >
- {incident.title} +
+
+ {incident.title} +
+
+ {OneUptimeDate.getDateAsUserFriendlyLocalFormattedString( + incident.declaredAt, + false, + )} +
+
+ {isClickable && ( + + + + )}
{incident.incidentSeverity && ( @@ -296,9 +409,9 @@ const UptimeBarTooltip: FunctionComponent = ( fontWeight: 600, color: incident.incidentSeverity.color.toString(), backgroundColor: - incident.incidentSeverity.color.toString() + "15", - border: `1px solid ${incident.incidentSeverity.color.toString()}30`, - padding: "1px 8px", + incident.incidentSeverity.color.toString() + "12", + border: `1px solid ${incident.incidentSeverity.color.toString()}25`, + padding: "1px 7px", borderRadius: "9999px", lineHeight: "1.6", }} @@ -315,9 +428,9 @@ const UptimeBarTooltip: FunctionComponent = ( incident.currentIncidentState.color.toString(), backgroundColor: incident.currentIncidentState.color.toString() + - "15", - border: `1px solid ${incident.currentIncidentState.color.toString()}30`, - padding: "1px 8px", + "12", + border: `1px solid ${incident.currentIncidentState.color.toString()}25`, + padding: "1px 7px", borderRadius: "9999px", lineHeight: "1.6", }} @@ -330,13 +443,14 @@ const UptimeBarTooltip: FunctionComponent = ( ); }, )} + {props.incidents.length > 3 && (
@@ -344,18 +458,6 @@ const UptimeBarTooltip: FunctionComponent = ( {props.incidents.length - 3 !== 1 ? "s" : ""}
)} -
- Click bar to view details -
)}
diff --git a/Common/UI/Components/MonitorGraphs/Uptime.tsx b/Common/UI/Components/MonitorGraphs/Uptime.tsx index 754c232732..1836668c3f 100644 --- a/Common/UI/Components/MonitorGraphs/Uptime.tsx +++ b/Common/UI/Components/MonitorGraphs/Uptime.tsx @@ -30,6 +30,9 @@ export interface ComponentProps { defaultBarColor: Color; incidents?: Array | undefined; onBarClick?: (date: Date, incidents: Array) => void; + onIncidentClick?: + | ((incidentId: string) => void) + | undefined; } const MonitorUptimeGraph: FunctionComponent = ( @@ -88,6 +91,7 @@ const MonitorUptimeGraph: FunctionComponent = ( } incidents={props.incidents} onBarClick={props.onBarClick} + onIncidentClick={props.onIncidentClick} /> ); }; diff --git a/Common/UI/Components/MonitorGraphs/UptimeBarDayModal.tsx b/Common/UI/Components/MonitorGraphs/UptimeBarDayModal.tsx index b2212c6a84..34b8b516c3 100644 --- a/Common/UI/Components/MonitorGraphs/UptimeBarDayModal.tsx +++ b/Common/UI/Components/MonitorGraphs/UptimeBarDayModal.tsx @@ -7,6 +7,9 @@ export interface ComponentProps { date: Date; incidents: Array; onClose: () => void; + onIncidentClick?: + | ((incidentId: string) => void) + | undefined; } const UptimeBarDayModal: FunctionComponent = ( @@ -17,45 +20,179 @@ const UptimeBarDayModal: FunctionComponent = ( return ( 0 + ? `${props.incidents.length} incident${props.incidents.length !== 1 ? "s" : ""} reported on this day` + : undefined + } onClose={props.onClose} modalWidth={ModalWidth.Medium} closeButtonText="Close" >
{props.incidents.length === 0 && ( -
- No incidents on this day. +
+
+ + + +
+
+ No incidents +
+
+ No incidents were reported on this day. +
)} + {props.incidents.map((incident: UptimeBarTooltipIncident) => { + const isClickable: boolean = Boolean(props.onIncidentClick); + return (
{ + props.onIncidentClick!(incident.id); + } + : undefined + } + style={{ + border: "1px solid #e5e7eb", + borderRadius: "10px", + padding: "14px 16px", + marginBottom: "10px", + cursor: isClickable ? "pointer" : "default", + transition: "all 0.15s ease", + backgroundColor: "#ffffff", + }} + onMouseEnter={(e: React.MouseEvent) => { + if (isClickable) { + (e.currentTarget as HTMLDivElement).style.backgroundColor = + "#f9fafb"; + (e.currentTarget as HTMLDivElement).style.borderColor = + "#d1d5db"; + (e.currentTarget as HTMLDivElement).style.boxShadow = + "0 1px 3px rgba(0,0,0,0.06)"; + } + }} + onMouseLeave={(e: React.MouseEvent) => { + if (isClickable) { + (e.currentTarget as HTMLDivElement).style.backgroundColor = + "#ffffff"; + (e.currentTarget as HTMLDivElement).style.borderColor = + "#e5e7eb"; + (e.currentTarget as HTMLDivElement).style.boxShadow = "none"; + } + }} > -
-
-
+
+
+
{incident.title}
-
- Declared at{" "} +
+ Declared{" "} {OneUptimeDate.getDateAsUserFriendlyLocalFormattedString( incident.declaredAt, false, )}
+ {isClickable && ( + + + + )}
-
+
{incident.incidentSeverity && ( {incident.incidentSeverity.name} @@ -63,11 +200,16 @@ const UptimeBarDayModal: FunctionComponent = ( )} {incident.currentIncidentState && ( {incident.currentIncidentState.name} diff --git a/Common/UI/Components/Tooltip/Tooltip.tsx b/Common/UI/Components/Tooltip/Tooltip.tsx index b7ac9c9346..99e07f7600 100644 --- a/Common/UI/Components/Tooltip/Tooltip.tsx +++ b/Common/UI/Components/Tooltip/Tooltip.tsx @@ -2,6 +2,7 @@ import Tippy from "@tippyjs/react"; import React, { FunctionComponent, ReactElement } from "react"; import "tippy.js/dist/tippy.css"; import "tippy.js/themes/light-border.css"; +import "tippy.js/animations/shift-away-subtle.css"; export interface ComponentProps { text?: string | undefined; @@ -22,20 +23,29 @@ const Tooltip: FunctionComponent = ( {props.text} ); - const themeProps: { theme: string } | Record = - props.richContent ? { theme: "light-border" } : {}; + const isRich: boolean = Boolean(props.richContent); + + const themeProps: { theme: string } | Record = isRich + ? { theme: "light-border" } + : {}; + + const animationProps: { animation: string } | Record = isRich + ? { animation: "shift-away-subtle" } + : {}; return (