From 003001f19f9857f4ddca88046685077020ea90c4 Mon Sep 17 00:00:00 2001 From: "JUST.in DO IT" Date: Fri, 13 Oct 2023 11:16:50 -0400 Subject: [PATCH] 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 +++++++++++++++++-- .../controls/ViewQueryModalFooter.tsx | 33 +++++++++++------ .../useExploreAdditionalActionsMenu/index.jsx | 2 +- .../queries/SavedQueryPreviewModal.test.jsx | 2 +- .../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 fcf45a4946b20..9e5dc0eddde96 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 6e11eaf1c5ca6..d151459e5d444 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 707138d5067c2..d72fe5f9e07c6 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 { )} {t('Swap dataset')} {!isMissingDatasource && canAccessSqlLab && ( - + {t('View in SQL Lab')} @@ -333,12 +363,13 @@ class DatasourceControl extends React.PureComponent { /> {canAccessSqlLab && ( - + {t('View in SQL Lab')} diff --git a/superset-frontend/src/explore/components/controls/ViewQueryModalFooter.tsx b/superset-frontend/src/explore/components/controls/ViewQueryModalFooter.tsx index fbc87d7f9f62f..27b0fd371e947 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 = (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 = (props: { > {SAVE_AS_DATASET} - + diff --git a/superset-frontend/src/pages/SavedQueryList/index.tsx b/superset-frontend/src/pages/SavedQueryList/index.tsx index 8c1ce2b3ddcec..f9af490bb44b3 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 c8802ca25ccfd..4015b7a028aa6 100644 --- a/superset/views/base.py +++ b/superset/views/base.py @@ -293,10 +293,13 @@ def json_response(obj: Any, status: int = 200) -> FlaskResponse: 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 708716511f9f1..b4bfa5194f20d 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