feat(studio): project creation status admonition (#51029)

## Summary

- Adds a new project-creation-form incident warning path behind the
`incidentIoStatusPage` ConfigCat flag (default off — no visible behavior
change until the flag is enabled)
- When the flag is on, reads the new `/api/status-page` endpoint (added
earlier in this stack) instead of the legacy `/api/incident-status`
endpoint, and only fetches the legacy endpoint when the flag is off
- Extracts region-matching logic into `RegionSelector.utils.ts`
(`regionMatches`, `getItemsAffectingProjectCreation`) with unit test
coverage

See Linear FE-4057 for full context and design.

## 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
components/interfaces/ProjectCreation/RegionSelector.utils.test.ts`
(35/35 passing)

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

* **New Features**
* Project creation displays a warning and links to the status page when
an incident or maintenance event may affect the selected region.
* Status notices match exact regions and broader smart-region groupings;
global project-creation notices are also shown.
* The AI assistant can report visible, active incidents and in-progress
maintenance, including affected components and update details. When
there are no active events, it reports that status.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Charis authored and GitHub committed 2026-09-29 17:37:01 -04:00
1 parent 4a941518e3
commit 53b20cad5e
4 files changed
+213 -39

No files matched your search

@@ -0,0 +1,41 @@
import { useQuery } from '@tanstack/react-query'
import { Admonition } from 'ui-patterns/Admonition'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { getItemsAffectingProjectCreation } from './RegionSelector.utils'
import { InlineLink } from '@/components/ui/InlineLink'
import { statusPageQueryOptions } from '@/data/platform/status-page-query'
import { normalizeStatusPage } from '@/lib/status-page/status-page.utils'
export const ProjectCreationStatusAdmonition = ({
selectedRegionCode,
}: {
selectedRegionCode?: string
}) => {
const { data } = useQuery({ ...statusPageQueryOptions(), select: normalizeStatusPage })
if (!data) return null
const items = getItemsAffectingProjectCreation(data.items, selectedRegionCode)
const item = items[0]
if (!item) return null
const isMaintenance = item.kind === 'maintenance'
const isRegionSpecific = item.projectCreationScope?.type === 'regions'
return (
<FormItemLayout layout="horizontal">
<Admonition
type="warning"
title={isMaintenance ? 'Maintenance in progress' : 'Incident in progress'}
description={
<>
{isMaintenance ? 'Maintenance' : 'An incident'} may affect project creation
{isRegionSpecific ? ' in this region' : ''}. Follow updates on{' '}
<InlineLink href={data.pageUrl}>status page</InlineLink>.
</>
}
className="mt-3"
/>
</FormItemLayout>
)
}
@@ -1,4 +1,4 @@
import { useFeatureFlags, useParams } from 'common'
import { useFeatureFlags, useFlag, useParams } from 'common'
import { Loader2, ThumbsUp } from 'lucide-react'
import { useEffect, useRef } from 'react'
import { UseFormReturn } from 'react-hook-form'
@@ -30,7 +30,12 @@ import {
getAvailableRegions,
getHighAvailabilityRegionCode,
} from './ProjectCreation.utils'
import { getRegionRestrictionCopy, SELECT_DIFFERENT_REGION } from './RegionSelector.utils'
import { ProjectCreationStatusAdmonition } from './ProjectCreationStatusAdmonition'
import {
getRegionRestrictionCopy,
regionMatches,
SELECT_DIFFERENT_REGION,
} from './RegionSelector.utils'
import { useRegionRestriction } from './useRegionRestriction'
import { AlertError } from '@/components/ui/AlertError'
import { InlineLink } from '@/components/ui/InlineLink'
@@ -55,18 +60,6 @@ interface RegionSelectorProps {
// I tried using https://flagpack.xyz/docs/development/react/ but couldn't get it to render
// ^ can try again next time
// Maps smart region group codes to the specific-region code prefixes they contain.
// Used to check whether an incident affecting specific regions also affects a smart region selection.
const SMART_REGION_PREFIXES: Record<string, Array<string>> = {
americas: ['us-', 'ca-', 'sa-'],
emea: ['eu-', 'me-', 'af-'],
apac: ['ap-'],
}
function smartRegionMatchesSpecific(smartCode: string, specificCode: string): boolean {
return (SMART_REGION_PREFIXES[smartCode] ?? []).some((prefix) => specificCode.startsWith(prefix))
}
// Map backend region names to user-friendly display names
const getDisplayNameForSmartRegion = (name: string): string => {
if (name === 'APAC') {
@@ -100,10 +93,11 @@ export const RegionSelector = ({
const { hasLoaded: flagsLoaded } = useFeatureFlags()
const smartRegionEnabled = cloudProvider !== 'AWS_NIMBUS'
const isStatusPageEnabled = useFlag('incidentIoStatusPage') === true
const { getRegionRestriction } = useRegionRestriction()
const { data: statusData } = useIncidentStatusQuery()
const { data: statusData } = useIncidentStatusQuery({ enabled: !isStatusPageEnabled })
const { incidents = [] } = statusData ?? {}
const { isPending: isLoadingDefaultRegion } = useDefaultRegionQuery(
@@ -250,12 +244,8 @@ export const RegionSelector = ({
const affectedRegions = incident.cache?.affected_regions ?? []
if (affectedRegions.length === 0 || selectedRegion?.code === undefined) return false
// Specific region: direct code match
if (affectedRegions.includes(selectedRegion.code)) return true
// Smart region: match if any affected region falls within the smart group
return affectedRegions.some((specificCode) =>
smartRegionMatchesSpecific(selectedRegion.code, specificCode)
return affectedRegions.some((affectedRegion) =>
regionMatches(selectedRegion.code, affectedRegion)
)
})
@@ -447,24 +437,28 @@ export const RegionSelector = ({
</FormControl>
</FormItemLayout>
{affectingIncidents.length > 0 && (
<FormItemLayout layout="horizontal">
<Admonition
type="warning"
title="Incident in progress for this region"
description={
<>
We're currently investigating an issue that may impact projects in this
region. Follow updates on{' '}
<InlineLink href="https://status.supabase.com">
status.supabase.com
</InlineLink>
.
</>
}
className="mt-3"
/>
</FormItemLayout>
{isStatusPageEnabled ? (
<ProjectCreationStatusAdmonition selectedRegionCode={selectedRegion?.code} />
) : (
affectingIncidents.length > 0 && (
<FormItemLayout layout="horizontal">
<Admonition
type="warning"
title="Incident in progress for this region"
description={
<>
We're currently investigating an issue that may impact projects in this
region. Follow updates on{' '}
<InlineLink href="https://status.supabase.com">
status.supabase.com
</InlineLink>
.
</>
}
className="mt-3"
/>
</FormItemLayout>
)
)}
{selectedRestrictionCopy !== undefined && (
@@ -1,13 +1,16 @@
import { describe, expect, it, vi } from 'vitest'
import {
getItemsAffectingProjectCreation,
getRegionRestrictionCopy,
getRegionRestrictionMessage,
parseRestrictedRegions,
REGION_RESTRICTION_COPY,
regionMatches,
resolveRegionRestriction,
SELECT_DIFFERENT_REGION,
} from './RegionSelector.utils'
import type { StatusItem } from '@/lib/status-page/status-page.utils'
vi.mock('@/lib/error-reporting', () => ({ captureCriticalError: vi.fn() }))
@@ -103,3 +106,106 @@ describe('getRegionRestrictionCopy', () => {
)
})
})
describe('regionMatches', () => {
it('matches an exact region code', () => {
expect(regionMatches('us-east-1', 'us-east-1')).toBe(true)
})
it('matches a smart group against a specific region it contains', () => {
expect(regionMatches('americas', 'us-east-1')).toBe(true)
})
it('does not match a smart group against a region from a different group', () => {
expect(regionMatches('apac', 'us-east-1')).toBe(false)
})
it.each([
['US-EAST-1', 'us-east-1'],
['us-east-1', 'US-EAST-1'],
['AMERICAS', 'us-east-1'],
])('matches case-insensitively (%s vs %s)', (selectedCode, affectedRegion) => {
expect(regionMatches(selectedCode, affectedRegion)).toBe(true)
})
it('does not match unrelated specific regions', () => {
expect(regionMatches('us-east-1', 'eu-central-1')).toBe(false)
})
})
describe('getItemsAffectingProjectCreation', () => {
type IncidentItem = Extract<StatusItem, { kind: 'incident' }>
type UpcomingMaintenanceItem = Extract<StatusItem, { kind: 'upcoming_maintenance' }>
function buildItem(overrides: Partial<IncidentItem> = {}): IncidentItem {
return {
id: 'incident-1',
name: 'Project creation degraded',
url: 'https://status.supabase.com/incidents/1',
lastUpdateAt: '2026-01-01T00:00:00Z',
lastUpdateMessage: null,
components: [],
scope: { type: 'global' },
projectCreationScope: null,
isProjectCreationOnly: true,
showBanner: true,
kind: 'incident',
status: 'investigating',
impact: 'partial_outage',
...overrides,
}
}
function buildUpcomingMaintenanceItem(
overrides: Partial<UpcomingMaintenanceItem> = {}
): UpcomingMaintenanceItem {
return {
id: 'maintenance-1',
name: 'Upcoming project creation maintenance',
url: 'https://status.supabase.com/incidents/2',
lastUpdateAt: '2026-01-01T00:00:00Z',
lastUpdateMessage: null,
components: [],
scope: { type: 'global' },
projectCreationScope: null,
isProjectCreationOnly: true,
showBanner: true,
kind: 'upcoming_maintenance',
startsAt: '2026-02-01T00:00:00Z',
endsAt: '2026-02-02T00:00:00Z',
leadDays: 7,
...overrides,
}
}
it('includes a global project creation item regardless of the selected region', () => {
const item = buildItem({ projectCreationScope: { type: 'global' } })
expect(getItemsAffectingProjectCreation([item], undefined)).toEqual([item])
expect(getItemsAffectingProjectCreation([item], 'us-east-1')).toEqual([item])
})
it('excludes a region-scoped item when no region is selected', () => {
const item = buildItem({ projectCreationScope: { type: 'regions', regions: ['us-east-1'] } })
expect(getItemsAffectingProjectCreation([item], undefined)).toEqual([])
})
it('excludes an item that does not affect project creation', () => {
const item = buildItem({ projectCreationScope: null })
expect(getItemsAffectingProjectCreation([item], 'us-east-1')).toEqual([])
})
it('excludes upcoming maintenance even with a matching project creation scope', () => {
const item = buildUpcomingMaintenanceItem({ projectCreationScope: { type: 'global' } })
expect(getItemsAffectingProjectCreation([item], undefined)).toEqual([])
})
it('still includes an item with showBanner false', () => {
const item = buildItem({ projectCreationScope: { type: 'global' }, showBanner: false })
expect(getItemsAffectingProjectCreation([item], undefined)).toEqual([item])
})
it('includes a region-scoped item matching the selected region via a smart group', () => {
const item = buildItem({ projectCreationScope: { type: 'regions', regions: ['us-east-1'] } })
expect(getItemsAffectingProjectCreation([item], 'americas')).toEqual([item])
})
})
@@ -1,6 +1,7 @@
import { z } from 'zod'
import { captureCriticalError } from '@/lib/error-reporting'
import { normalizeRegion, type StatusItem } from '@/lib/status-page/status-page.utils'
export const RESTRICTED_REGIONS_FLAG_KEY = 'projectCreationRestrictedRegions'
@@ -93,3 +94,35 @@ export function resolveRegionRestriction({
}: ResolveRegionRestrictionArgs): string | undefined {
return platformStatus ?? flagRestriction
}
// Maps smart region group codes to the specific-region code prefixes they contain.
// Used to check whether a status item affecting specific regions also affects a smart region selection.
export const SMART_REGION_PREFIXES: Record<string, Array<string>> = {
americas: ['us-', 'ca-', 'sa-'],
emea: ['eu-', 'me-', 'af-'],
apac: ['ap-'],
}
export function regionMatches(selectedCode: string, affectedRegion: string): boolean {
const normalizedSelected = normalizeRegion(selectedCode)
const normalizedAffected = normalizeRegion(affectedRegion)
if (normalizedSelected === normalizedAffected) return true
return (SMART_REGION_PREFIXES[normalizedSelected] ?? []).some((prefix) =>
normalizedAffected.startsWith(prefix)
)
}
export function getItemsAffectingProjectCreation(
items: Array<StatusItem>,
selectedRegionCode: string | undefined
): Array<StatusItem> {
return items.filter((item) => {
if (item.kind !== 'incident' && item.kind !== 'maintenance') return false
if (item.projectCreationScope === null) return false
if (item.projectCreationScope.type === 'global') return true
return (
selectedRegionCode !== undefined &&
item.projectCreationScope.regions.some((region) => regionMatches(selectedRegionCode, region))
)
})
}