fix(studio): restart action for table editor load errors FE-4054 (#48687)

## Problem

When the table editor showed a "Failed to load tables" or "Failed to
load schemas" error (for example, when the underlying database or API
gateway is unhealthy), there was no working way to restart the project
from that error state. Restarting only worked by navigating to Project
Settings.

## Fix

"Failed to load tables" goes through the existing `ErrorMatcher`
classification system, which only showed troubleshooting steps
(including a restart action) for connection-timeout errors. Added an
`ERROR_MAPPINGS` entry for the unclassified/generic API error case,
reusing the existing `RestartDatabaseTroubleshootingSection` and
`RestartProjectDialog` components already used for connection timeouts.

"Failed to load schemas" (in the shared `SchemaSelector`, used across
the table editor and several Database pages) only offered a retry. Added
a "Restart database" button next to it, wired to the same
`RestartProjectDialog`.

## How to test

- In the table editor, trigger a table-load failure that isn't a
connection timeout (any generic API error). The error card should now
show a "Try restarting your project" step with a working restart action.
- Open the schema selector while schemas fail to load (e.g. mock a 503
from the schemas query). A "Restart database" button should appear next
to "Reload schemas" and open the restart confirmation dialog.
-
`apps/studio/components/interfaces/ErrorHandling/ErrorMatcher.test.tsx`
and `apps/studio/components/ui/SchemaSelector.test.tsx` cover both
cases.

FE-4054

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
  * Added database restart guidance when schema loading fails.
* Added options to reload schemas or restart the database, including a
confirmation prompt.
* Added troubleshooting guidance for unclassified table-loading errors.

* **Bug Fixes**
* Improved error handling by displaying relevant fallback guidance for
unknown errors while preserving classified troubleshooting instructions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Jordi Enric authored and GitHub committed 2026-08-04 14:36:06 +02:00
1 parent 2ba2c37163
commit a66dae48f2
6 files changed
+106 -6

No files matched your search

