From 5566eb8dd625d79227cb0bb9e2f6284852993bc3 Mon Sep 17 00:00:00 2001 From: Kamil Gabryjelski Date: Thu, 28 Aug 2025 12:40:29 +0200 Subject: [PATCH] fix: Undefined error when viewing query in Explore + visual fixes (#34869) --- .../components/controls/ViewQuery.test.tsx | 74 +++++++++++++++++++ .../explore/components/controls/ViewQuery.tsx | 61 ++++++++------- .../controls/ViewQueryModalFooter.tsx | 7 +- .../features/queries/QueryPreviewModal.tsx | 8 +- .../queries/SavedQueryPreviewModal.tsx | 5 +- .../queries/SyntaxHighlighterCopy.tsx | 1 - .../src/pages/QueryHistoryList/index.tsx | 11 ++- 7 files changed, 128 insertions(+), 39 deletions(-) diff --git a/superset-frontend/src/explore/components/controls/ViewQuery.test.tsx b/superset-frontend/src/explore/components/controls/ViewQuery.test.tsx index a7f655b3b48..1340c1af109 100644 --- a/superset-frontend/src/explore/components/controls/ViewQuery.test.tsx +++ b/superset-frontend/src/explore/components/controls/ViewQuery.test.tsx @@ -191,3 +191,77 @@ test('hides View in SQL Lab button when user does not have SQL Lab access', () = expect(screen.queryByText('View in SQL Lab')).not.toBeInTheDocument(); expect(screen.getByText('Copy')).toBeInTheDocument(); // Copy button should still be visible }); + +test('handles undefined datasource without crashing', () => { + const propsWithUndefinedDatasource = { + ...mockProps, + datasource: undefined as any, + }; + + expect(() => setup(propsWithUndefinedDatasource)).not.toThrow(); +}); + +test('handles dataset API error gracefully when no exploreBackend', async () => { + const stateWithoutBackend = { + ...mockState(), + explore: undefined, + }; + + fetchMock.get( + datasetApiEndpoint, + { throws: new Error('API Error') }, + { overwriteRoutes: true }, + ); + + setup(mockProps, stateWithoutBackend); + + await waitFor(() => { + expect(screen.queryByRole('progressbar')).not.toBeInTheDocument(); + }); + + expect(fetchMock.calls(formatSqlEndpoint)).toHaveLength(0); +}); + +test('handles SQL formatting API error gracefully', async () => { + const stateWithoutBackend = { + ...mockState(), + explore: undefined, + }; + + fetchMock.post( + formatSqlEndpoint, + { throws: new Error('Format Error') }, + { overwriteRoutes: true }, + ); + + setup(mockProps, stateWithoutBackend); + + await waitFor(() => { + expect(screen.queryByRole('progressbar')).not.toBeInTheDocument(); + }); +}); + +test('uses exploreBackend from Redux state when available', async () => { + const stateWithBackend = { + ...mockState(), + explore: { + datasource: { + database: { + backend: 'postgresql', + }, + }, + }, + }; + + setup(mockProps, stateWithBackend); + + await waitFor(() => { + expect(fetchMock.calls(formatSqlEndpoint)).toHaveLength(1); + }); + + const formatCallBody = JSON.parse( + fetchMock.lastCall(formatSqlEndpoint)?.[1]?.body as string, + ); + expect(formatCallBody.engine).toBe('postgresql'); + expect(fetchMock.calls(datasetApiEndpoint)).toHaveLength(0); +}); diff --git a/superset-frontend/src/explore/components/controls/ViewQuery.tsx b/superset-frontend/src/explore/components/controls/ViewQuery.tsx index 048413af466..ed9cd7d9de4 100644 --- a/superset-frontend/src/explore/components/controls/ViewQuery.tsx +++ b/superset-frontend/src/explore/components/controls/ViewQuery.tsx @@ -43,6 +43,7 @@ import CodeSyntaxHighlighter, { preloadLanguages, } from '@superset-ui/core/components/CodeSyntaxHighlighter'; import { useHistory } from 'react-router-dom'; +import { ExplorePageState } from 'src/explore/types'; export interface ViewQueryProps { sql: string; @@ -74,7 +75,10 @@ const DATASET_BACKEND_QUERY = { const ViewQuery: FC = props => { const { sql, language = 'sql', datasource } = props; const theme = useTheme(); - const datasetId = datasource.split('__')[0]; + const datasetId = datasource?.split('__')[0]; + const exploreBackend = useSelector( + (state: ExplorePageState) => state.explore?.datasource?.database?.backend, + ); const [formattedSQL, setFormattedSQL] = useState(); const [showFormatSQL, setShowFormatSQL] = useState(true); const history = useHistory(); @@ -88,31 +92,37 @@ const ViewQuery: FC = props => { preloadLanguages([language]); }, [language]); - const formatCurrentQuery = useCallback(() => { + const formatCurrentQuery = useCallback(async () => { if (formattedSQL) { setShowFormatSQL(val => !val); - } else { - const queryParams = rison.encode(DATASET_BACKEND_QUERY); - SupersetClient.get({ - endpoint: `/api/v1/dataset/${datasetId}?q=${queryParams}`, - }) - .then(({ json }) => - SupersetClient.post({ - endpoint: `/api/v1/sqllab/format_sql/`, - body: JSON.stringify({ - sql, - engine: json.result.database.backend, - }), - headers: { 'Content-Type': 'application/json' }, - }), - ) - .then(({ json }) => { - setFormattedSQL(json.result); - setShowFormatSQL(true); - }) - .catch(() => { - setShowFormatSQL(true); + return; + } + try { + let backend = exploreBackend; + + // Fetch backend info if not available in Redux state + if (!backend) { + const queryParams = rison.encode(DATASET_BACKEND_QUERY); + const response = await SupersetClient.get({ + endpoint: `/api/v1/dataset/${datasetId}?q=${queryParams}`, }); + backend = response.json.result.database; + } + + // Format the SQL query + const formatResponse = await SupersetClient.post({ + endpoint: `/api/v1/sqllab/format_sql/`, + body: JSON.stringify({ + sql, + engine: backend, + }), + headers: { 'Content-Type': 'application/json' }, + }); + + setFormattedSQL(formatResponse.json.result); + setShowFormatSQL(true); + } catch (error) { + setShowFormatSQL(false); } }, [sql, datasetId, formattedSQL]); @@ -142,8 +152,9 @@ const ViewQuery: FC = props => { return ( - {!formattedSQL && } - {formattedSQL && ( + {!formattedSQL && showFormatSQL ? ( + + ) : ( = (props: { return (
-