Files
supabase/apps/studio/components/interfaces/ErrorHandling/RestartProjectDialog.tsx
T
Jordi Enric ec26943390 feat: improve db overload debugging UX (#43564)
When the dashboard hits a DB connection timeout, users currently see a
raw error message with no
path forward. This PR adds an inline troubleshooting system that detects
known error types and
surfaces contextual next steps — restart the DB, read the docs, or debug
with AI.

##  Changes

- New ErrorDisplay component (packages/ui-patterns) — styled error card
with a title, monospace error
block, optional troubleshooting slot, and a "Contact support" link that
always renders. Accepts
  typed supportFormParams to pre-fill the support form.

- Error classification in handleError (data/fetchers.ts) — on every API
error, the message is tested
against ERROR_PATTERNS. If matched, handleError throws a typed subclass
(ConnectionTimeoutError
extends ResponseError) instead of a plain ResponseError. Stack traces
now show the exact error
  class. All existing instanceof ResponseError checks continue to work.

- ErrorMatcher component — reads errorType from the thrown class
instance, does an O(1) lookup into
ERROR_MAPPINGS, and renders the matching troubleshooting accordion as
children of ErrorDisplay.
  Falls back to plain ErrorDisplay for unclassified errors.

- Connection timeout mapping — first error type wired up, with three
troubleshooting steps: restart
the database, link to the docs, and "Debug with AI" (opens the AI
assistant sidebar with a
  pre-filled prompt).

- Telemetry — three new typed events track when the troubleshooter is
shown, when accordion steps are
   toggled, and which CTAs are clicked.

##  Adding a new error type

  1. Add a class to types/api-errors.ts
  2. Add { pattern, ErrorClass } to data/error-patterns.ts
  3. Create a troubleshooting component in errorMappings/
  4. Add an entry to error-mappings.tsx
2026-03-16 11:22:30 +01:00

105 lines
3.3 KiB
TypeScript

'use client'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useSetProjectStatus } from 'data/projects/project-detail-query'
import { useProjectRestartMutation } from 'data/projects/project-restart-mutation'
import { useProjectRestartServicesMutation } from 'data/projects/project-restart-services-mutation'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { PROJECT_STATUS } from 'lib/constants'
import { useRouter } from 'next/router'
import { toast } from 'sonner'
import { ConfirmationModal } from 'ui-patterns/Dialogs/ConfirmationModal'
interface RestartProjectDialogProps {
visible: boolean
onClose: () => void
/** Restart type: 'project' for full restart, 'database' for fast database reboot */
restartType?: 'project' | 'database'
}
export function RestartProjectDialog({
visible,
onClose,
restartType = 'database',
}: RestartProjectDialogProps) {
const router = useRouter()
const { data: project } = useSelectedProjectQuery()
const { setProjectStatus } = useSetProjectStatus()
const { can: canRestartProject } = useAsyncCheckPermissions(
PermissionAction.INFRA_EXECUTE,
'reboot'
)
const { mutate: restartProject, isPending: isRestartingProject } = useProjectRestartMutation({
onSuccess: () => {
if (project?.ref) {
setProjectStatus({ ref: project.ref, status: PROJECT_STATUS.RESTARTING })
}
toast.success('Restarting project')
router.push(`/project/${project?.ref}`)
onClose()
},
onError: (error) => {
toast.error(`Unable to restart project: ${error.message}`)
},
})
const { mutate: restartProjectServices, isPending: isRestartingServices } =
useProjectRestartServicesMutation({
onSuccess: () => {
if (project?.ref) {
setProjectStatus({ ref: project.ref, status: PROJECT_STATUS.RESTARTING })
}
toast.success('Restarting database')
router.push(`/project/${project?.ref}`)
onClose()
},
onError: (error) => {
toast.error(`Unable to restart database: ${error.message}`)
},
})
const isLoading = isRestartingProject || isRestartingServices
const handleRestart = () => {
if (!project?.ref) return
if (!canRestartProject) {
return toast.error('You do not have the required permissions to restart this project')
}
if (restartType === 'project') {
restartProject({ ref: project.ref })
} else {
restartProjectServices({
ref: project.ref,
region: project.region,
services: ['postgresql'],
})
}
}
const title = restartType === 'project' ? 'Restart project' : 'Restart database'
const description =
restartType === 'project'
? 'Are you sure you want to restart your project? There will be a few minutes of downtime.'
: 'Are you sure you want to restart your database? There will be a brief downtime.'
return (
<ConfirmationModal
visible={visible}
variant="warning"
title={title}
description={description}
confirmLabel="Restart"
confirmLabelLoading="Restarting"
loading={isLoading}
disabled={!canRestartProject}
onCancel={onClose}
onConfirm={handleRestart}
/>
)
}