@@ -66,6 +66,32 @@ describe('ErrorMatcher', () => {
expect(screen.getByText('UNKNOWN ERROR')).toBeInTheDocument()
})
it('renders the caller-provided fallback when the error is unclassified', () => {
render(
<ErrorMatcher
title="Failed to load tables"
error="UNKNOWN ERROR"
supportFormParams={{}}
fallback={<div>Custom fallback</div>}
/>
)
expect(screen.getByText('Custom fallback')).toBeInTheDocument()
})
it('ignores the caller-provided fallback when the error is classified', () => {
const error = new ConnectionTimeoutError('CONNECTION TERMINATED DUE TO CONNECTION TIMEOUT')
render(
<ErrorMatcher
title="Failed to load tables"
error={error}
supportFormParams={{}}
fallback={<div>Custom fallback</div>}
/>
)
expect(screen.queryByText('Custom fallback')).not.toBeInTheDocument()
expect(screen.getByText('Try restarting your project')).toBeInTheDocument()
})
it('accepts error as object with message property', () => {
render(
<ErrorMatcher
@@ -1,5 +1,6 @@
'use client'
import { ReactNode } from 'react'
import { ErrorDisplay, SupportFormParams } from 'ui-patterns/ErrorDisplay/ErrorDisplay'
import { getMappingForError } from './ErrorMatcher.utils'
@@ -11,9 +12,17 @@ interface ErrorMatcherProps {
error: string | { message: string }
supportFormParams?: SupportFormParams
className?: string
/** Shown when the error isn't classified with its own troubleshooting steps. */
fallback?: ReactNode
}
export function ErrorMatcher({ title, error, supportFormParams, className }: ErrorMatcherProps) {
export function ErrorMatcher({
title,
error,
supportFormParams,
className,
fallback,
}: ErrorMatcherProps) {
const track = useTrack()
const message = typeof error === 'string' ? error : error.message
@@ -48,7 +57,7 @@ export function ErrorMatcher({ title, error, supportFormParams, className }: Err
: undefined
}
>
{Troubleshooting && <Troubleshooting />}
{Troubleshooting ? <Troubleshooting /> : fallback}
</ErrorDisplay>
)
}
@@ -0,0 +1,15 @@
import { TroubleshootingAccordion } from './TroubleshootingAccordion'
import { RestartDatabaseTroubleshootingSection } from './TroubleshootingSections'
const ERROR_TYPE = 'unknown'
export function RestartTroubleshootingFallback() {
return (
<TroubleshootingAccordion
errorType={ERROR_TYPE}
stepTitles={{ 1: 'Try restarting your project' }}
>
<RestartDatabaseTroubleshootingSection number={1} errorType={ERROR_TYPE} />
</TroubleshootingAccordion>
)
}
@@ -21,6 +21,7 @@ import { parseSupaTable } from '@/components/grid/SupabaseGrid.utils'
import { SupaTable } from '@/components/grid/types'
import { ProtectedSchemaWarning } from '@/components/interfaces/Database/ProtectedSchemaWarning'
import { ErrorMatcher } from '@/components/interfaces/ErrorHandling/ErrorMatcher'
import { RestartTroubleshootingFallback } from '@/components/interfaces/ErrorHandling/RestartTroubleshootingFallback'
import { EditorMenuListSkeleton } from '@/components/layouts/TableEditorLayout/EditorMenuListSkeleton'
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
import { InfiniteListDefault, LoaderForIconMenuItems } from '@/components/ui/InfiniteList'
@@ -305,6 +306,7 @@ export const TableEditorMenu = () => {
error={error ?? 'Failed to load tables'}
supportFormParams={{ projectRef: project?.ref }}
className="mx-4 mt-3"
fallback={<RestartTroubleshootingFallback />}
/>
)}
@@ -6,7 +6,7 @@ import { describe, expect, it, vi } from 'vitest'
import { SchemaSelector } from './SchemaSelector'
import { customRender } from '@/tests/lib/custom-render'
import { addAPIMock } from '@/tests/lib/msw'
import { addAPIMock, APIErrorBody } from '@/tests/lib/msw'
mockAnimationsApi()
@@ -41,6 +41,30 @@ const mockProjectAndSchemas = ({ highAvailability }: { highAvailability: boolean
})
}
const mockProjectAndFailingSchemas = () => {
addAPIMock({
method: 'get',
path: '/platform/projects/:ref',
// @ts-expect-error partial project response
response: {
cloud_provider: 'localhost',
id: 1,
inserted_at: '2021-08-02T06:40:40.646Z',
name: 'Default Project',
organization_id: 1,
ref: 'default',
region: 'local',
status: 'ACTIVE_HEALTHY',
},
})
addAPIMock({
method: 'post',
path: '/platform/pg-meta/:ref/query',
response: () =>
HttpResponse.json<APIErrorBody>({ message: 'Service unavailable' }, { status: 503 }),
})
}
const renderAndOpenSelector = async () => {
customRender(<SchemaSelector selectedSchemaName="public" onSelectSchema={vi.fn()} />)
@@ -66,4 +90,16 @@ describe('SchemaSelector', () => {
expect(screen.getByRole('option', { name: 'multigres' })).toBeInTheDocument()
})
it('offers to restart the database when schemas fail to load', async () => {
mockProjectAndFailingSchemas()
customRender(<SchemaSelector selectedSchemaName="public" onSelectSchema={vi.fn()} />)
await userEvent.click(await screen.findByRole('button', { name: 'Restart database' }))
expect(
await screen.findByText(/are you sure you want to restart your database/i)
).toBeInTheDocument()
})
})
+15 -3
View File
@@ -20,6 +20,7 @@ import {
Skeleton,
} from 'ui'
import { RestartProjectDialog } from '@/components/interfaces/ErrorHandling/RestartProjectDialog'
import { useSchemasQuery } from '@/data/database/schemas-query'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
import { useSchemasFilteredForHighAvailability } from '@/hooks/misc/useHighAvailability'
@@ -65,6 +66,7 @@ export const SchemaSelector = forwardRef<HTMLDivElement, SchemaSelectorProps>(
ref
) => {
const [internalOpen, setInternalOpen] = useState(false)
const [isRestartDialogVisible, setIsRestartDialogVisible] = useState(false)
const isControlled = openProp !== undefined
const open = isControlled ? openProp : internalOpen
const setOpen = (next: boolean) => {
@@ -119,9 +121,19 @@ export const SchemaSelector = forwardRef<HTMLDivElement, SchemaSelectorProps>(
<AlertDescription className="text-xs mb-2 wrap-break-word">
Error: {(schemasError as any)?.message}
</AlertDescription>
<Button variant="default" size="tiny" onClick={() => refetchSchemas()}>
Reload schemas
</Button>
<div className="flex flex-wrap gap-2">
<Button variant="default" size="tiny" onClick={() => refetchSchemas()}>
Reload schemas
</Button>
<Button variant="default" size="tiny" onClick={() => setIsRestartDialogVisible(true)}>
Restart database
</Button>
</div>
<RestartProjectDialog
visible={isRestartDialogVisible}
onClose={() => setIsRestartDialogVisible(false)}
restartType="database"
/>
</Alert>
)}