This is an automated email from the ASF dual-hosted git repository.

justinpark 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 3a3984006c chore(explore): Add format sql and view in SQL Lab option 
in View Query (#33341)
3a3984006c is described below

commit 3a3984006cfd506449323125f0a3138fe7650964
Author: JUST.in DO IT <[email protected]>
AuthorDate: Mon Jun 9 15:11:54 2025 -0700

    chore(explore): Add format sql and view in SQL Lab option in View Query 
(#33341)
---
 .../explore/components/controls/ViewQuery.test.tsx | 158 +++++++++++++++++++++
 .../src/explore/components/controls/ViewQuery.tsx  | 144 ++++++++++++++++---
 .../explore/components/controls/ViewQueryModal.tsx |  15 +-
 3 files changed, 294 insertions(+), 23 deletions(-)

diff --git 
a/superset-frontend/src/explore/components/controls/ViewQuery.test.tsx 
b/superset-frontend/src/explore/components/controls/ViewQuery.test.tsx
new file mode 100644
index 0000000000..9a18020c34
--- /dev/null
+++ b/superset-frontend/src/explore/components/controls/ViewQuery.test.tsx
@@ -0,0 +1,158 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {
+  screen,
+  render,
+  fireEvent,
+  waitFor,
+} from 'spec/helpers/testing-library';
+import fetchMock from 'fetch-mock';
+import copyTextToClipboard from 'src/utils/copy';
+import ViewQuery, { ViewQueryProps } from './ViewQuery';
+
+const mockHistoryPush = jest.fn();
+jest.mock('react-router-dom', () => ({
+  ...jest.requireActual('react-router-dom'),
+  useHistory: () => ({
+    push: mockHistoryPush,
+  }),
+}));
+
+jest.mock('src/utils/copy');
+
+function setup(props: ViewQueryProps) {
+  return render(<ViewQuery {...props} />, { useRouter: true, useRedux: true });
+}
+
+const mockProps = {
+  sql: 'select * from table',
+  datasource: '1__table',
+};
+
+const datasetApiEndpoint = 'glob:*/api/v1/dataset/1?**';
+const formatSqlEndpoint = 'glob:*/api/v1/sqllab/format_sql/';
+const formattedSQL = 'SELECT * FROM table;';
+
+beforeEach(() => {
+  fetchMock.get(datasetApiEndpoint, {
+    result: {
+      database: {
+        backend: 'sqlite',
+      },
+    },
+  });
+  fetchMock.post(formatSqlEndpoint, {
+    result: formattedSQL,
+  });
+});
+
+afterEach(() => {
+  jest.resetAllMocks();
+  fetchMock.restore();
+});
+
+const getFormatSwitch = () =>
+  screen.getByRole('switch', { name: 'Show original SQL' });
+
+test('renders the component with Formatted SQL and buttons', async () => {
+  const { container } = setup(mockProps);
+  expect(screen.getByText('Copy')).toBeInTheDocument();
+  expect(getFormatSwitch()).toBeInTheDocument();
+  expect(screen.getByText('View in SQL Lab')).toBeInTheDocument();
+
+  await waitFor(() =>
+    expect(fetchMock.calls(formatSqlEndpoint)).toHaveLength(1),
+  );
+
+  expect(container).toHaveTextContent(formattedSQL);
+});
+
+test('copies the SQL to the clipboard when Copy button is clicked', async () 
=> {
+  setup(mockProps);
+
+  (copyTextToClipboard as jest.Mock).mockResolvedValue('');
+  const copyButton = screen.getByText('Copy');
+  expect(copyTextToClipboard as jest.Mock).not.toHaveBeenCalled();
+  fireEvent.click(copyButton);
+  expect(copyTextToClipboard as jest.Mock).toHaveBeenCalled();
+});
+
+test('shows the original SQL when Format switch is unchecked', async () => {
+  const { container } = setup(mockProps);
+  const formatButton = getFormatSwitch();
+
+  await waitFor(() =>
+    expect(fetchMock.calls(formatSqlEndpoint)).toHaveLength(1),
+  );
+
+  fireEvent.click(formatButton);
+
+  expect(container).toHaveTextContent(mockProps.sql);
+});
+
+test('toggles back to formatted SQL when Format switch is clicked', async () 
=> {
+  const { container } = setup(mockProps);
+  const formatButton = getFormatSwitch();
+
+  await waitFor(() =>
+    expect(fetchMock.calls(formatSqlEndpoint)).toHaveLength(1),
+  );
+
+  // Click to format SQL
+  fireEvent.click(formatButton);
+
+  await waitFor(() => expect(container).toHaveTextContent(mockProps.sql));
+
+  // Toggle format switch
+  fireEvent.click(formatButton);
+
+  await waitFor(() => expect(container).toHaveTextContent(formattedSQL));
+});
+
+test('navigates to SQL Lab when View in SQL Lab button is clicked', () => {
+  setup(mockProps);
+
+  const viewInSQLLabButton = screen.getByText('View in SQL Lab');
+  fireEvent.click(viewInSQLLabButton);
+
+  expect(mockHistoryPush).toHaveBeenCalledWith('/sqllab', {
+    state: {
+      requestedQuery: {
+        datasourceKey: mockProps.datasource,
+        sql: mockProps.sql,
+      },
+    },
+  });
+});
+
+test('opens SQL Lab in a new tab when View in SQL Lab button is clicked with 
meta key', () => {
+  window.open = jest.fn();
+
+  setup(mockProps);
+  const viewInSQLLabButton = screen.getByText('View in SQL Lab');
+
+  fireEvent.click(viewInSQLLabButton, { metaKey: true });
+
+  const { datasource, sql } = mockProps;
+  expect(window.open).toHaveBeenCalledWith(
+    `/sqllab?datasourceKey=${datasource}&sql=${sql}`,
+    '_blank',
+  );
+});
diff --git a/superset-frontend/src/explore/components/controls/ViewQuery.tsx 
b/superset-frontend/src/explore/components/controls/ViewQuery.tsx
index 1b3e359ce1..804ca87997 100644
--- a/superset-frontend/src/explore/components/controls/ViewQuery.tsx
+++ b/superset-frontend/src/explore/components/controls/ViewQuery.tsx
@@ -16,10 +16,16 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-// TODO: Remove fa-icon
-/* eslint-disable icons/no-fa-icons-usage */
-import { FC } from 'react';
-import { styled } from '@superset-ui/core';
+import {
+  FC,
+  KeyboardEvent,
+  MouseEvent,
+  useCallback,
+  useEffect,
+  useState,
+} from 'react';
+import rison from 'rison';
+import { styled, SupersetClient, t } from '@superset-ui/core';
 import SyntaxHighlighter from 'react-syntax-highlighter/dist/cjs/light';
 import github from 'react-syntax-highlighter/dist/cjs/styles/hljs/github';
 import CopyToClipboard from 'src/components/CopyToClipboard';
@@ -28,6 +34,9 @@ import markdownSyntax from 
'react-syntax-highlighter/dist/cjs/languages/hljs/mar
 import htmlSyntax from 
'react-syntax-highlighter/dist/cjs/languages/hljs/htmlbars';
 import sqlSyntax from 'react-syntax-highlighter/dist/cjs/languages/hljs/sql';
 import jsonSyntax from 'react-syntax-highlighter/dist/cjs/languages/hljs/json';
+import { useHistory } from 'react-router-dom';
+import { Switch } from 'src/components/Switch';
+import { Button, Skeleton } from 'src/components';
 
 const CopyButtonViewQuery = styled(CopyButton)`
   && {
@@ -40,8 +49,9 @@ SyntaxHighlighter.registerLanguage('html', htmlSyntax);
 SyntaxHighlighter.registerLanguage('sql', sqlSyntax);
 SyntaxHighlighter.registerLanguage('json', jsonSyntax);
 
-interface ViewQueryProps {
+export interface ViewQueryProps {
   sql: string;
+  datasource: string;
   language?: string;
 }
 
@@ -51,26 +61,124 @@ const StyledSyntaxContainer = styled.div`
   flex-direction: column;
 `;
 
+const StyledHeaderMenuContainer = styled.div`
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  margin-top: ${({ theme }) => -theme.gridUnit * 4}px;
+  align-items: flex-end;
+`;
+
+const StyledHeaderActionContainer = styled.div`
+  display: flex;
+  flex-direction: row;
+  column-gap: ${({ theme }) => theme.gridUnit * 2}px;
+`;
+
 const StyledSyntaxHighlighter = styled(SyntaxHighlighter)`
   flex: 1;
 `;
 
+const StyledLabel = styled.label`
+  font-size: ${({ theme }) => theme.typography.sizes.m}px;
+`;
+
+const DATASET_BACKEND_QUERY = {
+  keys: ['none'],
+  columns: ['database.backend'],
+};
+
 const ViewQuery: FC<ViewQueryProps> = props => {
-  const { sql, language = 'sql' } = props;
+  const { sql, language = 'sql', datasource } = props;
+  const datasetId = datasource.split('__')[0];
+  const [formattedSQL, setFormattedSQL] = useState<string>();
+  const [showFormatSQL, setShowFormatSQL] = useState(true);
+  const history = useHistory();
+  const currentSQL = (showFormatSQL ? formattedSQL : sql) ?? sql;
+
+  const formatCurrentQuery = useCallback(() => {
+    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);
+        });
+    }
+  }, [sql, datasetId, formattedSQL]);
+
+  const navToSQLLab = useCallback(
+    (domEvent: KeyboardEvent<HTMLElement> | MouseEvent<HTMLElement>) => {
+      const requestedQuery = {
+        datasourceKey: datasource,
+        sql: currentSQL,
+      };
+      if (domEvent.metaKey || domEvent.ctrlKey) {
+        domEvent.preventDefault();
+        window.open(
+          `/sqllab?datasourceKey=${datasource}&sql=${currentSQL}`,
+          '_blank',
+        );
+      } else {
+        history.push('/sqllab', { state: { requestedQuery } });
+      }
+    },
+    [history, datasource, currentSQL],
+  );
+
+  useEffect(() => {
+    formatCurrentQuery();
+  }, [sql]);
+
   return (
     <StyledSyntaxContainer key={sql}>
-      <CopyToClipboard
-        text={sql}
-        shouldShowText={false}
-        copyNode={
-          <CopyButtonViewQuery buttonSize="xsmall">
-            <i className="fa fa-clipboard" />
-          </CopyButtonViewQuery>
-        }
-      />
-      <StyledSyntaxHighlighter language={language} style={github}>
-        {sql}
-      </StyledSyntaxHighlighter>
+      <StyledHeaderMenuContainer>
+        <StyledHeaderActionContainer>
+          <CopyToClipboard
+            text={currentSQL}
+            shouldShowText={false}
+            copyNode={
+              <CopyButtonViewQuery buttonSize="small">
+                {t('Copy')}
+              </CopyButtonViewQuery>
+            }
+          />
+          <Button onClick={navToSQLLab}>{t('View in SQL Lab')}</Button>
+        </StyledHeaderActionContainer>
+        <StyledHeaderActionContainer>
+          <Switch
+            id="formatSwitch"
+            checked={!showFormatSQL}
+            onChange={formatCurrentQuery}
+          />
+          <StyledLabel htmlFor="formatSwitch">
+            {t('Show original SQL')}
+          </StyledLabel>
+        </StyledHeaderActionContainer>
+      </StyledHeaderMenuContainer>
+      {!formattedSQL && <Skeleton active />}
+      {formattedSQL && (
+        <StyledSyntaxHighlighter language={language} style={github}>
+          {currentSQL}
+        </StyledSyntaxHighlighter>
+      )}
     </StyledSyntaxContainer>
   );
 };
diff --git 
a/superset-frontend/src/explore/components/controls/ViewQueryModal.tsx 
b/superset-frontend/src/explore/components/controls/ViewQueryModal.tsx
index ab4093d4bb..4d25b22b2a 100644
--- a/superset-frontend/src/explore/components/controls/ViewQueryModal.tsx
+++ b/superset-frontend/src/explore/components/controls/ViewQueryModal.tsx
@@ -23,13 +23,14 @@ import {
   ensureIsArray,
   t,
   getClientErrorObject,
+  QueryFormData,
 } from '@superset-ui/core';
 import Loading from 'src/components/Loading';
 import { getChartDataRequest } from 'src/components/Chart/chartAction';
 import ViewQuery from 'src/explore/components/controls/ViewQuery';
 
 interface Props {
-  latestQueryFormData: object;
+  latestQueryFormData: QueryFormData;
 }
 
 type Result = {
@@ -43,7 +44,7 @@ const ViewQueryModalContainer = styled.div`
   flex-direction: column;
 `;
 
-const ViewQueryModal: FC<Props> = props => {
+const ViewQueryModal: FC<Props> = ({ latestQueryFormData }) => {
   const [result, setResult] = useState<Result[]>([]);
   const [isLoading, setIsLoading] = useState(false);
   const [error, setError] = useState<string | null>(null);
@@ -51,7 +52,7 @@ const ViewQueryModal: FC<Props> = props => {
   const loadChartData = (resultType: string) => {
     setIsLoading(true);
     getChartDataRequest({
-      formData: props.latestQueryFormData,
+      formData: latestQueryFormData,
       resultFormat: 'json',
       resultType,
     })
@@ -74,7 +75,7 @@ const ViewQueryModal: FC<Props> = props => {
   };
   useEffect(() => {
     loadChartData('query');
-  }, [JSON.stringify(props.latestQueryFormData)]);
+  }, [JSON.stringify(latestQueryFormData)]);
 
   if (isLoading) {
     return <Loading />;
@@ -87,7 +88,11 @@ const ViewQueryModal: FC<Props> = props => {
     <ViewQueryModalContainer>
       {result.map(item =>
         item.query ? (
-          <ViewQuery sql={item.query} language={item.language || undefined} />
+          <ViewQuery
+            datasource={latestQueryFormData.datasource}
+            sql={item.query}
+            language={item.language || undefined}
+          />
         ) : null,
       )}
     </ViewQueryModalContainer>

Reply via email to