mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
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:
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,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',
|
||||
|
||||
Reference in new issue
Block a user