chore(studio): Update support form status page button to use incident query + make maintenance banner dismissible (#42248)

* Update support form status page button to use incident query + make maintenance banner dismissible

* Clean up

* Nit

* Attempt to fix tests

* Fix tests
This commit is contained in:
Joshen Lim authored and GitHub committed 2026-01-28 23:18:39 +08:00
1 parent a127f2cbbc
commit ba5538576b
9 files changed
+90 -78

No files matched your search

@@ -1,20 +1,16 @@
import { partition } from 'lodash'
import { PropsWithChildren } from 'react'
import { MaintenanceBanner } from '@/components/layouts/AppLayout/MaintenanceBanner'
import { useIncidentStatusQuery } from '@/data/platform/incident-status-query'
import { useFlag } from 'common'
import { ClockSkewBanner } from 'components/layouts/AppLayout/ClockSkewBanner'
import { IncidentBanner } from 'components/layouts/AppLayout/IncidentBanner'
import { NoticeBanner } from 'components/layouts/AppLayout/NoticeBanner'
import { PropsWithChildren } from 'react'
import { OrganizationResourceBanner } from '../Organization/HeaderBanner'
import { MaintenanceBanner } from '@/components/layouts/AppLayout/MaintenanceBanner'
import { useIncidentStatusQuery } from '@/data/platform/incident-status-query'
export const AppBannerWrapper = ({ children }: PropsWithChildren<{}>) => {
const { data: allStatusPageEvents } = useIncidentStatusQuery()
const [maintenanceEvents, incidents] = partition(
allStatusPageEvents ?? [],
(event) => event.impact === 'maintenance'
)
const { maintenanceEvents = [], incidents = [] } = allStatusPageEvents ?? {}
const ongoingIncident =
useFlag('ongoingIncident') ||
@@ -1,9 +1,8 @@
import { AnimatePresence, motion } from 'framer-motion'
import Link from 'next/link'
import { useIncidentStatusQuery } from 'data/platform/incident-status-query'
import { processIncidentData } from 'data/platform/incident-status-utils'
import { AnimatePresence, motion } from 'framer-motion'
import { ExternalLink } from 'lucide-react'
import Link from 'next/link'
import { Button } from 'ui'
import { Admonition } from 'ui-patterns/admonition'
@@ -54,8 +53,8 @@ const getStatusDescription = (
}
export function IncidentAdmonition({ isActive }: IncidentAdmonitionProps) {
const { data: allStatusPageEvents = [], isLoading, isError } = useIncidentStatusQuery()
const incidents = allStatusPageEvents.filter((x) => x.impact !== 'maintenance')
const { data: allStatusPageEvents, isLoading, isError } = useIncidentStatusQuery()
const { incidents = [] } = allStatusPageEvents ?? {}
// Don't render anything while loading, on error, or if no incidents
if (isLoading || isError || !incidents || incidents.length === 0) {
@@ -1,20 +1,18 @@
import * as Sentry from '@sentry/nextjs'
import CopyButton from 'components/ui/CopyButton'
import { useIncidentStatusQuery } from 'data/platform/incident-status-query'
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
import { useStateTransition } from 'hooks/misc/useStateTransition'
import { BASE_PATH, DOCS_URL } from 'lib/constants'
import { Loader2, Wrench } from 'lucide-react'
import Link from 'next/link'
import { type Dispatch, type PropsWithChildren, useCallback, useReducer } from 'react'
import type { UseFormReturn } from 'react-hook-form'
import SVG from 'react-inlinesvg'
import { toast } from 'sonner'
// End of third-party imports
import CopyButton from 'components/ui/CopyButton'
import { useIncidentStatusQuery } from 'data/platform/incident-status-query'
import { usePlatformStatusQuery } from 'data/platform/platform-status-query'
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
import { useStateTransition } from 'hooks/misc/useStateTransition'
import { BASE_PATH, DOCS_URL } from 'lib/constants'
import { Button, cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
import { Button, Tooltip, TooltipContent, TooltipTrigger, cn } from 'ui'
import { Admonition } from 'ui-patterns/admonition'
import { AIAssistantOption } from './AIAssistantOption'
import { DiscordCTACard } from './DiscordCTACard'
import { IncidentAdmonition } from './IncidentAdmonition'
@@ -22,10 +20,10 @@ import { Success } from './Success'
import type { ExtendedSupportCategories } from './Support.constants'
import type { SupportFormValues } from './SupportForm.schema'
import {
createInitialSupportFormState,
type SupportFormActions,
supportFormReducer,
type SupportFormState,
createInitialSupportFormState,
supportFormReducer,
} from './SupportForm.state'
import { NO_PROJECT_MARKER } from './SupportForm.utils'
import { SupportFormV2 } from './SupportFormV2'
@@ -67,10 +65,11 @@ function SupportFormPageContent() {
const { form, initialError, projectRef, orgSlug } = useSupportForm(dispatch)
const {
data: incidents,
data: allStatusPageEvents,
isPending: isIncidentsPending,
isError: isIncidentsError,
} = useIncidentStatusQuery()
const { incidents = [] } = allStatusPageEvents ?? {}
const hasActiveIncidents =
!isIncidentsPending && !isIncidentsError && incidents && incidents.length > 0
@@ -129,8 +128,10 @@ function SupportFormWrapper({ children }: PropsWithChildren) {
}
function SupportFormHeader() {
const { data, isPending: isLoading, isError } = usePlatformStatusQuery()
const isHealthy = data?.isHealthy
const { data: allStatusPageEvents, isPending: isLoading, isError } = useIncidentStatusQuery()
const { incidents = [], maintenanceEvents = [] } = allStatusPageEvents ?? {}
const isMaintenance = maintenanceEvents.length > 0
const isIncident = incidents.length > 0
return (
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-y-2">
@@ -157,10 +158,10 @@ function SupportFormHeader() {
icon={
isLoading ? (
<Loader2 className="animate-spin" />
) : isHealthy ? (
<div className="h-2 w-2 bg-brand rounded-full" />
) : (
<div className="h-2 w-2 bg-yellow-900 rounded-full" />
<div
className={cn('h-2 w-2 rounded-full', isIncident ? 'bg-warning' : 'bg-brand')}
/>
)
}
>
@@ -169,9 +170,11 @@ function SupportFormHeader() {
? 'Checking status'
: isError
? 'Failed to check status'
: isHealthy
? 'All systems operational'
: 'Active incident ongoing'}
: isIncident
? 'Active incident ongoing'
: isMaintenance
? 'Scheduled maintenance'
: 'All systems operational'}
</Link>
</Button>
</TooltipTrigger>
@@ -1,15 +1,16 @@
import { screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import dayjs from 'dayjs'
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
// End of third-party imports
import { API_URL } from 'lib/constants'
import { API_URL, BASE_PATH } from 'lib/constants'
import { HttpResponse, http } from 'msw'
import { createMockOrganization, createMockProject } from 'tests/helpers'
import { customRender } from 'tests/lib/custom-render'
import { addAPIMock, mswServer } from 'tests/lib/msw'
import { createMockProfileContext } from 'tests/lib/profile-helpers'
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
import { NO_ORG_MARKER, NO_PROJECT_MARKER } from '../SupportForm.utils'
import { SupportFormPage } from '../SupportFormPage'
@@ -173,6 +174,14 @@ vi.mock(import('lib/gotrue'), async (importOriginal) => {
}
})
vi.mock(import('lib/constants'), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
IS_PLATFORM: true,
}
})
const renderSupportFormPage = (options?: Parameters<typeof customRender>[1]) =>
customRender(<SupportFormPage />, {
profileContext: createMockProfileContext(),
@@ -397,11 +406,9 @@ describe('SupportFormPage', () => {
},
})
addAPIMock({
method: 'get',
path: '/platform/status',
response: { is_healthy: true } as any,
})
mswServer.use(
http.get(`${BASE_PATH}/api/incident-status`, () => HttpResponse.json([], { status: 200 }))
)
addAPIMock({
method: 'get',
@@ -459,11 +466,22 @@ describe('SupportFormPage', () => {
})
test('shows system status: not healthy', async () => {
addAPIMock({
method: 'get',
path: '/platform/status',
response: { is_healthy: false } as any,
})
mswServer.use(
http.get(`${BASE_PATH}/api/incident-status`, () =>
HttpResponse.json(
[
{
id: 'z3qp8rln72pl',
active_since: '2026-01-26T10:30:00Z',
impact: 'critical',
status: 'in_progress',
name: 'Test incident',
},
],
{ status: 200 }
)
)
)
renderSupportFormPage()
@@ -474,7 +492,7 @@ describe('SupportFormPage', () => {
test('shows system status: check failed', async () => {
mswServer.use(
http.get(`${API_URL}/platform/status`, () =>
http.get(`${BASE_PATH}/api/incident-status`, () =>
HttpResponse.json({ msg: 'Status service unavailable' }, { status: 500 })
)
)
@@ -1,10 +1,25 @@
import { LOCAL_STORAGE_KEYS } from 'common'
import { HeaderBanner } from 'components/interfaces/Organization/HeaderBanner'
import { InlineLink } from 'components/ui/InlineLink'
import { useIncidentStatusQuery } from '@/data/platform/incident-status-query'
import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
/**
* Used to display ongoing maintenance
*/
export function MaintenanceBanner() {
const { data: allStatusPageEvents } = useIncidentStatusQuery()
const { maintenanceEvents = [] } = allStatusPageEvents ?? {}
const currentEventId = maintenanceEvents[0]?.id ?? ''
const [dismissed, setDismissed] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.MAINTENANCE_BANNER_DISMISSED(currentEventId),
false
)
if (dismissed) return null
return (
<HeaderBanner
variant="note"
@@ -15,6 +30,7 @@ export function MaintenanceBanner() {
updates
</>
}
onDismiss={() => setDismissed(true)}
/>
)
}
@@ -1,11 +1,14 @@
import { useQuery } from '@tanstack/react-query'
import type { IncidentInfo } from 'lib/api/incident-status'
import { BASE_PATH, IS_PLATFORM } from 'lib/constants'
import { partition } from 'lodash'
import { UseCustomQueryOptions } from 'types'
import { platformKeys } from './keys'
export async function getIncidentStatus(signal?: AbortSignal): Promise<IncidentInfo[]> {
export async function getIncidentStatus(
signal?: AbortSignal
): Promise<{ maintenanceEvents: IncidentInfo[]; incidents: IncidentInfo[] }> {
const response = await fetch(`${BASE_PATH}/api/incident-status`, {
signal,
method: 'GET',
@@ -21,7 +24,11 @@ export async function getIncidentStatus(signal?: AbortSignal): Promise<IncidentI
}
const data = await response.json()
return data as IncidentInfo[]
const [maintenanceEvents, incidents] = partition(
data ?? [],
(event) => event.impact === 'maintenance'
)
return { maintenanceEvents, incidents }
}
export type IncidentStatusData = Awaited<ReturnType<typeof getIncidentStatus>>
-1
View File
@@ -1,4 +1,3 @@
export const platformKeys = {
status: () => ['platform', 'status'] as const,
incidentStatus: () => ['platform', 'incident-status'] as const,
}
@@ -1,27 +0,0 @@
import { useQuery } from '@tanstack/react-query'
import { get, handleError } from 'data/fetchers'
import { platformKeys } from './keys'
import { UseCustomQueryOptions } from 'types'
export type PlatformStatusResponse = {
isHealthy: boolean
}
export async function getPlatformStatus(signal?: AbortSignal) {
const { data, error } = await get('/platform/status', { signal })
if (error) handleError(error)
return { isHealthy: (data as any).is_healthy } as PlatformStatusResponse
}
export type PlatformStatusData = Awaited<ReturnType<typeof getPlatformStatus>>
export type PlatformStatusError = unknown
export const usePlatformStatusQuery = <TData = PlatformStatusData>(
options: UseCustomQueryOptions<PlatformStatusData, PlatformStatusError, TData> = {}
) =>
useQuery<PlatformStatusData, PlatformStatusError, TData>({
queryKey: platformKeys.status(),
queryFn: ({ signal }) => getPlatformStatus(signal),
...options,
})
@@ -9,6 +9,7 @@ export const LOCAL_STORAGE_KEYS = {
PROJECTS_VIEW: 'projects-view',
FEEDBACK_WIDGET_CONTENT: 'feedback-widget-content',
FEEDBACK_WIDGET_SCREENSHOT: 'feedback-widget-screenshot',
MAINTENANCE_BANNER_DISMISSED: (id: string) => `maintenance-banner-dismissed-${id}`,
UI_PREVIEW_API_SIDE_PANEL: 'supabase-ui-api-side-panel',
UI_PREVIEW_CLS: 'supabase-ui-cls',