mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
feat: enhance UptimeBarTooltip with improved color handling, layout adjustments, and incident display; optimize status breakdown and tooltip styles
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user