This is an automated email from the ASF dual-hosted git repository. kgabryje pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push: new 5566eb8dd6 fix: Undefined error when viewing query in Explore + visual fixes (#34869) 5566eb8dd6 is described below commit 5566eb8dd625d79227cb0bb9e2f6284852993bc3 Author: Kamil Gabryjelski <kamil.gabryjel...@gmail.com> AuthorDate: Thu Aug 28 12:40:29 2025 +0200 fix: Undefined error when viewing query in Explore + visual fixes (#34869) --- .../explore/components/controls/ViewQuery.test.tsx | 74 ++++++++++++++++++++++ .../src/explore/components/controls/ViewQuery.tsx | 61 ++++++++++-------- .../components/controls/ViewQueryModalFooter.tsx | 7 +- .../src/features/queries/QueryPreviewModal.tsx | 8 +-- .../features/queries/SavedQueryPreviewModal.tsx | 5 +- .../src/features/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 a7f655b3b4..1340c1af10 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 048413af46..ed9cd7d9de 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<ViewQueryProps> = 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<string>(); const [showFormatSQL, setShowFormatSQL] = useState(true); const history = useHistory(); @@ -88,31 +92,37 @@ const ViewQuery: FC<ViewQueryProps> = 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<ViewQueryProps> = props => { return ( <Card bodyStyle={{ padding: theme.sizeUnit * 4 }}> <StyledSyntaxContainer key={sql}> - {!formattedSQL && <Skeleton active />} - {formattedSQL && ( + {!formattedSQL && showFormatSQL ? ( + <Skeleton active /> + ) : ( <StyledThemedSyntaxHighlighter language={language} customStyle={{ flex: 1, marginBottom: theme.sizeUnit * 3 }} diff --git a/superset-frontend/src/explore/components/controls/ViewQueryModalFooter.tsx b/superset-frontend/src/explore/components/controls/ViewQueryModalFooter.tsx index 3fa59a9a5e..5834cbd16e 100644 --- a/superset-frontend/src/explore/components/controls/ViewQueryModalFooter.tsx +++ b/superset-frontend/src/explore/components/controls/ViewQueryModalFooter.tsx @@ -76,6 +76,7 @@ const ViewQueryModalFooter: FC<ViewQueryModalFooterProps> = (props: { return ( <div> <Button + buttonStyle="secondary" onClick={() => { props?.closeModal?.(); props?.changeDatasource?.(); @@ -83,11 +84,13 @@ const ViewQueryModalFooter: FC<ViewQueryModalFooterProps> = (props: { > {SAVE_AS_DATASET} </Button> - <Button onClick={({ metaKey }) => openSQL(Boolean(metaKey))}> + <Button + buttonStyle="secondary" + onClick={({ metaKey }) => openSQL(Boolean(metaKey))} + > {OPEN_IN_SQL_LAB} </Button> <Button - buttonStyle="primary" onClick={() => { props?.closeModal?.(); }} diff --git a/superset-frontend/src/features/queries/QueryPreviewModal.tsx b/superset-frontend/src/features/queries/QueryPreviewModal.tsx index 447b3e7d8a..1f7f7a0db9 100644 --- a/superset-frontend/src/features/queries/QueryPreviewModal.tsx +++ b/superset-frontend/src/features/queries/QueryPreviewModal.tsx @@ -40,11 +40,10 @@ const QueryLabel = styled.div` `; const QueryViewToggle = styled.div` - margin: 0 0 ${({ theme }) => theme.sizeUnit * 6}px 0; + display: flex; `; const TabButton = styled.div` - display: inline; font-size: ${({ theme }) => theme.fontSizeSM}px; padding: ${({ theme }) => theme.sizeUnit * 2}px ${({ theme }) => theme.sizeUnit * 4}px; @@ -55,9 +54,7 @@ const TabButton = styled.div` &:focus, &:hover { background: ${({ theme }) => theme.colorPrimaryBg}; - border-bottom: none; border-radius: ${({ theme }) => theme.borderRadius}px; - margin-bottom: ${({ theme }) => theme.sizeUnit * 2}px; } &:hover:not(.active) { @@ -110,6 +107,7 @@ function QueryPreviewModal({ <Button data-test="previous-query" key="previous-query" + buttonStyle="secondary" disabled={disablePrevious} onClick={() => handleDataChange(true)} > @@ -118,6 +116,7 @@ function QueryPreviewModal({ <Button data-test="next-query" key="next-query" + buttonStyle="secondary" disabled={disableNext} onClick={() => handleDataChange(false)} > @@ -126,7 +125,6 @@ function QueryPreviewModal({ <Button data-test="open-in-sql-lab" key="open-in-sql-lab" - buttonStyle="primary" onClick={() => openInSqlLab(id)} > {t('Open in SQL Lab')} diff --git a/superset-frontend/src/features/queries/SavedQueryPreviewModal.tsx b/superset-frontend/src/features/queries/SavedQueryPreviewModal.tsx index ce888945e0..9e6b69a6d6 100644 --- a/superset-frontend/src/features/queries/SavedQueryPreviewModal.tsx +++ b/superset-frontend/src/features/queries/SavedQueryPreviewModal.tsx @@ -34,7 +34,7 @@ const QueryTitle = styled.div` const QueryLabel = styled.div` color: ${({ theme }) => theme.colorTextLabel}; font-size: ${({ theme }) => theme.fontSize}px; - padding: 4px 0 16px 0; + padding-top: ${({ theme }) => theme.sizeUnit}px; `; const StyledModal = styled(Modal)` @@ -89,6 +89,7 @@ const SavedQueryPreviewModal: FunctionComponent< <Button data-test="previous-saved-query" key="previous-saved-query" + buttonStyle="secondary" disabled={disablePrevious} onClick={() => handleDataChange(true)} > @@ -97,6 +98,7 @@ const SavedQueryPreviewModal: FunctionComponent< <Button data-test="next-saved-query" key="next-saved-query" + buttonStyle="secondary" disabled={disableNext} onClick={() => handleDataChange(false)} > @@ -105,7 +107,6 @@ const SavedQueryPreviewModal: FunctionComponent< <Button data-test="open-in-sql-lab" key="open-in-sql-lab" - buttonStyle="primary" onClick={({ metaKey }) => openInSqlLab(savedQuery.id, Boolean(metaKey)) } diff --git a/superset-frontend/src/features/queries/SyntaxHighlighterCopy.tsx b/superset-frontend/src/features/queries/SyntaxHighlighterCopy.tsx index 4b38e381a3..5262548f48 100644 --- a/superset-frontend/src/features/queries/SyntaxHighlighterCopy.tsx +++ b/superset-frontend/src/features/queries/SyntaxHighlighterCopy.tsx @@ -29,7 +29,6 @@ import copyTextToClipboard from 'src/utils/copy'; const SyntaxHighlighterWrapper = styled.div` position: relative; - margin-top: -24px; &:hover { .copy-button { diff --git a/superset-frontend/src/pages/QueryHistoryList/index.tsx b/superset-frontend/src/pages/QueryHistoryList/index.tsx index dc4de5b309..e2291678ff 100644 --- a/superset-frontend/src/pages/QueryHistoryList/index.tsx +++ b/superset-frontend/src/pages/QueryHistoryList/index.tsx @@ -190,7 +190,7 @@ function QueryList({ addDangerToast }: QueryListProps) { ) { statusConfig.name = ( <Icons.CloseOutlined - iconSize="xs" + iconSize="m" iconColor={ status === QueryState.Failed ? theme.colorError @@ -201,19 +201,22 @@ function QueryList({ addDangerToast }: QueryListProps) { statusConfig.label = t('Failed'); } else if (status === QueryState.Running) { statusConfig.name = ( - <Icons.Running iconColor={theme.colorPrimary} /> + <Icons.LoadingOutlined + iconSize="m" + iconColor={theme.colorPrimary} + /> ); statusConfig.label = t('Running'); } else if (status === QueryState.TimedOut) { statusConfig.name = ( - <Icons.CircleSolid iconColor={theme.colorIcon} /> + <Icons.CircleSolid iconSize="m" iconColor={theme.colorIcon} /> ); statusConfig.label = t('Offline'); } else if ( status === QueryState.Scheduled || status === QueryState.Pending ) { - statusConfig.name = <Icons.Queued />; + statusConfig.name = <Icons.Queued iconSize="m" />; statusConfig.label = t('Scheduled'); } return (