From 53b20cad5ea89ed016f685f90f92477d30398daa Mon Sep 17 00:00:00 2001 From: Charis <26616127+charislam@users.noreply.github.com> Date: Tue, 29 Sep 2026 17:37:01 -0400 Subject: [PATCH] feat(studio): project creation status admonition (#51029) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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) ## 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. --- .../ProjectCreationStatusAdmonition.tsx | 41 +++++++ .../ProjectCreation/RegionSelector.tsx | 72 ++++++------ .../RegionSelector.utils.test.ts | 106 ++++++++++++++++++ .../ProjectCreation/RegionSelector.utils.ts | 33 ++++++ 4 files changed, 213 insertions(+), 39 deletions(-) create mode 100644 apps/studio/components/interfaces/ProjectCreation/ProjectCreationStatusAdmonition.tsx diff --git a/apps/studio/components/interfaces/ProjectCreation/ProjectCreationStatusAdmonition.tsx b/apps/studio/components/interfaces/ProjectCreation/ProjectCreationStatusAdmonition.tsx new file mode 100644 index 00000000000..e2ad9aff1f6 --- /dev/null +++ b/apps/studio/components/interfaces/ProjectCreation/ProjectCreationStatusAdmonition.tsx @@ -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 ( + + + {isMaintenance ? 'Maintenance' : 'An incident'} may affect project creation + {isRegionSpecific ? ' in this region' : ''}. Follow updates on{' '} + status page. + + } + className="mt-3" + /> + + ) +} diff --git a/apps/studio/components/interfaces/ProjectCreation/RegionSelector.tsx b/apps/studio/components/interfaces/ProjectCreation/RegionSelector.tsx index 68844b3be40..587b31e247b 100644 --- a/apps/studio/components/interfaces/ProjectCreation/RegionSelector.tsx +++ b/apps/studio/components/interfaces/ProjectCreation/RegionSelector.tsx @@ -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> = { - 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 = ({ - {affectingIncidents.length > 0 && ( - - - We're currently investigating an issue that may impact projects in this - region. Follow updates on{' '} - - status.supabase.com - - . - - } - className="mt-3" - /> - + {isStatusPageEnabled ? ( + + ) : ( + affectingIncidents.length > 0 && ( + + + We're currently investigating an issue that may impact projects in this + region. Follow updates on{' '} + + status.supabase.com + + . + + } + className="mt-3" + /> + + ) )} {selectedRestrictionCopy !== undefined && ( diff --git a/apps/studio/components/interfaces/ProjectCreation/RegionSelector.utils.test.ts b/apps/studio/components/interfaces/ProjectCreation/RegionSelector.utils.test.ts index 9f2a1f3d6b6..d3c71228949 100644 --- a/apps/studio/components/interfaces/ProjectCreation/RegionSelector.utils.test.ts +++ b/apps/studio/components/interfaces/ProjectCreation/RegionSelector.utils.test.ts @@ -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 + type UpcomingMaintenanceItem = Extract + + function buildItem(overrides: Partial = {}): 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 { + 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]) + }) +}) diff --git a/apps/studio/components/interfaces/ProjectCreation/RegionSelector.utils.ts b/apps/studio/components/interfaces/ProjectCreation/RegionSelector.utils.ts index 877e9887d20..d6984eb8ad5 100644 --- a/apps/studio/components/interfaces/ProjectCreation/RegionSelector.utils.ts +++ b/apps/studio/components/interfaces/ProjectCreation/RegionSelector.utils.ts @@ -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> = { + 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, + selectedRegionCode: string | undefined +): Array { + 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)) + ) + }) +}