Skip to content

Commit

Permalink
fix(sqllab): Allow opening of SQL Lab in new browser tab (apache#25582)
Browse files Browse the repository at this point in the history
  • Loading branch information
justinpark authored Oct 13, 2023
1 parent e56e0de commit 003001f
Show file tree
Hide file tree
Showing 10 changed files with 99 additions and 32 deletions.
19 changes: 13 additions & 6 deletions superset-frontend/src/components/Chart/chartAction.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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'))),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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],
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';

Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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>
Expand Down Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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 (
Expand All @@ -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={() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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>
Expand Down
11 changes: 8 additions & 3 deletions superset-frontend/src/pages/SavedQueryList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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);
Expand Down
5 changes: 4 additions & 1 deletion superset/views/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
12 changes: 10 additions & 2 deletions superset/views/sqllab.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down

0 comments on commit 003001f

Please sign in to comment.