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