diff --git a/apps/studio/components/interfaces/App/AppBannerWrapper.tsx b/apps/studio/components/interfaces/App/AppBannerWrapper.tsx
index 70bffc48d3e..1041597198f 100644
--- a/apps/studio/components/interfaces/App/AppBannerWrapper.tsx
+++ b/apps/studio/components/interfaces/App/AppBannerWrapper.tsx
@@ -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') ||
diff --git a/apps/studio/components/interfaces/Support/IncidentAdmonition.tsx b/apps/studio/components/interfaces/Support/IncidentAdmonition.tsx
index e7caa76a708..4561e4c30b7 100644
--- a/apps/studio/components/interfaces/Support/IncidentAdmonition.tsx
+++ b/apps/studio/components/interfaces/Support/IncidentAdmonition.tsx
@@ -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) {
diff --git a/apps/studio/components/interfaces/Support/SupportFormPage.tsx b/apps/studio/components/interfaces/Support/SupportFormPage.tsx
index 672103a7a81..60268f00035 100644
--- a/apps/studio/components/interfaces/Support/SupportFormPage.tsx
+++ b/apps/studio/components/interfaces/Support/SupportFormPage.tsx
@@ -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 (
@@ -157,10 +158,10 @@ function SupportFormHeader() {
icon={
isLoading ? (
- ) : isHealthy ? (
-
) : (
-
+
)
}
>
@@ -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'}
diff --git a/apps/studio/components/interfaces/Support/__tests__/SupportFormPage.test.tsx b/apps/studio/components/interfaces/Support/__tests__/SupportFormPage.test.tsx
index c64257e678d..4c85b45f3ca 100644
--- a/apps/studio/components/interfaces/Support/__tests__/SupportFormPage.test.tsx
+++ b/apps/studio/components/interfaces/Support/__tests__/SupportFormPage.test.tsx
@@ -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
[1]) =>
customRender(, {
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 })
)
)
diff --git a/apps/studio/components/layouts/AppLayout/MaintenanceBanner.tsx b/apps/studio/components/layouts/AppLayout/MaintenanceBanner.tsx
index 388f2ee16a5..4c5610b36b6 100644
--- a/apps/studio/components/layouts/AppLayout/MaintenanceBanner.tsx
+++ b/apps/studio/components/layouts/AppLayout/MaintenanceBanner.tsx
@@ -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 (
}
+ onDismiss={() => setDismissed(true)}
/>
)
}
diff --git a/apps/studio/data/platform/incident-status-query.ts b/apps/studio/data/platform/incident-status-query.ts
index 6efb96c1a09..a6db924e096 100644
--- a/apps/studio/data/platform/incident-status-query.ts
+++ b/apps/studio/data/platform/incident-status-query.ts
@@ -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 {
+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 event.impact === 'maintenance'
+ )
+ return { maintenanceEvents, incidents }
}
export type IncidentStatusData = Awaited>
diff --git a/apps/studio/data/platform/keys.ts b/apps/studio/data/platform/keys.ts
index 4b7877df0f0..3052d7b2fdf 100644
--- a/apps/studio/data/platform/keys.ts
+++ b/apps/studio/data/platform/keys.ts
@@ -1,4 +1,3 @@
export const platformKeys = {
- status: () => ['platform', 'status'] as const,
incidentStatus: () => ['platform', 'incident-status'] as const,
}
diff --git a/apps/studio/data/platform/platform-status-query.ts b/apps/studio/data/platform/platform-status-query.ts
deleted file mode 100644
index 73f944673bf..00000000000
--- a/apps/studio/data/platform/platform-status-query.ts
+++ /dev/null
@@ -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>
-export type PlatformStatusError = unknown
-
-export const usePlatformStatusQuery = (
- options: UseCustomQueryOptions = {}
-) =>
- useQuery({
- queryKey: platformKeys.status(),
- queryFn: ({ signal }) => getPlatformStatus(signal),
- ...options,
- })
diff --git a/packages/common/constants/local-storage.ts b/packages/common/constants/local-storage.ts
index 4553bcf8b50..86475d95ba7 100644
--- a/packages/common/constants/local-storage.ts
+++ b/packages/common/constants/local-storage.ts
@@ -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',