Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
61 changes: 36 additions & 25 deletions superset-frontend/src/explore/components/controls/ViewQuery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand All @@ -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]);

Expand Down Expand Up @@ -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 }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,18 +76,21 @@ const ViewQueryModalFooter: FC<ViewQueryModalFooterProps> = (props: {
return (
<div>
<Button
buttonStyle="secondary"
onClick={() => {
props?.closeModal?.();
props?.changeDatasource?.();
}}
>
{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?.();
}}
Expand Down
8 changes: 3 additions & 5 deletions superset-frontend/src/features/queries/QueryPreviewModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) {
Expand Down Expand Up @@ -110,6 +107,7 @@ function QueryPreviewModal({
<Button
data-test="previous-query"
key="previous-query"
buttonStyle="secondary"
disabled={disablePrevious}
onClick={() => handleDataChange(true)}
>
Expand All @@ -118,6 +116,7 @@ function QueryPreviewModal({
<Button
data-test="next-query"
key="next-query"
buttonStyle="secondary"
disabled={disableNext}
onClick={() => handleDataChange(false)}
>
Expand All @@ -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')}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)`
Expand Down Expand Up @@ -89,6 +89,7 @@ const SavedQueryPreviewModal: FunctionComponent<
<Button
data-test="previous-saved-query"
key="previous-saved-query"
buttonStyle="secondary"
disabled={disablePrevious}
onClick={() => handleDataChange(true)}
>
Expand All @@ -97,6 +98,7 @@ const SavedQueryPreviewModal: FunctionComponent<
<Button
data-test="next-saved-query"
key="next-saved-query"
buttonStyle="secondary"
disabled={disableNext}
onClick={() => handleDataChange(false)}
>
Expand All @@ -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))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import copyTextToClipboard from 'src/utils/copy';

const SyntaxHighlighterWrapper = styled.div`
position: relative;
margin-top: -24px;

&:hover {
.copy-button {
Expand Down
11 changes: 7 additions & 4 deletions superset-frontend/src/pages/QueryHistoryList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ function QueryList({ addDangerToast }: QueryListProps) {
) {
statusConfig.name = (
<Icons.CloseOutlined
iconSize="xs"
iconSize="m"
iconColor={
status === QueryState.Failed
? theme.colorError
Expand All @@ -201,19 +201,22 @@ function QueryList({ addDangerToast }: QueryListProps) {
statusConfig.label = t('Failed');
} else if (status === QueryState.Running) {
statusConfig.name = (
<Icons.Running iconColor={theme.colorPrimary} />
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed this icon because running.svg was our custom icon, not antd, and it's viewport was very weird, causing the actual path to be very small even when I set a bigger icon size

<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 (
Expand Down
Loading