mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 01:15:03 +03:00
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:
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))
|
||||
)
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user