feat: enhance UptimeBarTooltip with improved color handling, layout adjustments, and incident display; optimize status breakdown and tooltip styles

This commit is contained in:
Nawaz Dhandala
2026-04-01 15:03:24 +01:00
parent ecbca3208f
commit 16e2c2cb39

View File

@@ -27,88 +27,92 @@ const UptimeBarTooltip: FunctionComponent<ComponentProps> = (
const dateStr: string =
OneUptimeDate.getDateAsUserFriendlyLocalFormattedString(props.date, true);
const uptimeColor: string =
props.uptimePercent >= 99.9
? "#16a34a"
: props.uptimePercent >= 99
? "#ca8a04"
: "#dc2626";
// Color tiers
const isGood: boolean = props.uptimePercent >= 99.9;
const isWarn: boolean = !isGood && props.uptimePercent >= 99;
const uptimeColor: string = isGood
? "#059669"
: isWarn
? "#d97706"
: "#dc2626";
const uptimeBgColor: string =
props.uptimePercent >= 99.9
? "#f0fdf4"
: props.uptimePercent >= 99
? "#fefce8"
: "#fef2f2";
const uptimeBg: string = isGood
? "#ecfdf5"
: isWarn
? "#fffbeb"
: "#fef2f2";
const uptimeTrackColor: string =
props.uptimePercent >= 99.9
? "#dcfce7"
: props.uptimePercent >= 99
? "#fef9c3"
: "#fee2e2";
const uptimeTrack: string = isGood
? "#d1fae5"
: isWarn
? "#fef3c7"
: "#fecaca";
// Sort: downtime statuses first so they're prominent
// Sort: downtime first, then by duration desc
const sortedDurations: Array<StatusDuration> = [
...props.statusDurations,
].sort((a: StatusDuration, b: StatusDuration) => {
if (a.isDowntime && !b.isDowntime) {
return -1;
}
if (!a.isDowntime && b.isDowntime) {
return 1;
if (a.isDowntime !== b.isDowntime) {
return a.isDowntime ? -1 : 1;
}
return b.seconds - a.seconds;
});
const totalSeconds: number = sortedDurations.reduce(
(sum: number, d: StatusDuration) => {
return sum + d.seconds;
},
0,
);
const hasIncidents: boolean = props.incidents.length > 0;
const hasStatuses: boolean = sortedDurations.length > 0;
return (
<div style={{ minWidth: "270px", maxWidth: "340px" }}>
{/* ── Header ── */}
<div
style={{
minWidth: "280px",
maxWidth: "340px",
fontFamily:
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
}}
>
{/* ── Date header ── */}
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
paddingBottom: "10px",
marginBottom: props.hasEvents ? "0" : "8px",
borderBottom: props.hasEvents ? "none" : "1px solid #e5e7eb",
marginBottom: "12px",
}}
>
<span style={{ fontWeight: 600, fontSize: "13px", color: "#111827" }}>
<span
style={{
fontWeight: 600,
fontSize: "13px",
color: "#1f2937",
letterSpacing: "-0.01em",
}}
>
{dateStr}
</span>
{props.hasEvents && props.incidents.length === 0 && (
<span
style={{
fontSize: "10px",
fontWeight: 600,
color: uptimeColor,
backgroundColor: uptimeBgColor,
padding: "2px 8px",
borderRadius: "9999px",
lineHeight: "1.5",
}}
>
{props.uptimePercent >= 100 ? "100%" : props.uptimePercent.toFixed(2) + "%"}
</span>
)}
</div>
{/* ── Uptime meter ── */}
{props.hasEvents && (
<div
style={{
backgroundColor: uptimeBgColor,
backgroundColor: uptimeBg,
borderRadius: "10px",
padding: "12px 14px",
marginBottom: sortedDurations.length > 0 || props.incidents.length > 0 ? "12px" : "0",
padding: "10px 12px",
marginBottom: hasStatuses || hasIncidents ? "12px" : "0",
}}
>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "baseline",
alignItems: "center",
marginBottom: "8px",
}}
>
@@ -116,46 +120,97 @@ const UptimeBarTooltip: FunctionComponent<ComponentProps> = (
style={{
fontSize: "11px",
color: "#6b7280",
fontWeight: 500,
fontWeight: 600,
textTransform: "uppercase",
letterSpacing: "0.04em",
letterSpacing: "0.05em",
}}
>
Uptime
</span>
<span
style={{
fontSize: "20px",
fontSize: "22px",
fontWeight: 700,
color: uptimeColor,
fontVariantNumeric: "tabular-nums",
lineHeight: 1,
letterSpacing: "-0.02em",
}}
>
{props.uptimePercent >= 100
? "100"
: props.uptimePercent.toFixed(2)}
<span style={{ fontSize: "13px", fontWeight: 600 }}>%</span>
<span
style={{
fontSize: "12px",
fontWeight: 600,
marginLeft: "1px",
opacity: 0.8,
}}
>
%
</span>
</span>
</div>
<div
style={{
width: "100%",
height: "6px",
backgroundColor: uptimeTrackColor,
borderRadius: "100px",
overflow: "hidden",
}}
>
{/* Segmented bar showing all statuses proportionally */}
{totalSeconds > 0 && sortedDurations.length > 1 ? (
<div
style={{
width: `${Math.min(props.uptimePercent, 100)}%`,
height: "100%",
backgroundColor: uptimeColor,
width: "100%",
height: "6px",
borderRadius: "100px",
overflow: "hidden",
display: "flex",
gap: "1px",
backgroundColor: uptimeTrack,
}}
/>
</div>
>
{sortedDurations.map(
(status: StatusDuration, index: number) => {
const widthPercent: number =
(status.seconds / totalSeconds) * 100;
if (widthPercent < 0.5) {
return null;
}
return (
<div
key={index}
style={{
width: `${widthPercent}%`,
height: "100%",
backgroundColor: status.color.toString(),
borderRadius:
index === 0
? "100px 0 0 100px"
: index === sortedDurations.length - 1
? "0 100px 100px 0"
: "0",
}}
/>
);
},
)}
</div>
) : (
<div
style={{
width: "100%",
height: "6px",
backgroundColor: uptimeTrack,
borderRadius: "100px",
overflow: "hidden",
}}
>
<div
style={{
width: `${Math.min(props.uptimePercent, 100)}%`,
height: "100%",
backgroundColor: uptimeColor,
borderRadius: "100px",
}}
/>
</div>
)}
</div>
)}
@@ -165,10 +220,25 @@ const UptimeBarTooltip: FunctionComponent<ComponentProps> = (
style={{
backgroundColor: "#f9fafb",
borderRadius: "10px",
padding: "16px",
padding: "14px 16px",
textAlign: "center",
}}
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="#d1d5db"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
style={{ margin: "0 auto 6px" }}
>
<circle cx="12" cy="12" r="10" />
<line x1="12" y1="8" x2="12" y2="12" />
<line x1="12" y1="16" x2="12.01" y2="16" />
</svg>
<div
style={{
fontSize: "12px",
@@ -182,13 +252,12 @@ const UptimeBarTooltip: FunctionComponent<ComponentProps> = (
)}
{/* ── Status breakdown ── */}
{sortedDurations.length > 0 && (
{hasStatuses && (
<div
style={{
marginBottom: props.incidents.length > 0 ? "0" : "0",
paddingBottom: props.incidents.length > 0 ? "10px" : "0",
borderBottom:
props.incidents.length > 0 ? "1px solid #e5e7eb" : "none",
paddingBottom: hasIncidents ? "10px" : "0",
marginBottom: hasIncidents ? "2px" : "0",
borderBottom: hasIncidents ? "1px solid #f3f4f6" : "none",
}}
>
<div
@@ -198,12 +267,16 @@ const UptimeBarTooltip: FunctionComponent<ComponentProps> = (
textTransform: "uppercase",
letterSpacing: "0.06em",
fontWeight: 600,
marginBottom: "6px",
marginBottom: "4px",
}}
>
Status Breakdown
</div>
{sortedDurations.map((status: StatusDuration, index: number) => {
const pct: string =
totalSeconds > 0
? ((status.seconds / totalSeconds) * 100).toFixed(1)
: "0";
return (
<div
key={index}
@@ -211,25 +284,26 @@ const UptimeBarTooltip: FunctionComponent<ComponentProps> = (
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "4px 0",
padding: "3px 0",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
gap: "8px",
gap: "7px",
flex: 1,
minWidth: 0,
}}
>
<span
style={{
width: "8px",
height: "8px",
width: "7px",
height: "7px",
borderRadius: "50%",
backgroundColor: status.color.toString(),
display: "inline-block",
flexShrink: 0,
boxShadow: `0 0 0 2px ${status.color.toString()}25`,
}}
/>
<span
@@ -241,13 +315,24 @@ const UptimeBarTooltip: FunctionComponent<ComponentProps> = (
>
{status.label}
</span>
<span
style={{
fontSize: "10px",
color: "#d1d5db",
fontWeight: 400,
}}
>
{pct}%
</span>
</div>
<span
style={{
fontSize: "12px",
fontSize: "11px",
color: status.isDowntime ? "#dc2626" : "#6b7280",
fontWeight: status.isDowntime ? 600 : 400,
fontVariantNumeric: "tabular-nums",
whiteSpace: "nowrap",
marginLeft: "12px",
}}
>
{OneUptimeDate.secondsToFormattedFriendlyTimeString(
@@ -261,42 +346,51 @@ const UptimeBarTooltip: FunctionComponent<ComponentProps> = (
)}
{/* ── Incidents ── */}
{props.incidents.length > 0 && (
{hasIncidents && (
<div style={{ paddingTop: "10px" }}>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: "6px",
marginBottom: "8px",
}}
>
<div
{/* small warning triangle icon */}
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="#dc2626"
style={{ flexShrink: 0, opacity: 0.8 }}
>
<path d="M12 2L1 21h22L12 2zm0 4l7.53 13H4.47L12 6zm-1 5v4h2v-4h-2zm0 6v2h2v-2h-2z" />
</svg>
<span
style={{
fontSize: "10px",
color: "#9ca3af",
textTransform: "uppercase",
letterSpacing: "0.06em",
fontWeight: 600,
flex: 1,
}}
>
Incidents
</div>
<div
</span>
<span
style={{
fontSize: "10px",
fontWeight: 700,
color: "#dc2626",
backgroundColor: "#fef2f2",
padding: "1px 8px",
padding: "1px 7px",
borderRadius: "9999px",
lineHeight: "1.6",
minWidth: "20px",
textAlign: "center",
}}
>
{props.incidents.length}
</div>
</span>
</div>
{props.incidents.slice(0, 3).map(
@@ -315,77 +409,65 @@ const UptimeBarTooltip: FunctionComponent<ComponentProps> = (
: undefined
}
style={{
backgroundColor: "#f9fafb",
border: "1px solid #e5e7eb",
border: "1px solid #f3f4f6",
borderRadius: "8px",
padding: "8px 10px",
marginBottom: "6px",
padding: "7px 10px",
marginBottom: "5px",
cursor: isClickable ? "pointer" : "default",
transition: "all 0.15s ease",
transition: "all 0.12s ease",
backgroundColor: "#ffffff",
}}
onMouseEnter={(e: React.MouseEvent<HTMLDivElement>) => {
if (isClickable) {
(e.currentTarget as HTMLDivElement).style.backgroundColor =
"#f3f4f6";
(e.currentTarget as HTMLDivElement).style.borderColor =
"#d1d5db";
const el: HTMLDivElement =
e.currentTarget as HTMLDivElement;
el.style.backgroundColor = "#f9fafb";
el.style.borderColor = "#e5e7eb";
}
}}
onMouseLeave={(e: React.MouseEvent<HTMLDivElement>) => {
if (isClickable) {
(e.currentTarget as HTMLDivElement).style.backgroundColor =
"#f9fafb";
(e.currentTarget as HTMLDivElement).style.borderColor =
"#e5e7eb";
const el: HTMLDivElement =
e.currentTarget as HTMLDivElement;
el.style.backgroundColor = "#ffffff";
el.style.borderColor = "#f3f4f6";
}
}}
>
{/* Title row */}
<div
style={{
display: "flex",
alignItems: "flex-start",
justifyContent: "space-between",
gap: "8px",
alignItems: "center",
gap: "6px",
}}
>
<div style={{ flex: 1, minWidth: 0 }}>
<div
style={{
fontSize: "12px",
color: isClickable ? "#2563eb" : "#111827",
fontWeight: 600,
lineHeight: "1.4",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{incident.title}
</div>
<div
style={{
fontSize: "10px",
color: "#9ca3af",
marginTop: "2px",
}}
>
{OneUptimeDate.getDateAsUserFriendlyLocalFormattedString(
incident.declaredAt,
false,
)}
</div>
<div
style={{
flex: 1,
minWidth: 0,
fontSize: "12px",
color: isClickable ? "#2563eb" : "#111827",
fontWeight: 600,
lineHeight: "1.3",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{incident.title}
</div>
{isClickable && (
<svg
width="14"
height="14"
width="12"
height="12"
viewBox="0 0 16 16"
fill="none"
style={{ flexShrink: 0, marginTop: "2px" }}
style={{ flexShrink: 0, opacity: 0.4 }}
>
<path
d="M6 3l5 5-5 5"
stroke="#9ca3af"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
@@ -393,13 +475,14 @@ const UptimeBarTooltip: FunctionComponent<ComponentProps> = (
</svg>
)}
</div>
{/* Meta row: badges */}
<div
style={{
display: "flex",
alignItems: "center",
gap: "5px",
gap: "4px",
flexWrap: "wrap",
marginTop: "5px",
marginTop: "4px",
}}
>
{incident.incidentSeverity && (
@@ -409,11 +492,10 @@ const UptimeBarTooltip: FunctionComponent<ComponentProps> = (
fontWeight: 600,
color: incident.incidentSeverity.color.toString(),
backgroundColor:
incident.incidentSeverity.color.toString() + "12",
border: `1px solid ${incident.incidentSeverity.color.toString()}25`,
padding: "1px 7px",
borderRadius: "9999px",
lineHeight: "1.6",
incident.incidentSeverity.color.toString() + "10",
padding: "0px 6px",
borderRadius: "4px",
lineHeight: "1.7",
}}
>
{incident.incidentSeverity.name}
@@ -428,16 +510,28 @@ const UptimeBarTooltip: FunctionComponent<ComponentProps> = (
incident.currentIncidentState.color.toString(),
backgroundColor:
incident.currentIncidentState.color.toString() +
"12",
border: `1px solid ${incident.currentIncidentState.color.toString()}25`,
padding: "1px 7px",
borderRadius: "9999px",
lineHeight: "1.6",
"10",
padding: "0px 6px",
borderRadius: "4px",
lineHeight: "1.7",
}}
>
{incident.currentIncidentState.name}
</span>
)}
<span
style={{
fontSize: "10px",
color: "#c0c0c0",
marginLeft: "auto",
whiteSpace: "nowrap",
}}
>
{OneUptimeDate.getDateAsUserFriendlyLocalFormattedString(
incident.declaredAt,
false,
)}
</span>
</div>
</div>
);
@@ -448,14 +542,13 @@ const UptimeBarTooltip: FunctionComponent<ComponentProps> = (
<div
style={{
fontSize: "11px",
color: "#6b7280",
color: "#9ca3af",
textAlign: "center",
padding: "4px 0 2px",
padding: "3px 0 0",
fontWeight: 500,
}}
>
+{props.incidents.length - 3} more incident
{props.incidents.length - 3 !== 1 ? "s" : ""}
+{props.incidents.length - 3} more
</div>
)}
</div>