mirror of
https://github.com/supabase/supabase.git
synced 2026-05-07 06:27:16 +08:00
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:
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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([])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user