Files
Charis 53b20cad5e 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 -->
2026-09-29 17:37:01 -04:00

129 lines
4.2 KiB
TypeScript

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'
export type PlatformRegionStatus = 'capacity' | 'other'
const FLAG_RESTRICTIONS = ['unavailable'] as const
export type FlagRestriction = (typeof FLAG_RESTRICTIONS)[number]
export type RegionRestriction = PlatformRegionStatus | FlagRestriction
export type RegionRestrictionCopy = {
badge: string
title: string
tooltip: string
notice: string
}
const GENERIC_RESTRICTION_COPY: RegionRestrictionCopy = {
badge: 'Unavailable',
title: 'Selected region is unavailable',
tooltip: 'Temporarily unavailable for new projects.',
notice: 'This region is temporarily unavailable for new projects.',
}
export const REGION_RESTRICTION_COPY: Record<RegionRestriction, RegionRestrictionCopy> = {
capacity: {
badge: 'Unavailable',
title: 'Selected region is at capacity',
tooltip: 'Temporarily unavailable due to this region being at capacity.',
notice:
'This region currently has capacity for Micro compute and above. Free plan projects run on Nano compute.',
},
other: GENERIC_RESTRICTION_COPY,
unavailable: GENERIC_RESTRICTION_COPY,
}
export const SELECT_DIFFERENT_REGION = 'Select a different region to continue.'
function isKnownRestriction(value: string): value is RegionRestriction {
return Object.prototype.hasOwnProperty.call(REGION_RESTRICTION_COPY, value)
}
export function getRegionRestrictionCopy(restriction: string): RegionRestrictionCopy {
return isKnownRestriction(restriction)
? REGION_RESTRICTION_COPY[restriction]
: GENERIC_RESTRICTION_COPY
}
export function getRegionRestrictionMessage(restriction: string) {
return `${getRegionRestrictionCopy(restriction).title}. ${SELECT_DIFFERENT_REGION}`
}
const RestrictedRegionsSchema = z.record(z.string(), z.enum(FLAG_RESTRICTIONS))
export function parseRestrictedRegions(flagValue: unknown): Record<string, FlagRestriction> {
if (typeof flagValue !== 'string' || flagValue.trim() === '') return {}
let payload: unknown
try {
payload = JSON.parse(flagValue)
} catch {
reportInvalidPayload(flagValue, 'invalid JSON')
return {}
}
const result = RestrictedRegionsSchema.safeParse(payload)
if (!result.success) {
reportInvalidPayload(flagValue, result.error.issues[0]?.message ?? 'invalid shape')
return {}
}
return result.data
}
function reportInvalidPayload(flagValue: string, reason: string) {
captureCriticalError(
new Error(`Ignoring ${RESTRICTED_REGIONS_FLAG_KEY} flag (${reason}): ${flagValue}`),
'parse restricted regions flag'
)
}
type ResolveRegionRestrictionArgs = {
platformStatus: string | undefined
flagRestriction: FlagRestriction | undefined
}
export function resolveRegionRestriction({
platformStatus,
flagRestriction,
}: 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))
)
})
}