mirror of
https://github.com/apache/superset.git
synced 2026-04-20 16:44:46 +00:00
fix(sqllab): Allow opening of SQL Lab in new browser tab (#25582)
This commit is contained in:
@@ -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'))),
|
||||
|
||||
@@ -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],
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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={() => {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user