feat: restore ability to dismiss incident banners (#43249)

## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.

YES

## What kind of change does this PR introduce?

Feature restoration

## What is the current behavior?

Incident banners cannot be dismissed by users.

## What is the new behavior?

Users can dismiss incident banners again.

## Additional context
This commit is contained in:
Charis
2026-03-02 01:59:14 -05:00
committed by GitHub
parent 5644beebf1
commit 941de105b8
5 changed files with 204 additions and 16 deletions

View File

@@ -16,5 +16,12 @@ export const StatusPageBanner = () => {
if (!banner) return null
return <HeaderBanner variant="warning" title={banner.title} description={BANNER_DESCRIPTION} />
return (
<HeaderBanner
variant="warning"
title={banner.title}
description={BANNER_DESCRIPTION}
onDismiss={banner.dismiss}
/>
)
}

View File

@@ -1,18 +1,22 @@
import { describe, expect, it } from 'vitest'
import { shouldShowBanner } from './StatusPageBanner.utils'
import { getRelevantIncidentIds, shouldShowBanner } from './StatusPageBanner.utils'
const noCache = { cache: null } as const
const noCache = { id: 'no-cache', cache: null } as const
const noRestrictions = {
id: 'no-restrictions',
cache: { affected_regions: null, affects_project_creation: false },
}
const affectsCreation = {
id: 'affects-creation',
cache: { affected_regions: null, affects_project_creation: true },
}
const usEast1Only = {
id: 'us-east-1-only',
cache: { affected_regions: ['us-east-1'], affects_project_creation: false },
}
const usEast1AndCreation = {
id: 'us-east-1-and-creation',
cache: { affected_regions: ['us-east-1'], affects_project_creation: true },
}
@@ -87,7 +91,9 @@ describe('shouldShowBanner', () => {
it('shows when affected_regions is an empty array', () => {
expect(
shouldShowBanner({
incidents: [{ cache: { affected_regions: [], affects_project_creation: false } }],
incidents: [
{ id: 'test', cache: { affected_regions: [], affects_project_creation: false } },
],
hasProjects: true,
userRegions: new Set(['us-east-1']),
})
@@ -110,7 +116,10 @@ describe('shouldShowBanner', () => {
expect(
shouldShowBanner({
incidents: [
{ cache: { affected_regions: ['eu-west-1'], affects_project_creation: false } },
{
id: 'test',
cache: { affected_regions: ['eu-west-1'], affects_project_creation: false },
},
],
hasProjects: true,
userRegions: new Set(['us-east-1', 'eu-west-1']),
@@ -123,6 +132,7 @@ describe('shouldShowBanner', () => {
shouldShowBanner({
incidents: [
{
id: 'test',
cache: {
affected_regions: ['us-east-1', 'ap-southeast-1'],
affects_project_creation: false,
@@ -223,3 +233,107 @@ describe('shouldShowBanner', () => {
})
})
})
describe('getRelevantIncidentIds', () => {
it('returns empty array when there are no incidents', () => {
expect(
getRelevantIncidentIds({ incidents: [], hasProjects: true, userRegions: new Set() })
).toEqual([])
})
it('returns empty array when no incidents are relevant to the user', () => {
expect(
getRelevantIncidentIds({
incidents: [usEast1Only],
hasProjects: true,
userRegions: new Set(['eu-west-1']),
})
).toEqual([])
})
it('returns the ID of a single relevant incident', () => {
expect(
getRelevantIncidentIds({
incidents: [noRestrictions],
hasProjects: true,
userRegions: new Set(['us-east-1']),
})
).toEqual(['no-restrictions'])
})
it('returns IDs of all relevant incidents', () => {
const euWest1Only = {
id: 'eu-west-1-only',
cache: { affected_regions: ['eu-west-1'], affects_project_creation: false },
}
const apSoutheast1Only = {
id: 'ap-southeast-1-only',
cache: { affected_regions: ['ap-southeast-1'], affects_project_creation: false },
}
expect(
getRelevantIncidentIds({
incidents: [euWest1Only, apSoutheast1Only],
hasProjects: true,
userRegions: new Set(['eu-west-1', 'ap-southeast-1']),
})
).toEqual(expect.arrayContaining(['ap-southeast-1-only', 'eu-west-1-only']))
})
it('excludes incidents irrelevant to the user from the result', () => {
// User is in eu-west-1; us-east-1-only incident should not be included
expect(
getRelevantIncidentIds({
incidents: [usEast1Only, noRestrictions],
hasProjects: true,
userRegions: new Set(['eu-west-1']),
})
).toEqual(['no-restrictions'])
})
describe('user has no projects', () => {
it('includes incidents with affects_project_creation', () => {
expect(
getRelevantIncidentIds({
incidents: [affectsCreation],
hasProjects: false,
userRegions: new Set(),
})
).toEqual(['affects-creation'])
})
it('excludes incidents without affects_project_creation', () => {
expect(
getRelevantIncidentIds({
incidents: [noRestrictions, usEast1Only],
hasProjects: false,
userRegions: new Set(),
})
).toEqual([])
})
})
describe('hasUnknownRegions', () => {
it('includes region-restricted incidents when regions are unknown', () => {
expect(
getRelevantIncidentIds({
incidents: [usEast1Only],
hasProjects: true,
userRegions: new Set(),
hasUnknownRegions: true,
})
).toEqual(['us-east-1-only'])
})
it('still excludes incidents for no-project users even when regions are unknown', () => {
expect(
getRelevantIncidentIds({
incidents: [usEast1Only],
hasProjects: false,
userRegions: new Set(),
hasUnknownRegions: true,
})
).toEqual([])
})
})
})

View File

@@ -1,6 +1,6 @@
import type { IncidentCache } from 'lib/api/incident-status'
type BannerIncident = { cache?: IncidentCache | null }
type BannerIncident = { id: string; cache?: IncidentCache | null }
/**
* Determines whether the incident status banner should be shown to a given user,
@@ -42,3 +42,27 @@ export function shouldShowBanner({
return affectedRegions.some((region) => userRegions.has(region))
})
}
/**
* Returns the IDs of incidents that are relevant to the given user.
*
* An incident is considered relevant if it would trigger banner visibility for
* the user, per the same logic as shouldShowBanner.
*/
export function getRelevantIncidentIds({
incidents,
hasProjects,
userRegions,
hasUnknownRegions = false,
}: {
incidents: Array<BannerIncident>
hasProjects: boolean
userRegions: Set<string>
hasUnknownRegions?: boolean
}): Array<string> {
return incidents
.filter((incident) =>
shouldShowBanner({ incidents: [incident], hasProjects, userRegions, hasUnknownRegions })
)
.map((incident) => incident.id)
}

View File

@@ -1,7 +1,8 @@
import { useQueries } from '@tanstack/react-query'
import { useFlag } from 'common'
import { LOCAL_STORAGE_KEYS, useFlag } from 'common'
import { useCallback, useMemo } from 'react'
import { shouldShowBanner } from './StatusPageBanner.utils'
import { getRelevantIncidentIds, shouldShowBanner } from './StatusPageBanner.utils'
import { useOrganizationsQuery } from '@/data/organizations/organizations-query'
import { useIncidentStatusQuery } from '@/data/platform/incident-status-query'
import { projectKeys } from '@/data/projects/keys'
@@ -9,8 +10,9 @@ import {
getOrganizationProjects,
type OrgProject,
} from '@/data/projects/org-projects-infinite-query'
import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
export type StatusPageBannerData = { title: string }
export type StatusPageBannerData = { title: string; dismiss?: () => void }
export function useStatusPageBannerVisibility(): StatusPageBannerData | null {
const showIncidentBannerOverride =
@@ -40,22 +42,63 @@ export function useStatusPageBannerVisibility(): StatusPageBannerData | null {
const allProjects = orgProjectsQueries.flatMap((q) => q.data?.projects ?? [])
const hasProjects = allProjects.length > 0
const userRegions = new Set(
allProjects.flatMap((project: OrgProject) => project.databases.map((db) => db.region))
const userRegions = useMemo(
() =>
new Set(
allProjects.flatMap((project: OrgProject) => project.databases.map((db) => db.region))
),
[allProjects]
)
const hasUnknownRegions = orgProjectsQueries.some(
(q) => q.isError || (q.data !== undefined && q.data.pagination.count > q.data.projects.length)
)
const [dismissedIds, setDismissedIds, { isSuccess: isDismissedLoaded }] = useLocalStorageQuery<
Array<string>
>(LOCAL_STORAGE_KEYS.INCIDENT_BANNER_DISMISSED_IDS, [])
const dismiss = useCallback(() => {
const activeIncidentIds = new Set(incidents.map((i) => i.id))
const relevantIds = getRelevantIncidentIds({
incidents,
hasProjects,
userRegions,
hasUnknownRegions,
})
setDismissedIds((prev) => [
...new Set([...prev.filter((id) => activeIncidentIds.has(id)), ...relevantIds]),
])
}, [incidents, hasProjects, userRegions, hasUnknownRegions, setDismissedIds])
if (showIncidentBannerOverride) return { title: 'We are investigating a technical issue' }
if (!hasActiveIncidents || !isProjectsFetched) return null
if (!shouldShowBanner({ incidents, hasProjects, userRegions, hasUnknownRegions })) return null
// Filter out individually dismissed incidents. An incident stays dismissed as
// long as its ID remains in the stored set, regardless of whether other
// incidents are added or removed.
const dismissedIdSet = new Set(dismissedIds)
const undismissedIncidents = incidents.filter((i) => !dismissedIdSet.has(i.id))
// If dismissed state hasn't loaded yet, hide to prevent a flash of the banner.
// If all relevant incidents have been dismissed, hide the banner.
if (
!isDismissedLoaded ||
!shouldShowBanner({
incidents: undismissedIncidents,
hasProjects,
userRegions,
hasUnknownRegions,
})
)
return null
const title = hasProjects
? 'We are investigating a technical issue'
: 'Project creation may be impacted in some regions'
return {
title: hasProjects
? 'We are investigating a technical issue'
: 'Project creation may be impacted in some regions',
title,
dismiss,
}
}

View File

@@ -11,7 +11,7 @@ export const LOCAL_STORAGE_KEYS = {
PROJECTS_SORT: 'projects-sort',
FEEDBACK_WIDGET_CONTENT: 'feedback-widget-content',
FEEDBACK_WIDGET_SCREENSHOT: 'feedback-widget-screenshot',
INCIDENT_BANNER_DISMISSED: (id: string) => `incident-banner-dismissed-${id}`,
INCIDENT_BANNER_DISMISSED_IDS: 'incident-banner-dismissed-ids',
MAINTENANCE_BANNER_DISMISSED: (id: string) => `maintenance-banner-dismissed-${id}`,
UI_PREVIEW_API_SIDE_PANEL: 'supabase-ui-api-side-panel',