Files
supabase/apps/studio/components/layouts/AppLayout/StatusPageBanner.utils.ts
Charis d22702e907 refactor(studio): extract user project regions + emergency override hooks (#51033)
## Summary
- Extracts `useUserProjectRegions` (org + project region aggregation,
fail-open on fetch errors) and `useEmergencyIncidentOverride` (the
`ongoingIncident` flag / env var check) out of
`useStatusPageBannerVisibility`, so the upcoming incident.io status-page
banner can reuse both without duplicating the org/project fan-out logic.
- No behavior change for the legacy banner except one intentional fix:
`StatusPageBanner.utils.ts` compared the incident's affected regions
(lowercased) against the user's regions (not normalized), so a
mixed-case region on either side could silently fail to match. Both
sides now go through the same `normalizeRegion` helper.
- Part of the Linear FE-4057 stack (PR 5a of 6). Base branch is PR 4
(`charis/fe-4057-pr4-project-creation-status-admonition`), not master.

Linear: FE-4057

## Test plan
- [x] `pnpm --filter studio run typecheck`
- [x] `pnpm --filter studio run lint:ratchet`
- [x] `pnpm knip --workspace apps/studio`
- [x] `pnpm test:prettier`
- [x] `pnpm --filter studio exec vitest run` on the touched test files
and the `hooks/misc/` and `components/layouts/AppLayout/` directories
(all passing, no regressions)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Improved status banner targeting by matching incidents against
normalized regions across the user’s projects.
* Updated banner visibility checks to handle incomplete project or
region data, including when project information fails to load.
* Improved loading behavior so the banner can be evaluated based on
user-region data rather than waiting for separate organization and
project requests.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-29 17:49:06 -04:00

76 lines
2.8 KiB
TypeScript

import type { IncidentCache } from '@/lib/api/incident-status'
import { normalizeRegion } from '@/lib/status-page/status-page.utils'
type BannerIncident = { id: string; cache?: IncidentCache | null }
/**
* Determines whether the incident status banner should be shown to a given user,
* given all active incidents and the user's project state.
*
* Returns true if any incident matches the user's context.
*
* @param incidents - Active incidents from the incident-status endpoint
* @param hasProjects - Whether the user has any projects at all
* @param userRegions - Deduplicated set of regions of all databases (primary and read replicas)
* owned by the user, normalized via normalizeRegion
* @param hasUnknownRegions - True when region data is incomplete (an org has more than 100
* projects, or an org's projects failed to load). When true, the region check is skipped and
* a match is assumed.
*/
export function shouldShowBanner({
incidents,
hasProjects,
userRegions,
hasUnknownRegions = false,
}: {
incidents: Array<BannerIncident>
hasProjects: boolean
userRegions: ReadonlySet<string>
hasUnknownRegions?: boolean
}): boolean {
return incidents.some((incident) => {
// Forced incidents are shown unconditionally, regardless of regions or project state
if (incident.cache?.force) return true
const affectedRegions = incident.cache?.affected_regions ?? []
const affectsProjectCreation = incident.cache?.affects_project_creation ?? false
// Users with no projects only see the banner if the incident affects project creation
// and has no specific region targeting (inline notice in RegionSelector handles region-specific incidents)
if (!hasProjects) return affectsProjectCreation && affectedRegions.length === 0
// User has projects: if no region restriction, always show
if (affectedRegions.length === 0) return true
// Region data is incomplete — assume the user has a database in an affected region
if (hasUnknownRegions) return true
// Region restriction: only show if the user has a database in an affected region
return affectedRegions.some((region) => userRegions.has(normalizeRegion(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: ReadonlySet<string>
hasUnknownRegions?: boolean
}): Array<string> {
return incidents
.filter((incident) =>
shouldShowBanner({ incidents: [incident], hasProjects, userRegions, hasUnknownRegions })
)
.map((incident) => incident.id)
}