mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
## Summary * Adds the client query layer for `/api/status-page` (`statusPageQueryOptions` in `data/platform/status-page-query.ts`) and a shared normalization module (`lib/status-page/status-page.utils.ts`) that maps the endpoint response into region- and project-creation-aware `StatusItem`s — later PRs in this stack (assistant tool, project-creation admonition, global banner) build on this same module. * Wires the support form's status pill and incident admonition to the new data behind the `incidentIoStatusPage` ConfigCat flag. `useSupportStatus` branches between the new endpoint and the existing `useIncidentStatusQuery`/`processIncidentData` path, both mapped to the same `SupportStatus` shape. * `IncidentAdmonition` becomes purely presentational; the status link now points at the `pageUrl` returned by the endpoint instead of a hardcoded URL. Part of [FE-4057](https://linear.app/supabase/issue/FE-4057/frontend-bannerbot-reconfigured) — see Linear for full design context. ## 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` (touched files) — 70 tests passing, including a flag-on case for `SupportFormPage` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Support pages now display current incident and maintenance information, including relevant status descriptions and links to the status page. * Status details can reflect items affecting a user’s region or project-creation services. Upcoming maintenance is excluded from active alerts. * **Bug Fixes** * Status labels and alerts now account for loading, errors, incidents, and maintenance consistently across support pages. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
130 lines
4.9 KiB
TypeScript
130 lines
4.9 KiB
TypeScript
import { IMPACT_RANK, type StatusItem } from '@/lib/status-page/status-page.utils'
|
|
|
|
export const DEFAULT_STATUS_PAGE_URL = 'https://status.supabase.com/'
|
|
|
|
const STATUS_DESCRIPTION_SIGN_OFF = 'Follow the status page for updates.'
|
|
|
|
function capitalizeFirstLetter(value: string): string {
|
|
return value.charAt(0).toUpperCase() + value.slice(1)
|
|
}
|
|
|
|
/**
|
|
* Describes the current state of one or more active issues. `status` covers incident statuses
|
|
* plus the synthetic `maintenance_in_progress` status used for in-progress maintenance.
|
|
*/
|
|
export function getStatusDescription({
|
|
status,
|
|
hasMultipleIncidents,
|
|
allSameStatus,
|
|
}: {
|
|
status: string
|
|
hasMultipleIncidents: boolean
|
|
allSameStatus: boolean
|
|
}): string {
|
|
const issueTerm = hasMultipleIncidents ? 'these issues' : 'this issue'
|
|
|
|
switch (status) {
|
|
case 'investigating':
|
|
if (hasMultipleIncidents && !allSameStatus) {
|
|
return `We are aware of multiple ongoing issues and are investigating. ${STATUS_DESCRIPTION_SIGN_OFF}`
|
|
}
|
|
return `We are investigating ${issueTerm}. ${STATUS_DESCRIPTION_SIGN_OFF}`
|
|
|
|
case 'identified':
|
|
if (hasMultipleIncidents && !allSameStatus) {
|
|
return `We have identified the cause of some of ${issueTerm} and are working on fixes. ${STATUS_DESCRIPTION_SIGN_OFF}`
|
|
}
|
|
return `We have identified the cause of ${issueTerm} and are working on a fix. ${STATUS_DESCRIPTION_SIGN_OFF}`
|
|
|
|
case 'monitoring':
|
|
if (hasMultipleIncidents && !allSameStatus) {
|
|
return `Fixes have been deployed for some of ${issueTerm} and we are monitoring the results. ${STATUS_DESCRIPTION_SIGN_OFF}`
|
|
}
|
|
return `A fix has been deployed and we are monitoring the results. ${STATUS_DESCRIPTION_SIGN_OFF}`
|
|
|
|
case 'resolved':
|
|
if (hasMultipleIncidents && !allSameStatus) {
|
|
return `Some of ${issueTerm} have been resolved, but others may still be ongoing. ${STATUS_DESCRIPTION_SIGN_OFF}`
|
|
}
|
|
return `${capitalizeFirstLetter(issueTerm)} ${hasMultipleIncidents ? 'have' : 'has'} been resolved but may take some time to fully recover. ${STATUS_DESCRIPTION_SIGN_OFF}`
|
|
|
|
case 'maintenance_in_progress':
|
|
if (hasMultipleIncidents && !allSameStatus) {
|
|
return `Scheduled maintenance is in progress alongside other issues. ${STATUS_DESCRIPTION_SIGN_OFF}`
|
|
}
|
|
return `Scheduled maintenance is in progress. ${STATUS_DESCRIPTION_SIGN_OFF}`
|
|
|
|
default:
|
|
return `We are investigating ${issueTerm}. ${STATUS_DESCRIPTION_SIGN_OFF}`
|
|
}
|
|
}
|
|
|
|
export type ActiveStatusItem = Extract<StatusItem, { kind: 'incident' | 'maintenance' }>
|
|
|
|
function getItemStatusLabel(item: ActiveStatusItem): string {
|
|
return item.kind === 'incident' ? item.status : 'maintenance_in_progress'
|
|
}
|
|
|
|
/**
|
|
* Incidents and in-progress maintenance only (never upcoming maintenance), ranked by impact
|
|
* (maintenance ranks below every incident impact), then by recency. Ignores `showBanner`:
|
|
* "Hide banner" only hides the global banner, not the support form's status pill/admonition.
|
|
*/
|
|
export function prioritizeActiveItems(items: Array<StatusItem>): Array<ActiveStatusItem> {
|
|
const active = items.filter(
|
|
(item): item is ActiveStatusItem => item.kind === 'incident' || item.kind === 'maintenance'
|
|
)
|
|
|
|
return active.sort((a, b) => {
|
|
const rankA = a.kind === 'incident' ? IMPACT_RANK[a.impact] : 0
|
|
const rankB = b.kind === 'incident' ? IMPACT_RANK[b.impact] : 0
|
|
if (rankB !== rankA) return rankB - rankA
|
|
return new Date(b.lastUpdateAt).getTime() - new Date(a.lastUpdateAt).getTime()
|
|
})
|
|
}
|
|
|
|
export function getSupportAdmonitionCopy(
|
|
items: Array<StatusItem>
|
|
): { title: string; description: string } | null {
|
|
const active = prioritizeActiveItems(items)
|
|
if (active.length === 0) return null
|
|
|
|
const [top, ...rest] = active
|
|
const hasMultipleIncidents = rest.length > 0
|
|
const title = hasMultipleIncidents
|
|
? `${top.name} and ${rest.length} other issue${rest.length > 1 ? 's' : ''}`
|
|
: top.name
|
|
|
|
const topStatus = getItemStatusLabel(top)
|
|
const allSameStatus = active.every((item) => getItemStatusLabel(item) === topStatus)
|
|
|
|
return {
|
|
title,
|
|
description: getStatusDescription({ status: topStatus, hasMultipleIncidents, allSameStatus }),
|
|
}
|
|
}
|
|
|
|
export function getStatusPillLabel(items: Array<StatusItem>): string {
|
|
const active = prioritizeActiveItems(items)
|
|
if (active.length === 0) return 'All systems operational'
|
|
if (active.some((item) => item.kind === 'incident')) return 'Active incident ongoing'
|
|
return 'Scheduled maintenance'
|
|
}
|
|
|
|
export type SupportStatus =
|
|
| { status: 'pending' }
|
|
| { status: 'error' }
|
|
| {
|
|
status: 'success'
|
|
hasActiveIncidents: boolean
|
|
pillLabel: string
|
|
admonition: { title: string; description: string } | null
|
|
pageUrl: string
|
|
}
|
|
|
|
export function getSupportStatusLabel(status: SupportStatus): string {
|
|
if (status.status === 'pending') return 'Checking status'
|
|
if (status.status === 'error') return 'Failed to check status'
|
|
return status.pillLabel
|
|
}
|