You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ju...@apache.org on 2023/10/13 15:16:58 UTC

[superset] branch master updated: fix(sqllab): Allow opening of SQL Lab in new browser tab (#25582)

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 003001f19f fix(sqllab): Allow opening of SQL Lab in new browser tab  (#25582)
003001f19f is described below

commit 003001f19f9857f4ddca88046685077020ea90c4
Author: JUST.in DO IT <ju...@airbnb.com>
AuthorDate: Fri Oct 13 11:16:50 2023 -0400

    fix(sqllab): Allow opening of SQL Lab in new browser tab  (#25582)
---
 .../src/components/Chart/chartAction.js            | 19 +++++++----
 .../components/ExploreChartHeader/index.jsx        |  4 +--
 .../controls/DatasourceControl/index.jsx           | 37 ++++++++++++++++++++--
 .../components/controls/ViewQueryModalFooter.tsx   | 33 ++++++++++++-------
 .../useExploreAdditionalActionsMenu/index.jsx      |  2 +-
 .../queries/SavedQueryPreviewModal.test.jsx        |  2 +-
 .../features/queries/SavedQueryPreviewModal.tsx    |  6 ++--
 .../src/pages/SavedQueryList/index.tsx             | 11 +++++--
 superset/views/base.py                             |  5 ++-
 superset/views/sqllab.py                           | 12 +++++--
 10 files changed, 99 insertions(+), 32 deletions(-)

diff --git a/superset-frontend/src/components/Chart/chartAction.js b/superset-frontend/src/components/Chart/chartAction.js
index fcf45a4946..9e5dc0eddd 100644
--- a/superset-frontend/src/components/Chart/chartAction.js
+++ b/superset-frontend/src/components/Chart/chartAction.js
@@ -42,6 +42,7 @@ import { getClientErrorObject } from 'src/utils/getClientErrorObject';
 import { allowCrossDomain as domainShardingEnabled } from 'src/utils/hostNamesConfig';
 import { updateDataMask } from 'src/dataMask/actions';
 import { waitForAsyncData } from 'src/middleware/asyncEvent';
+import { safeStringify } from 'src/utils/safeStringify';
 
 export const CHART_UPDATE_STARTED = 'CHART_UPDATE_STARTED';
 export function chartUpdateStarted(queryController, latestQueryFormData, key) {
@@ -579,12 +580,18 @@ export function redirectSQLLab(formData, history) {
           datasourceKey: formData.datasource,
           sql: json.result[0].query,
         };
-        history.push({
-          pathname: redirectUrl,
-          state: {
-            requestedQuery: payload,
-          },
-        });
+        if (history) {
+          history.push({
+            pathname: redirectUrl,
+            state: {
+              requestedQuery: payload,
+            },
+          });
+        } else {
+          SupersetClient.postForm(redirectUrl, {
+            form_data: safeStringify(payload),
+          });
+        }
       })
       .catch(() =>
         dispatch(addDangerToast(t('An error occurred while loading the SQL'))),
diff --git a/superset-frontend/src/explore/components/ExploreChartHeader/index.jsx b/superset-frontend/src/explore/components/ExploreChartHeader/index.jsx
index 6e11eaf1c5..d151459e5d 100644
--- a/superset-frontend/src/explore/components/ExploreChartHeader/index.jsx
+++ b/superset-frontend/src/explore/components/ExploreChartHeader/index.jsx
@@ -156,8 +156,8 @@ export const ExploreChartHeader = ({
   const { redirectSQLLab } = actions;
 
   const redirectToSQLLab = useCallback(
-    formData => {
-      redirectSQLLab(formData, history);
+    (formData, openNewWindow = false) => {
+      redirectSQLLab(formData, !openNewWindow && history);
     },
     [redirectSQLLab, history],
   );
diff --git a/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx b/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx
index 707138d506..d72fe5f9e0 100644
--- a/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx
+++ b/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx
@@ -20,7 +20,13 @@
 
 import React from 'react';
 import PropTypes from 'prop-types';
-import { DatasourceType, styled, t, withTheme } from '@superset-ui/core';
+import {
+  DatasourceType,
+  SupersetClient,
+  styled,
+  t,
+  withTheme,
+} from '@superset-ui/core';
 import { getTemporalColumns } from '@superset-ui/chart-controls';
 import { getUrlParam } from 'src/utils/urlUtils';
 import { AntdDropdown } from 'src/components';
@@ -44,6 +50,7 @@ import ModalTrigger from 'src/components/ModalTrigger';
 import ViewQueryModalFooter from 'src/explore/components/controls/ViewQueryModalFooter';
 import ViewQuery from 'src/explore/components/controls/ViewQuery';
 import { SaveDatasetModal } from 'src/SqlLab/components/SaveDatasetModal';
+import { safeStringify } from 'src/utils/safeStringify';
 import { isString } from 'lodash';
 import { Link } from 'react-router-dom';
 
@@ -120,6 +127,7 @@ const Styles = styled.div`
 `;
 
 const CHANGE_DATASET = 'change_dataset';
+const VIEW_IN_SQL_LAB = 'view_in_sql_lab';
 const EDIT_DATASET = 'edit_dataset';
 const QUERY_PREVIEW = 'query_preview';
 const SAVE_AS_DATASET = 'save_as_dataset';
@@ -155,6 +163,14 @@ export const getDatasourceTitle = datasource => {
   return datasource?.name || '';
 };
 
+const preventRouterLinkWhileMetaClicked = evt => {
+  if (evt.metaKey) {
+    evt.preventDefault();
+  } else {
+    evt.stopPropagation();
+  }
+};
+
 class DatasourceControl extends React.PureComponent {
   constructor(props) {
     super(props);
@@ -231,6 +247,19 @@ class DatasourceControl extends React.PureComponent {
         this.toggleEditDatasourceModal();
         break;
 
+      case VIEW_IN_SQL_LAB:
+        {
+          const { datasource } = this.props;
+          const payload = {
+            datasourceKey: `${datasource.id}__${datasource.type}`,
+            sql: datasource.sql,
+          };
+          SupersetClient.postForm('/sqllab/', {
+            form_data: safeStringify(payload),
+          });
+        }
+        break;
+
       case SAVE_AS_DATASET:
         this.toggleSaveDatasetModal();
         break;
@@ -294,12 +323,13 @@ class DatasourceControl extends React.PureComponent {
         )}
         <Menu.Item key={CHANGE_DATASET}>{t('Swap dataset')}</Menu.Item>
         {!isMissingDatasource && canAccessSqlLab && (
-          <Menu.Item>
+          <Menu.Item key={VIEW_IN_SQL_LAB}>
             <Link
               to={{
                 pathname: '/sqllab',
                 state: { requestedQuery },
               }}
+              onClick={preventRouterLinkWhileMetaClicked}
             >
               {t('View in SQL Lab')}
             </Link>
@@ -333,12 +363,13 @@ class DatasourceControl extends React.PureComponent {
           />
         </Menu.Item>
         {canAccessSqlLab && (
-          <Menu.Item>
+          <Menu.Item key={VIEW_IN_SQL_LAB}>
             <Link
               to={{
                 pathname: '/sqllab',
                 state: { requestedQuery },
               }}
+              onClick={preventRouterLinkWhileMetaClicked}
             >
               {t('View in SQL Lab')}
             </Link>
diff --git a/superset-frontend/src/explore/components/controls/ViewQueryModalFooter.tsx b/superset-frontend/src/explore/components/controls/ViewQueryModalFooter.tsx
index fbc87d7f9f..27b0fd371e 100644
--- a/superset-frontend/src/explore/components/controls/ViewQueryModalFooter.tsx
+++ b/superset-frontend/src/explore/components/controls/ViewQueryModalFooter.tsx
@@ -18,7 +18,7 @@
  */
 import React from 'react';
 import { isObject } from 'lodash';
-import { t } from '@superset-ui/core';
+import { t, SupersetClient } from '@superset-ui/core';
 import Button from 'src/components/Button';
 import { useHistory } from 'react-router-dom';
 
@@ -44,24 +44,33 @@ const ViewQueryModalFooter: React.FC<ViewQueryModalFooterProps> = (props: {
   datasource: SimpleDataSource;
 }) => {
   const history = useHistory();
-  const viewInSQLLab = (id: string, type: string, sql: string) => {
+  const viewInSQLLab = (
+    openInNewWindow: boolean,
+    id: string,
+    type: string,
+    sql: string,
+  ) => {
     const payload = {
       datasourceKey: `${id}__${type}`,
       sql,
     };
-    history.push({
-      pathname: '/sqllab',
-      state: {
-        requestedQuery: payload,
-      },
-    });
+    if (openInNewWindow) {
+      SupersetClient.postForm('/sqllab/', payload);
+    } else {
+      history.push({
+        pathname: '/sqllab',
+        state: {
+          requestedQuery: payload,
+        },
+      });
+    }
   };
 
-  const openSQL = () => {
+  const openSQL = (openInNewWindow: boolean) => {
     const { datasource } = props;
     if (isObject(datasource)) {
       const { id, type, sql } = datasource;
-      viewInSQLLab(id, type, sql);
+      viewInSQLLab(openInNewWindow, id, type, sql);
     }
   };
   return (
@@ -74,7 +83,9 @@ const ViewQueryModalFooter: React.FC<ViewQueryModalFooterProps> = (props: {
       >
         {SAVE_AS_DATASET}
       </Button>
-      <Button onClick={() => openSQL()}>{OPEN_IN_SQL_LAB}</Button>
+      <Button onClick={({ metaKey }) => openSQL(Boolean(metaKey))}>
+        {OPEN_IN_SQL_LAB}
+      </Button>
       <Button
         buttonStyle="primary"
         onClick={() => {
diff --git a/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx b/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx
index eca77780b1..db1efd711d 100644
--- a/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx
+++ b/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx
@@ -280,7 +280,7 @@ export const useExploreAdditionalActionsMenu = (
           setIsDropdownVisible(false);
           break;
         case MENU_KEYS.RUN_IN_SQL_LAB:
-          onOpenInEditor(latestQueryFormData);
+          onOpenInEditor(latestQueryFormData, domEvent.metaKey);
           setIsDropdownVisible(false);
           break;
         default:
diff --git a/superset-frontend/src/features/queries/SavedQueryPreviewModal.test.jsx b/superset-frontend/src/features/queries/SavedQueryPreviewModal.test.jsx
index 66e388bc16..2cb1b7fcf4 100644
--- a/superset-frontend/src/features/queries/SavedQueryPreviewModal.test.jsx
+++ b/superset-frontend/src/features/queries/SavedQueryPreviewModal.test.jsx
@@ -129,7 +129,7 @@ describe('SavedQueryPreviewModal', () => {
 
   it('handle open in sql lab', async () => {
     act(() => {
-      wrapper.find('[data-test="open-in-sql-lab"]').first().props().onClick();
+      wrapper.find('[data-test="open-in-sql-lab"]').first().props().onClick({});
     });
     expect(mockedProps.openInSqlLab).toHaveBeenCalled();
     expect(mockedProps.openInSqlLab.mock.calls[0][0]).toEqual(1);
diff --git a/superset-frontend/src/features/queries/SavedQueryPreviewModal.tsx b/superset-frontend/src/features/queries/SavedQueryPreviewModal.tsx
index d46212b938..0db06f91cf 100644
--- a/superset-frontend/src/features/queries/SavedQueryPreviewModal.tsx
+++ b/superset-frontend/src/features/queries/SavedQueryPreviewModal.tsx
@@ -65,7 +65,7 @@ type SavedQueryObject = {
 interface SavedQueryPreviewModalProps extends ToastProps {
   fetchData: (id: number) => {};
   onHide: () => void;
-  openInSqlLab: (id: number) => {};
+  openInSqlLab: (id: number, openInNewWindow: boolean) => {};
   queries: Array<SavedQueryObject>;
   savedQuery: SavedQueryObject;
   show: boolean;
@@ -117,7 +117,9 @@ const SavedQueryPreviewModal: FunctionComponent<SavedQueryPreviewModalProps> =
                 data-test="open-in-sql-lab"
                 key="open-in-sql-lab"
                 buttonStyle="primary"
-                onClick={() => openInSqlLab(savedQuery.id)}
+                onClick={({ metaKey }) =>
+                  openInSqlLab(savedQuery.id, Boolean(metaKey))
+                }
               >
                 {t('Open in SQL Lab')}
               </Button>
diff --git a/superset-frontend/src/pages/SavedQueryList/index.tsx b/superset-frontend/src/pages/SavedQueryList/index.tsx
index 8c1ce2b3dd..f9af490bb4 100644
--- a/superset-frontend/src/pages/SavedQueryList/index.tsx
+++ b/superset-frontend/src/pages/SavedQueryList/index.tsx
@@ -213,8 +213,12 @@ function SavedQueryList({
   menuData.buttons = subMenuButtons;
 
   // Action methods
-  const openInSqlLab = (id: number) => {
-    history.push(`/sqllab?savedQueryId=${id}`);
+  const openInSqlLab = (id: number, openInNewWindow: boolean) => {
+    if (openInNewWindow) {
+      window.open(`/sqllab?savedQueryId=${id}`);
+    } else {
+      history.push(`/sqllab?savedQueryId=${id}`);
+    }
   };
 
   const copyQueryLink = useCallback(
@@ -389,7 +393,8 @@ function SavedQueryList({
           const handlePreview = () => {
             handleSavedQueryPreview(original.id);
           };
-          const handleEdit = () => openInSqlLab(original.id);
+          const handleEdit = ({ metaKey }: React.MouseEvent) =>
+            openInSqlLab(original.id, Boolean(metaKey));
           const handleCopy = () => copyQueryLink(original.id);
           const handleExport = () => handleBulkSavedQueryExport([original]);
           const handleDelete = () => setQueryCurrentlyDeleting(original);
diff --git a/superset/views/base.py b/superset/views/base.py
index c8802ca25c..4015b7a028 100644
--- a/superset/views/base.py
+++ b/superset/views/base.py
@@ -293,10 +293,13 @@ class BaseSupersetView(BaseView):
             mimetype="application/json",
         )
 
-    def render_app_template(self) -> FlaskResponse:
+    def render_app_template(
+        self, extra_bootstrap_data: Optional[dict[str, Any]] = None
+    ) -> FlaskResponse:
         payload = {
             "user": bootstrap_user_data(g.user, include_perms=True),
             "common": common_bootstrap_payload(g.user),
+            **(extra_bootstrap_data or {}),
         }
         return self.render_template(
             "superset/spa.html",
diff --git a/superset/views/sqllab.py b/superset/views/sqllab.py
index 708716511f..b4bfa5194f 100644
--- a/superset/views/sqllab.py
+++ b/superset/views/sqllab.py
@@ -14,6 +14,10 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
+import contextlib
+
+import simplejson as json
+from flask import request
 from flask_appbuilder import permission_name
 from flask_appbuilder.api import expose
 from flask_appbuilder.security.decorators import has_access
@@ -31,12 +35,16 @@ class SqllabView(BaseSupersetView):
 
     method_permission_name = MODEL_API_RW_METHOD_PERMISSION_MAP
 
-    @expose("/")
+    @expose("/", methods=["GET", "POST"])
     @has_access
     @permission_name("read")
     @event_logger.log_this
     def root(self) -> FlaskResponse:
-        return self.render_app_template()
+        payload = {}
+        if form_data := request.form.get("form_data"):
+            with contextlib.suppress(json.JSONDecodeError):
+                payload["requested_query"] = json.loads(form_data)
+        return self.render_app_template(payload)
 
     @expose("/history/", methods=("GET",))
     @has_access