From 8b4da15e997233449e10cb7f26ae67ab990a6c5a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 9 Apr 2026 21:07:57 +0000 Subject: [PATCH 1/7] feat: Add explanation tooltip for availability calculation Adds an info icon next to the "Availability by Region" title that shows a popover explaining how availability is calculated, what incidents are included (T0 + availability impact), and the color thresholds. Addresses RELENG-527. Slack thread: https://sentry.slack.com/archives/C08RJKP6NR0/p1775768667336599?thread_ts=1775753083.599719&cid=C08RJKP6NR0 https://claude.ai/code/session_01BUgGWDuKdFSjCNQh6h5zox --- frontend/src/routes/availability/index.tsx | 54 ++++++++++++++++++++-- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/frontend/src/routes/availability/index.tsx b/frontend/src/routes/availability/index.tsx index 0545ac62..f1faab59 100644 --- a/frontend/src/routes/availability/index.tsx +++ b/frontend/src/routes/availability/index.tsx @@ -4,6 +4,8 @@ import {zodValidator} from '@tanstack/zod-adapter'; import {Card} from 'components/Card'; import {ErrorState} from 'components/ErrorState'; import {GetHelpLink} from 'components/GetHelpLink'; +import {Popover, PopoverContent, PopoverTrigger} from 'components/Popover'; +import {Info} from 'lucide-react'; import {z} from 'zod'; import {PeriodLabels} from './components/PeriodLabels'; @@ -73,16 +75,60 @@ function AvailabilityPage() {
-

- Availability by Region -

+
+

+ Availability by Region +

+ + + + + +

+ How availability is calculated +

+

+ Availability percentage is calculated as: +

+

+ (Total Time − Downtime) / Total Time × 100 +

+

+ Only T0 service tier incidents with{' '} + availability impact are included. +

+

+ Color thresholds +

+
    +
  • + + Green: ≥ 99.9% +
  • +
  • + + Yellow: ≥ 99.85% +
  • +
  • + + Red: < 99.85% +
  • +
+
+
+

{getDateRangeLabel(periods)}

