From a66dae48f27ee7023483c1846b492b6ff05b277f Mon Sep 17 00:00:00 2001 From: Jordi Enric <37541088+jordienr@users.noreply.github.com> Date: Tue, 4 Aug 2026 14:36:06 +0200 Subject: [PATCH] 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 ## 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. --- .../ErrorHandling/ErrorMatcher.test.tsx | 26 +++++++++++++ .../interfaces/ErrorHandling/ErrorMatcher.tsx | 13 ++++++- .../RestartTroubleshootingFallback.tsx | 15 ++++++++ .../TableEditorLayout/TableEditorMenu.tsx | 2 + .../components/ui/SchemaSelector.test.tsx | 38 ++++++++++++++++++- apps/studio/components/ui/SchemaSelector.tsx | 18 +++++++-- 6 files changed, 106 insertions(+), 6 deletions(-) create mode 100644 apps/studio/components/interfaces/ErrorHandling/RestartTroubleshootingFallback.tsx diff --git a/apps/studio/components/interfaces/ErrorHandling/ErrorMatcher.test.tsx b/apps/studio/components/interfaces/ErrorHandling/ErrorMatcher.test.tsx index 01e58b27988..d3f551ce4d5 100644 --- a/apps/studio/components/interfaces/ErrorHandling/ErrorMatcher.test.tsx +++ b/apps/studio/components/interfaces/ErrorHandling/ErrorMatcher.test.tsx @@ -66,6 +66,32 @@ describe('ErrorMatcher', () => { expect(screen.getByText('UNKNOWN ERROR')).toBeInTheDocument() }) + it('renders the caller-provided fallback when the error is unclassified', () => { + render( + Custom fallback} + /> + ) + 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( + Custom fallback} + /> + ) + expect(screen.queryByText('Custom fallback')).not.toBeInTheDocument() + expect(screen.getByText('Try restarting your project')).toBeInTheDocument() + }) + it('accepts error as object with message property', () => { render( - {Troubleshooting && } + {Troubleshooting ? : fallback} ) } diff --git a/apps/studio/components/interfaces/ErrorHandling/RestartTroubleshootingFallback.tsx b/apps/studio/components/interfaces/ErrorHandling/RestartTroubleshootingFallback.tsx new file mode 100644 index 00000000000..6d715d98ff6 --- /dev/null +++ b/apps/studio/components/interfaces/ErrorHandling/RestartTroubleshootingFallback.tsx @@ -0,0 +1,15 @@ +import { TroubleshootingAccordion } from './TroubleshootingAccordion' +import { RestartDatabaseTroubleshootingSection } from './TroubleshootingSections' + +const ERROR_TYPE = 'unknown' + +export function RestartTroubleshootingFallback() { + return ( + + + + ) +} diff --git a/apps/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx b/apps/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx index 80710c92136..7dcf8969319 100644 --- a/apps/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx +++ b/apps/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx @@ -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={} /> )} diff --git a/apps/studio/components/ui/SchemaSelector.test.tsx b/apps/studio/components/ui/SchemaSelector.test.tsx index 0cff282ba8b..20500671f37 100644 --- a/apps/studio/components/ui/SchemaSelector.test.tsx +++ b/apps/studio/components/ui/SchemaSelector.test.tsx @@ -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({ message: 'Service unavailable' }, { status: 503 }), + }) +} + const renderAndOpenSelector = async () => { customRender() @@ -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() + + 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() + }) }) diff --git a/apps/studio/components/ui/SchemaSelector.tsx b/apps/studio/components/ui/SchemaSelector.tsx index a051ce4c6f4..d7cd9d1a0c3 100644 --- a/apps/studio/components/ui/SchemaSelector.tsx +++ b/apps/studio/components/ui/SchemaSelector.tsx @@ -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( 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( Error: {(schemasError as any)?.message} - +
+ + +
+ setIsRestartDialogVisible(false)} + restartType="database" + /> )}