From 832b87e6d5ddc071fb34bd65d6e292b16fe8ce75 Mon Sep 17 00:00:00 2001 From: Nawaz Dhandala Date: Wed, 1 Apr 2026 12:42:00 +0100 Subject: [PATCH] feat: implement incident handling in uptime graphs with tooltips and modals for better user experience --- .../src/Pages/Monitor/View/Index.tsx | 93 ++++++++++++++ .../Components/Monitor/MonitorOverview.tsx | 30 ++++- .../src/Pages/Overview/Overview.tsx | 70 +++++++++++ Common/Server/API/StatusPageAPI.ts | 46 +++++++ .../Types/Monitor/UptimeBarTooltipIncident.ts | 21 ++++ .../UI/Components/Graphs/DayUptimeGraph.tsx | 116 ++++++++++++------ .../UI/Components/Graphs/UptimeBarTooltip.tsx | 100 +++++++++++++++ Common/UI/Components/MonitorGraphs/Uptime.tsx | 5 + .../MonitorGraphs/UptimeBarDayModal.tsx | 85 +++++++++++++ Common/UI/Components/Tooltip/Tooltip.tsx | 14 ++- Probe/package-lock.json | 1 + Telemetry/package-lock.json | 1 + Worker/package-lock.json | 1 + 13 files changed, 543 insertions(+), 40 deletions(-) create mode 100644 Common/Types/Monitor/UptimeBarTooltipIncident.ts create mode 100644 Common/UI/Components/Graphs/UptimeBarTooltip.tsx create mode 100644 Common/UI/Components/MonitorGraphs/UptimeBarDayModal.tsx diff --git a/App/FeatureSet/Dashboard/src/Pages/Monitor/View/Index.tsx b/App/FeatureSet/Dashboard/src/Pages/Monitor/View/Index.tsx index 9d6670a03c..5e7b66059a 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Monitor/View/Index.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Monitor/View/Index.tsx @@ -65,6 +65,10 @@ import MonitorFeedElement from "../../../Components/Monitor/MonitorFeed"; import URL from "Common/Types/API/URL"; import { APP_API_URL } from "Common/UI/Config"; import MonitorEvaluationSummary from "Common/Types/Monitor/MonitorEvaluationSummary"; +import Incident from "Common/Models/DatabaseModels/Incident"; +import UptimeBarTooltipIncident from "Common/Types/Monitor/UptimeBarTooltipIncident"; +import UptimeBarDayModal from "Common/UI/Components/MonitorGraphs/UptimeBarDayModal"; +import Color from "Common/Types/Color"; const MonitorView: FunctionComponent = (): ReactElement => { const modelId: ObjectID = Navigation.getLastParamAsObjectID(); @@ -110,6 +114,15 @@ const MonitorView: FunctionComponent = (): ReactElement => { MonitorEvaluationSummary | undefined >(undefined); + const [timelineIncidents, setTimelineIncidents] = useState< + Array + >([]); + + const [selectedDay, setSelectedDay] = useState(null); + const [selectedDayIncidents, setSelectedDayIncidents] = useState< + Array + >([]); + const getUptimePercent: () => ReactElement = (): ReactElement => { if (isLoading) { return <>; @@ -297,6 +310,67 @@ const MonitorView: FunctionComponent = (): ReactElement => { ); setStatusTimelines(monitorStatus.data); + // Fetch incidents for this monitor in the timeline date range + const incidentResult: ListResult = await ModelAPI.getList({ + modelType: Incident, + query: { + monitors: [modelId] as any, + declaredAt: new InBetween(startDate, endDate), + projectId: ProjectUtil.getCurrentProjectId()!, + }, + limit: LIMIT_PER_PROJECT, + skip: 0, + select: { + _id: true, + title: true, + declaredAt: true, + incidentSeverity: { + name: true, + color: true, + }, + currentIncidentState: { + _id: true, + name: true, + color: true, + }, + monitors: { + _id: true, + }, + }, + sort: { + declaredAt: SortOrder.Descending, + }, + }); + + const parsedIncidents: Array = + incidentResult.data.map((incident: Incident) => { + return { + id: incident._id || "", + title: incident.title || "", + declaredAt: incident.declaredAt || new Date(), + incidentSeverity: incident.incidentSeverity + ? { + name: incident.incidentSeverity.name || "", + color: + incident.incidentSeverity.color || new Color("#000000"), + } + : undefined, + currentIncidentState: incident.currentIncidentState + ? { + name: incident.currentIncidentState.name || "", + color: + incident.currentIncidentState.color || + new Color("#000000"), + } + : undefined, + monitorIds: (incident.monitors || []).map((m: Monitor) => { + return new ObjectID(m._id?.toString() || ""); + }), + }; + }); + + setTimelineIncidents(parsedIncidents); + const isMonitoredByProbe: boolean = item.monitorType ? MonitorTypeHelper.isProbableMonitor(item.monitorType) : false; @@ -614,9 +688,28 @@ const MonitorView: FunctionComponent = (): ReactElement => { isLoading={isLoading} defaultBarColor={Green} downtimeMonitorStatuses={downTimeMonitorStatues} + incidents={timelineIncidents} + onBarClick={( + date: Date, + incidents: Array, + ) => { + setSelectedDay(date); + setSelectedDayIncidents(incidents); + }} /> + {selectedDay && ( + { + setSelectedDay(null); + setSelectedDayIncidents([]); + }} + /> + )} + ; defaultBarColor: Color; uptimeHistoryDays?: number | undefined; + incidents?: Array | undefined; } const MonitorOverview: FunctionComponent = ( props: ComponentProps, ): ReactElement => { + const [selectedDay, setSelectedDay] = useState(null); + const [selectedDayIncidents, setSelectedDayIncidents] = useState< + Array + >([]); + const getCurrentStatus: GetReactElementFunction = (): ReactElement => { // if the current status is operational then show uptime Percent. @@ -137,6 +145,14 @@ const MonitorOverview: FunctionComponent = ( endDate={props.endDate} isLoading={false} height={props.uptimeGraphHeight} + incidents={props.incidents} + onBarClick={( + date: Date, + incidents: Array, + ) => { + setSelectedDay(date); + setSelectedDayIncidents(incidents); + }} /> )} @@ -148,6 +164,18 @@ const MonitorOverview: FunctionComponent = (
Today
)} + + {/* Incident detail modal */} + {selectedDay && ( + { + setSelectedDay(null); + setSelectedDayIncidents([]); + }} + /> + )} ); }; diff --git a/App/FeatureSet/StatusPage/src/Pages/Overview/Overview.tsx b/App/FeatureSet/StatusPage/src/Pages/Overview/Overview.tsx index 65244cdb6b..00b1f4e635 100644 --- a/App/FeatureSet/StatusPage/src/Pages/Overview/Overview.tsx +++ b/App/FeatureSet/StatusPage/src/Pages/Overview/Overview.tsx @@ -40,6 +40,7 @@ import IncidentEpisodePublicNote from "Common/Models/DatabaseModels/IncidentEpis import IncidentEpisodeStateTimeline from "Common/Models/DatabaseModels/IncidentEpisodeStateTimeline"; import IncidentPublicNote from "Common/Models/DatabaseModels/IncidentPublicNote"; import IncidentStateTimeline from "Common/Models/DatabaseModels/IncidentStateTimeline"; +import Monitor from "Common/Models/DatabaseModels/Monitor"; import MonitorStatus from "Common/Models/DatabaseModels/MonitorStatus"; import MonitorStatusTimeline from "Common/Models/DatabaseModels/MonitorStatusTimeline"; import ScheduledMaintenance from "Common/Models/DatabaseModels/ScheduledMaintenance"; @@ -59,6 +60,8 @@ import React, { import UptimePrecision from "Common/Types/StatusPage/UptimePrecision"; import StatusPageResourceUptimeUtil from "Common/Utils/StatusPage/ResourceUptime"; import BadDataException from "Common/Types/Exception/BadDataException"; +import UptimeBarTooltipIncident from "Common/Types/Monitor/UptimeBarTooltipIncident"; +import Color from "Common/Types/Color"; const Overview: FunctionComponent = ( props: PageComponentProps, @@ -141,6 +144,10 @@ const Overview: FunctionComponent = ( const [monitorGroupCurrentStatuses, setMonitorGroupCurrentStatuses] = useState>({}); + const [timelineIncidents, setTimelineIncidents] = useState< + Array + >([]); + StatusPageUtil.checkIfUserHasLoggedIn(); const loadPage: PromiseVoidFunction = async (): Promise => { @@ -277,6 +284,39 @@ const Overview: FunctionComponent = ( (data["monitorGroupCurrentStatuses"] as JSONObject) || {}, ) as Dictionary; + // Parse timeline incidents for uptime bar tooltips + const rawTimelineIncidents: Array = BaseModel.fromJSONArray( + (data["timelineIncidents"] as JSONArray) || [], + Incident, + ); + + const parsedTimelineIncidents: Array = + rawTimelineIncidents.map((incident: Incident) => { + return { + id: incident._id || "", + title: incident.title || "", + declaredAt: incident.declaredAt || new Date(), + incidentSeverity: incident.incidentSeverity + ? { + name: incident.incidentSeverity.name || "", + color: incident.incidentSeverity.color || new Color("#000000"), + } + : undefined, + currentIncidentState: incident.currentIncidentState + ? { + name: incident.currentIncidentState.name || "", + color: + incident.currentIncidentState.color || + new Color("#000000"), + } + : undefined, + monitorIds: (incident.monitors || []).map((m: Monitor) => { + return new ObjectID(m._id?.toString() || ""); + }), + }; + }); + + setTimelineIncidents(parsedTimelineIncidents); setMonitorsInGroup(monitorsInGroup); setMonitorGroupCurrentStatuses(monitorGroupCurrentStatuses); @@ -463,6 +503,18 @@ const Overview: FunctionComponent = ( currentStatus.color = Green; } + const monitorId: string = + resource.monitor?._id?.toString() || ""; + + const monitorIncidents: Array = + timelineIncidents.filter( + (incident: UptimeBarTooltipIncident) => { + return incident.monitorIds.some((id: ObjectID) => { + return id.toString() === monitorId; + }); + }, + ); + elements.push( = ( uptimeGraphHeight={10} defaultBarColor={statusPage?.defaultBarColor || Green} uptimeHistoryDays={uptimeHistoryDays} + incidents={monitorIncidents} />, ); } @@ -519,6 +572,22 @@ const Overview: FunctionComponent = ( currentStatus.color = Green; } + // Get monitor IDs in this group + const groupMonitorIds: Array = ( + monitorsInGroup[resource.monitorGroupId?.toString() || ""] || [] + ).map((id: ObjectID) => { + return id.toString(); + }); + + const groupIncidents: Array = + timelineIncidents.filter( + (incident: UptimeBarTooltipIncident) => { + return incident.monitorIds.some((id: ObjectID) => { + return groupMonitorIds.includes(id.toString()); + }); + }, + ); + elements.push( = ( uptimeGraphHeight={10} defaultBarColor={statusPage?.defaultBarColor || Green} uptimeHistoryDays={uptimeHistoryDays} + incidents={groupIncidents} />, ); } diff --git a/Common/Server/API/StatusPageAPI.ts b/Common/Server/API/StatusPageAPI.ts index 672798e884..129473bc8e 100644 --- a/Common/Server/API/StatusPageAPI.ts +++ b/Common/Server/API/StatusPageAPI.ts @@ -2169,6 +2169,48 @@ export default class StatusPageAPI extends BaseAPI< }, }); + // Fetch all incidents (active + resolved) in the timeline date range + // for the uptime bar tooltip and click-through + let timelineIncidents: Array = []; + if ( + monitorsOnStatusPage.length > 0 && + statusPage.showIncidentsOnStatusPage + ) { + timelineIncidents = await IncidentService.findBy({ + query: { + monitors: monitorsOnStatusPage as any, + declaredAt: QueryHelper.inBetween(startDate, endDate), + isVisibleOnStatusPage: true, + projectId: statusPage.projectId!, + }, + select: { + _id: true, + title: true, + declaredAt: true, + incidentSeverity: { + name: true, + color: true, + }, + currentIncidentState: { + _id: true, + name: true, + color: true, + }, + monitors: { + _id: true, + }, + }, + sort: { + declaredAt: SortOrder.Descending, + }, + skip: 0, + limit: LIMIT_PER_PROJECT, + props: { + isRoot: true, + }, + }); + } + const overallStatus: MonitorStatus | null = StatusPageService.getOverallMonitorStatus({ statusPageResources, @@ -2251,6 +2293,10 @@ export default class StatusPageAPI extends BaseAPI< monitorGroupCurrentStatuses, ), monitorsInGroup: JSONFunctions.serialize(monitorsInGroup), + timelineIncidents: BaseModel.toJSONArray( + timelineIncidents, + Incident, + ), }; return Response.sendJsonObjectResponse(req, res, response); diff --git a/Common/Types/Monitor/UptimeBarTooltipIncident.ts b/Common/Types/Monitor/UptimeBarTooltipIncident.ts new file mode 100644 index 0000000000..4dd7d6063a --- /dev/null +++ b/Common/Types/Monitor/UptimeBarTooltipIncident.ts @@ -0,0 +1,21 @@ +import Color from "../Color"; +import ObjectID from "../ObjectID"; + +export default interface UptimeBarTooltipIncident { + id: string; + title: string; + declaredAt: Date; + incidentSeverity?: + | { + name: string; + color: Color; + } + | undefined; + currentIncidentState?: + | { + name: string; + color: Color; + } + | undefined; + monitorIds: Array; +} diff --git a/Common/UI/Components/Graphs/DayUptimeGraph.tsx b/Common/UI/Components/Graphs/DayUptimeGraph.tsx index f0c6d71910..a44028b34e 100644 --- a/Common/UI/Components/Graphs/DayUptimeGraph.tsx +++ b/Common/UI/Components/Graphs/DayUptimeGraph.tsx @@ -1,9 +1,11 @@ import Tooltip from "../Tooltip/Tooltip"; +import UptimeBarTooltip from "./UptimeBarTooltip"; import { Green } from "../../../Types/BrandColors"; import Color from "../../../Types/Color"; import OneUptimeDate from "../../../Types/Date"; import Dictionary from "../../../Types/Dictionary"; import ObjectID from "../../../Types/ObjectID"; +import UptimeBarTooltipIncident from "../../../Types/Monitor/UptimeBarTooltipIncident"; import React, { FunctionComponent, ReactElement, @@ -27,6 +29,10 @@ export interface ComponentProps { barColorRules?: Array | undefined; downtimeEventStatusIds?: Array | undefined; defaultBarColor: Color; + incidents?: Array | undefined; + onBarClick?: + | ((date: Date, incidents: Array) => void) + | undefined; } const DayUptimeGraph: FunctionComponent = ( @@ -43,6 +49,28 @@ const DayUptimeGraph: FunctionComponent = ( ); }, [props.startDate, props.endDate]); + type GetIncidentsForDayFunction = ( + startOfDay: Date, + endOfDay: Date, + ) => Array; + + const getIncidentsForDay: GetIncidentsForDayFunction = ( + startOfDay: Date, + endOfDay: Date, + ): Array => { + if (!props.incidents || props.incidents.length === 0) { + return []; + } + + return props.incidents.filter((incident: UptimeBarTooltipIncident) => { + return OneUptimeDate.isBetween( + incident.declaredAt, + startOfDay, + endOfDay, + ); + }); + }; + type GetUptimeBarFunction = (dayNumber: number) => ReactElement; const getUptimeBar: GetUptimeBarFunction = ( @@ -55,11 +83,6 @@ const DayUptimeGraph: FunctionComponent = ( dayNumber, ); - let toolTipText: string = `${OneUptimeDate.getDateAsUserFriendlyLocalFormattedString( - todaysDay, - true, - )}`; - const startOfTheDay: Date = OneUptimeDate.getStartOfDay(todaysDay); const endOfTheDay: Date = OneUptimeDate.getEndOfDay(todaysDay); @@ -140,33 +163,20 @@ const DayUptimeGraph: FunctionComponent = ( let totalUptimeInSeconds: number = 0; + const downtimeStatusIds: Array = ( + props.downtimeEventStatusIds || [] + ).map((id: ObjectID) => { + return id.toString(); + }); + for (const key in secondsOfEvent) { hasEvents = true; const eventStatusId: string = key; - // if this is downtime state then, include tooltip. - - if ( - (props.downtimeEventStatusIds?.filter((id: ObjectID) => { - return id.toString() === eventStatusId.toString(); - }).length || 0) > 0 - ) { - toolTipText += `, ${ - eventLabels[key] - } for ${OneUptimeDate.secondsToFormattedFriendlyTimeString( - secondsOfEvent[key] || 0, - )}`; - } - - const isDowntimeEvent: boolean = Boolean( - props.downtimeEventStatusIds?.find((id: ObjectID) => { - return id.toString() === eventStatusId; - }), - ); + const isDowntimeEvent: boolean = downtimeStatusIds.includes(eventStatusId); if (isDowntimeEvent) { - // remove the seconds from total uptime. const secondsOfDowntime: number = secondsOfEvent[key] || 0; totalDowntimeInSeconds += secondsOfDowntime; } else { @@ -177,8 +187,11 @@ const DayUptimeGraph: FunctionComponent = ( // now check bar rules and finalize the color of the bar const uptimePercentForTheDay: number = - (totalUptimeInSeconds / (totalDowntimeInSeconds + totalUptimeInSeconds)) * - 100; + totalUptimeInSeconds + totalDowntimeInSeconds > 0 + ? (totalUptimeInSeconds / + (totalDowntimeInSeconds + totalUptimeInSeconds)) * + 100 + : 100; for (const rules of props.barColorRules || []) { if (uptimePercentForTheDay >= rules.uptimePercentGreaterThanOrEqualTo) { @@ -187,31 +200,62 @@ const DayUptimeGraph: FunctionComponent = ( } } - if (todaysEvents.length === 1) { + if (todaysEvents.length === 1 && !hasEvents) { hasEvents = true; - toolTipText = `${OneUptimeDate.getDateAsUserFriendlyLocalFormattedString( - todaysDay, - true, - )} - 100% ${todaysEvents[0]?.label || "Operational"}.`; } - if (!hasEvents) { - toolTipText += ` - No data for this day.`; + if (todaysEvents.length === 1) { + hasEvents = true; + } + + if (todaysEvents.length === 0) { + hasEvents = false; color = props.defaultBarColor || Green; } + // Get incidents for this day + const dayIncidents: Array = getIncidentsForDay( + startOfTheDay, + endOfTheDay, + ); + let className: string = "h-20 w-20"; if (props.height) { className = "w-20 h-" + props.height; } + + const hasDayIncidents: boolean = dayIncidents.length > 0; + const isClickable: boolean = + hasDayIncidents && Boolean(props.onBarClick); + return ( - + + } + >
{ + props.onBarClick!(todaysDay, dayIncidents); + } + : undefined + } >
); diff --git a/Common/UI/Components/Graphs/UptimeBarTooltip.tsx b/Common/UI/Components/Graphs/UptimeBarTooltip.tsx new file mode 100644 index 0000000000..a2918cafd7 --- /dev/null +++ b/Common/UI/Components/Graphs/UptimeBarTooltip.tsx @@ -0,0 +1,100 @@ +import OneUptimeDate from "../../../Types/Date"; +import Dictionary from "../../../Types/Dictionary"; +import UptimeBarTooltipIncident from "../../../Types/Monitor/UptimeBarTooltipIncident"; +import React, { FunctionComponent, ReactElement } from "react"; + +export interface ComponentProps { + date: Date; + uptimePercent: number; + hasEvents: boolean; + eventLabels: Dictionary; + secondsOfEvent: Dictionary; + downtimeEventStatusIds: Array; + incidents: Array; +} + +const UptimeBarTooltip: FunctionComponent = ( + props: ComponentProps, +): ReactElement => { + const dateStr: string = + OneUptimeDate.getDateAsUserFriendlyLocalFormattedString(props.date, true); + + return ( +
+
{dateStr}
+ + {props.hasEvents && ( +
+ + {props.uptimePercent.toFixed(2)}% uptime + +
+ )} + + {!props.hasEvents && ( +
No data for this day.
+ )} + + {/* Status durations */} + {Object.keys(props.secondsOfEvent).length > 0 && ( +
+ {Object.keys(props.secondsOfEvent).map((key: string) => { + const isDowntime: boolean = props.downtimeEventStatusIds.includes(key); + if (!isDowntime) { + return null; + } + return ( +
+ {props.eventLabels[key]} for{" "} + {OneUptimeDate.secondsToFormattedFriendlyTimeString( + props.secondsOfEvent[key] || 0, + )} +
+ ); + })} +
+ )} + + {/* Incidents */} + {props.incidents.length > 0 && ( +
+
+ {props.incidents.length} Incident + {props.incidents.length > 1 ? "s" : ""}: +
+ {props.incidents.slice(0, 5).map( + (incident: UptimeBarTooltipIncident) => { + return ( +
+ {incident.incidentSeverity?.color && ( + + )} + {incident.title} +
+ ); + }, + )} + {props.incidents.length > 5 && ( +
+ +{props.incidents.length - 5} more... +
+ )} +
+ )} + + {props.incidents.length > 0 && ( +
+ Click for details +
+ )} +
+ ); +}; + +export default UptimeBarTooltip; diff --git a/Common/UI/Components/MonitorGraphs/Uptime.tsx b/Common/UI/Components/MonitorGraphs/Uptime.tsx index 6519a80868..754c232732 100644 --- a/Common/UI/Components/MonitorGraphs/Uptime.tsx +++ b/Common/UI/Components/MonitorGraphs/Uptime.tsx @@ -7,6 +7,7 @@ import CommonMonitorEvent from "../../../Utils/Uptime/MonitorEvent"; import MonitorStatus from "../../../Models/DatabaseModels/MonitorStatus"; import MonitorStatusTimeline from "../../../Models/DatabaseModels/MonitorStatusTimeline"; import StatusPageHistoryChartBarColorRule from "../../../Models/DatabaseModels/StatusPageHistoryChartBarColorRule"; +import UptimeBarTooltipIncident from "../../../Types/Monitor/UptimeBarTooltipIncident"; import React, { FunctionComponent, ReactElement, @@ -27,6 +28,8 @@ export interface ComponentProps { barColorRules?: Array | undefined; downtimeMonitorStatuses: Array | undefined; defaultBarColor: Color; + incidents?: Array | undefined; + onBarClick?: (date: Date, incidents: Array) => void; } const MonitorUptimeGraph: FunctionComponent = ( @@ -83,6 +86,8 @@ const MonitorUptimeGraph: FunctionComponent = ( return status.id!; }) || [] } + incidents={props.incidents} + onBarClick={props.onBarClick} /> ); }; diff --git a/Common/UI/Components/MonitorGraphs/UptimeBarDayModal.tsx b/Common/UI/Components/MonitorGraphs/UptimeBarDayModal.tsx new file mode 100644 index 0000000000..b2212c6a84 --- /dev/null +++ b/Common/UI/Components/MonitorGraphs/UptimeBarDayModal.tsx @@ -0,0 +1,85 @@ +import Modal, { ModalWidth } from "../Modal/Modal"; +import OneUptimeDate from "../../../Types/Date"; +import UptimeBarTooltipIncident from "../../../Types/Monitor/UptimeBarTooltipIncident"; +import React, { FunctionComponent, ReactElement } from "react"; + +export interface ComponentProps { + date: Date; + incidents: Array; + onClose: () => void; +} + +const UptimeBarDayModal: FunctionComponent = ( + props: ComponentProps, +): ReactElement => { + const dateStr: string = + OneUptimeDate.getDateAsUserFriendlyLocalFormattedString(props.date, true); + + return ( + +
+ {props.incidents.length === 0 && ( +
+ No incidents on this day. +
+ )} + {props.incidents.map((incident: UptimeBarTooltipIncident) => { + return ( +
+
+
+
+ {incident.title} +
+
+ Declared at{" "} + {OneUptimeDate.getDateAsUserFriendlyLocalFormattedString( + incident.declaredAt, + false, + )} +
+
+
+
+ {incident.incidentSeverity && ( + + {incident.incidentSeverity.name} + + )} + {incident.currentIncidentState && ( + + {incident.currentIncidentState.name} + + )} +
+
+ ); + })} +
+
+ ); +}; + +export default UptimeBarDayModal; diff --git a/Common/UI/Components/Tooltip/Tooltip.tsx b/Common/UI/Components/Tooltip/Tooltip.tsx index 0349d5962a..474a67aef5 100644 --- a/Common/UI/Components/Tooltip/Tooltip.tsx +++ b/Common/UI/Components/Tooltip/Tooltip.tsx @@ -3,24 +3,32 @@ import React, { FunctionComponent, ReactElement } from "react"; import "tippy.js/dist/tippy.css"; export interface ComponentProps { - text: string; + text?: string | undefined; children: ReactElement; + richContent?: ReactElement | undefined; } const Tooltip: FunctionComponent = ( props: ComponentProps, ): ReactElement => { - if (!props.text) { + if (!props.text && !props.richContent) { return props.children; } + const tooltipContent: ReactElement = props.richContent ? ( + props.richContent + ) : ( + {props.text} + ); + return ( {props.text}} + content={tooltipContent} interactive={true} trigger="mouseenter focus" hideOnClick={false} + maxWidth={350} aria={{ content: "describedby", expanded: "auto", diff --git a/Probe/package-lock.json b/Probe/package-lock.json index 740d65295e..b0e5122da5 100644 --- a/Probe/package-lock.json +++ b/Probe/package-lock.json @@ -60,6 +60,7 @@ "@opentelemetry/sdk-node": "^0.207.0", "@opentelemetry/sdk-trace-web": "^1.25.1", "@opentelemetry/semantic-conventions": "^1.37.0", + "@pyroscope/nodejs": "^0.4.11", "@remixicon/react": "^4.2.0", "@simplewebauthn/server": "^13.2.2", "@tippyjs/react": "^4.2.6", diff --git a/Telemetry/package-lock.json b/Telemetry/package-lock.json index 15c690b98f..bef61702a8 100644 --- a/Telemetry/package-lock.json +++ b/Telemetry/package-lock.json @@ -52,6 +52,7 @@ "@opentelemetry/sdk-node": "^0.207.0", "@opentelemetry/sdk-trace-web": "^1.25.1", "@opentelemetry/semantic-conventions": "^1.37.0", + "@pyroscope/nodejs": "^0.4.11", "@remixicon/react": "^4.2.0", "@simplewebauthn/server": "^13.2.2", "@tippyjs/react": "^4.2.6", diff --git a/Worker/package-lock.json b/Worker/package-lock.json index 29d834d731..e62070e264 100644 --- a/Worker/package-lock.json +++ b/Worker/package-lock.json @@ -48,6 +48,7 @@ "@opentelemetry/sdk-node": "^0.207.0", "@opentelemetry/sdk-trace-web": "^1.25.1", "@opentelemetry/semantic-conventions": "^1.37.0", + "@pyroscope/nodejs": "^0.4.11", "@remixicon/react": "^4.2.0", "@simplewebauthn/server": "^13.2.2", "@tippyjs/react": "^4.2.6",