- + {regionNames.map(name => ( From 371484670a2c076ecf37353c3217f67995e1adc9 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 10 Apr 2026 16:02:08 +0000 Subject: [PATCH 2/7] feat: Add availability explanation tooltip with spacing and downtime note Adds an info icon next to the "Availability by Region" title with a popover explaining how availability is calculated, what incidents are included, and color thresholds. Also adds a note clarifying that downtime is captured in the month the incident was created. Changes from PR #148: - Increased gap between title and info icon (gap-space-xs -> gap-space-sm) - Added note about downtime attribution to incident creation month Slack thread: https://sentry.slack.com/archives/C08RJKP6NR0/p1775836770308749?thread_ts=1775753083.599719&cid=C08RJKP6NR0 https://claude.ai/code/session_01QCetBNyVqVwzh3oNK6xoyd --- frontend/src/routes/availability/index.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/src/routes/availability/index.tsx b/frontend/src/routes/availability/index.tsx index f1faab59..2e665019 100644 --- a/frontend/src/routes/availability/index.tsx +++ b/frontend/src/routes/availability/index.tsx @@ -75,7 +75,7 @@ function AvailabilityPage() {
-
+

Availability by Region

@@ -98,10 +98,13 @@ function AvailabilityPage() {

(Total Time − Downtime) / Total Time × 100

-

+

Only T0 service tier incidents with{' '} availability impact are included.

+

+ Downtime is captured in the month the incident was created. +

Color thresholds

From cd1597c5c8500e3b06ca8cac3ad6542d170951f6 Mon Sep 17 00:00:00 2001 From: Spencer Murray Date: Fri, 10 Apr 2026 14:30:50 -0400 Subject: [PATCH 3/7] Show tooltip on hover instead of click --- frontend/src/routes/availability/index.tsx | 28 ++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/frontend/src/routes/availability/index.tsx b/frontend/src/routes/availability/index.tsx index 2e665019..280b1776 100644 --- a/frontend/src/routes/availability/index.tsx +++ b/frontend/src/routes/availability/index.tsx @@ -1,3 +1,4 @@ +import {useCallback, useRef, useState} from 'react'; import {useSuspenseQuery} from '@tanstack/react-query'; import {createFileRoute} from '@tanstack/react-router'; import {zodValidator} from '@tanstack/zod-adapter'; @@ -66,6 +67,22 @@ function AvailabilityPage() { const {period: periodParam} = Route.useSearch(); const activePeriod: Period = periodParam ?? 'month'; const {data} = useSuspenseQuery(availabilityQueryOptions()); + const [tooltipOpen, setTooltipOpen] = useState(false); + const closeTimeout = useRef | null>(null); + + const handleMouseEnter = useCallback(() => { + if (closeTimeout.current) { + clearTimeout(closeTimeout.current); + closeTimeout.current = null; + } + setTooltipOpen(true); + }, []); + + const handleMouseLeave = useCallback(() => { + closeTimeout.current = setTimeout(() => { + setTooltipOpen(false); + }, 100); + }, []); const periods = getPeriodsForGranularity(data, activePeriod); const currentPeriod = periods[0]; @@ -79,16 +96,23 @@ function AvailabilityPage() {

Availability by Region

- + - +

How availability is calculated

From 83d0afbcd56675ff096f9a5b03f754dc3c65d6ea Mon Sep 17 00:00:00 2001 From: Spencer Murray Date: Fri, 10 Apr 2026 14:31:03 -0400 Subject: [PATCH 4/7] Update month to time period in attribution text --- frontend/src/routes/availability/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/routes/availability/index.tsx b/frontend/src/routes/availability/index.tsx index 280b1776..f5ce7382 100644 --- a/frontend/src/routes/availability/index.tsx +++ b/frontend/src/routes/availability/index.tsx @@ -127,7 +127,7 @@ function AvailabilityPage() { availability impact are included.

- Downtime is captured in the month the incident was created. + Downtime is captured in the time period the incident was created.

Color thresholds From 9582129b3bbd74ad7c62fc4a8735cfacdc4329ef Mon Sep 17 00:00:00 2001 From: Spencer Murray Date: Fri, 10 Apr 2026 14:32:08 -0400 Subject: [PATCH 5/7] Clean up tooltip timeout on unmount --- frontend/src/routes/availability/index.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/frontend/src/routes/availability/index.tsx b/frontend/src/routes/availability/index.tsx index f5ce7382..52ce02b3 100644 --- a/frontend/src/routes/availability/index.tsx +++ b/frontend/src/routes/availability/index.tsx @@ -1,4 +1,4 @@ -import {useCallback, useRef, useState} from 'react'; +import {useCallback, useEffect, useRef, useState} from 'react'; import {useSuspenseQuery} from '@tanstack/react-query'; import {createFileRoute} from '@tanstack/react-router'; import {zodValidator} from '@tanstack/zod-adapter'; @@ -84,6 +84,14 @@ function AvailabilityPage() { }, 100); }, []); + useEffect(() => { + return () => { + if (closeTimeout.current) { + clearTimeout(closeTimeout.current); + } + }; + }, []); + const periods = getPeriodsForGranularity(data, activePeriod); const currentPeriod = periods[0]; const regionNames = currentPeriod?.regions.map(r => r.name) ?? []; From 9000b3ee2f81964ddce98a02bf1557aac698ae07 Mon Sep 17 00:00:00 2001 From: Spencer Murray Date: Fri, 10 Apr 2026 15:59:37 -0400 Subject: [PATCH 6/7] Use radix tooltip primitive for availability explanation --- frontend/bun.lock | 9 +++ frontend/package.json | 1 + frontend/src/components/Tooltip.tsx | 47 +++++++++++ frontend/src/routes/__root.tsx | 17 ++-- .../components/AvailabilityTooltip.tsx | 52 ++++++++++++ frontend/src/routes/availability/index.tsx | 81 +------------------ 6 files changed, 121 insertions(+), 86 deletions(-) create mode 100644 frontend/src/components/Tooltip.tsx create mode 100644 frontend/src/routes/availability/components/AvailabilityTooltip.tsx diff --git a/frontend/bun.lock b/frontend/bun.lock index 6d303098..ce842f62 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -7,6 +7,7 @@ "dependencies": { "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-tooltip": "^1.2.8", "@sentry/react": "^10.30.0", "@t3-oss/env-core": "^0.13.8", "@tanstack/react-query": "^5.90.12", @@ -296,6 +297,8 @@ "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="], + "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="], @@ -310,6 +313,8 @@ "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="], + "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug=="], + "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.47", "", {}, "sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw=="], @@ -982,6 +987,10 @@ "@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="], + "@radix-ui/react-tooltip/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-visually-hidden/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="], diff --git a/frontend/package.json b/frontend/package.json index 85859854..b9c6514e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,6 +16,7 @@ "dependencies": { "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-tooltip": "^1.2.8", "@sentry/react": "^10.30.0", "@t3-oss/env-core": "^0.13.8", "@tanstack/react-query": "^5.90.12", diff --git a/frontend/src/components/Tooltip.tsx b/frontend/src/components/Tooltip.tsx new file mode 100644 index 00000000..2f917ff6 --- /dev/null +++ b/frontend/src/components/Tooltip.tsx @@ -0,0 +1,47 @@ +import * as TooltipPrimitive from '@radix-ui/react-tooltip'; +import {cn} from 'utils/cn'; + +const TooltipProvider = TooltipPrimitive.Provider; + +const Tooltip = TooltipPrimitive.Root; + +const TooltipTrigger = TooltipPrimitive.Trigger; + +interface TooltipContentProps extends React.ComponentPropsWithoutRef< + typeof TooltipPrimitive.Content +> { + ref?: React.Ref; +} + +function TooltipContent({ + className, + align = 'center', + sideOffset = 4, + ref, + ...props +}: TooltipContentProps) { + return ( + + + + ); +} + +export {TooltipProvider, Tooltip, TooltipTrigger, TooltipContent}; diff --git a/frontend/src/routes/__root.tsx b/frontend/src/routes/__root.tsx index 193596eb..36c33014 100644 --- a/frontend/src/routes/__root.tsx +++ b/frontend/src/routes/__root.tsx @@ -2,17 +2,20 @@ import type {QueryClient} from '@tanstack/react-query'; import {createRootRouteWithContext, Outlet} from '@tanstack/react-router'; import {TanStackRouterDevtools} from '@tanstack/react-router-devtools'; import {ErrorState} from 'components/ErrorState'; +import {TooltipProvider} from 'components/Tooltip'; import {Header} from './components/Header'; const RootLayout = () => ( -
-
-
- -
- -
+ +
+
+
+ +
+ +
+
); export const Route = createRootRouteWithContext<{queryClient: QueryClient}>()({ diff --git a/frontend/src/routes/availability/components/AvailabilityTooltip.tsx b/frontend/src/routes/availability/components/AvailabilityTooltip.tsx new file mode 100644 index 00000000..0b394d6e --- /dev/null +++ b/frontend/src/routes/availability/components/AvailabilityTooltip.tsx @@ -0,0 +1,52 @@ +import {Tooltip, TooltipContent, TooltipTrigger} from 'components/Tooltip'; +import {Info} from 'lucide-react'; + +export function AvailabilityTooltip() { + return ( + + + + + +

+ How availability is calculated +

+

+ Availability percentage is calculated as: +

+

+ (Total Time − Downtime) / Total Time × 100 +

+

+ Only T0 service tier incidents with{' '} + availability impact are included. +

+

+ Downtime is captured in the time period the incident was created. +

+

+ Color thresholds +

+
    +
  • + + Green: ≥ 99.9% +
  • +
  • + + Yellow: ≥ 99.85% +
  • +
  • + + Red: < 99.85% +
  • +
+
+
+ ); +} diff --git a/frontend/src/routes/availability/index.tsx b/frontend/src/routes/availability/index.tsx index 52ce02b3..5b8c6398 100644 --- a/frontend/src/routes/availability/index.tsx +++ b/frontend/src/routes/availability/index.tsx @@ -1,14 +1,12 @@ -import {useCallback, useEffect, useRef, useState} from 'react'; import {useSuspenseQuery} from '@tanstack/react-query'; import {createFileRoute} from '@tanstack/react-router'; import {zodValidator} from '@tanstack/zod-adapter'; import {Card} from 'components/Card'; import {ErrorState} from 'components/ErrorState'; import {GetHelpLink} from 'components/GetHelpLink'; -import {Popover, PopoverContent, PopoverTrigger} from 'components/Popover'; -import {Info} from 'lucide-react'; import {z} from 'zod'; +import {AvailabilityTooltip} from './components/AvailabilityTooltip'; import {PeriodLabels} from './components/PeriodLabels'; import {PeriodTabs} from './components/PeriodTabs'; import {RegionRow} from './components/RegionRow'; @@ -67,30 +65,6 @@ function AvailabilityPage() { const {period: periodParam} = Route.useSearch(); const activePeriod: Period = periodParam ?? 'month'; const {data} = useSuspenseQuery(availabilityQueryOptions()); - const [tooltipOpen, setTooltipOpen] = useState(false); - const closeTimeout = useRef | null>(null); - - const handleMouseEnter = useCallback(() => { - if (closeTimeout.current) { - clearTimeout(closeTimeout.current); - closeTimeout.current = null; - } - setTooltipOpen(true); - }, []); - - const handleMouseLeave = useCallback(() => { - closeTimeout.current = setTimeout(() => { - setTooltipOpen(false); - }, 100); - }, []); - - useEffect(() => { - return () => { - if (closeTimeout.current) { - clearTimeout(closeTimeout.current); - } - }; - }, []); const periods = getPeriodsForGranularity(data, activePeriod); const currentPeriod = periods[0]; @@ -104,58 +78,7 @@ function AvailabilityPage() {

Availability by Region

- - - - - -

- How availability is calculated -

-

- Availability percentage is calculated as: -

-

- (Total Time − Downtime) / Total Time × 100 -

-

- Only T0 service tier incidents with{' '} - availability impact are included. -

-

- Downtime is captured in the time period the incident was created. -

-

- Color thresholds -

-
    -
  • - - Green: ≥ 99.9% -
  • -
  • - - Yellow: ≥ 99.85% -
  • -
  • - - Red: < 99.85% -
  • -
-
-
+

{getDateRangeLabel(periods)} From d6cdef4d5c388fcb82912378dcb8f49fc674ef2d Mon Sep 17 00:00:00 2001 From: Spencer Murray Date: Fri, 10 Apr 2026 16:21:22 -0400 Subject: [PATCH 7/7] Remove broken animation classes from Tooltip component --- frontend/src/components/Tooltip.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/components/Tooltip.tsx b/frontend/src/components/Tooltip.tsx index 2f917ff6..4fbe4ca3 100644 --- a/frontend/src/components/Tooltip.tsx +++ b/frontend/src/components/Tooltip.tsx @@ -35,7 +35,6 @@ function TooltipContent({ 'p-space-md', 'shadow-lg', 'outline-none', - 'animate-in fade-in-0 zoom-in-95', className )} {...props}