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/04 19:21:47 UTC
[superset] branch master updated: feat(sqllab): SPA migration (#25151)
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 5ab1e7eae4 feat(sqllab): SPA migration (#25151)
5ab1e7eae4 is described below
commit 5ab1e7eae45b789c08c0b99612b4a410bbb986b8
Author: JUST.in DO IT <ju...@airbnb.com>
AuthorDate: Wed Oct 4 12:21:41 2023 -0700
feat(sqllab): SPA migration (#25151)
---
.../e2e/sqllab/_skip.sourcePanel.index.test.js | 2 +-
.../cypress-base/cypress/e2e/sqllab/query.test.ts | 2 +-
.../cypress/e2e/sqllab/sqllab.applitools.test.ts | 2 +-
.../cypress-base/cypress/e2e/sqllab/tabs.test.ts | 2 +-
superset-frontend/package-lock.json | 2 +-
superset-frontend/spec/helpers/reducerIndex.ts | 3 +-
superset-frontend/src/SqlLab/App.jsx | 84 ------------------
.../AceEditorWrapper/useKeywords.test.ts | 2 +-
.../src/SqlLab/components/App/App.test.jsx | 5 +-
.../src/SqlLab/components/App/index.jsx | 12 ++-
.../src/SqlLab/components/QueryTable/index.tsx | 2 +-
.../SqlLab/components/SqlEditor/SqlEditor.test.jsx | 2 +-
.../SqlEditorLeftBar/SqlEditorLeftBar.test.jsx | 2 +-
.../TabbedSqlEditors/TabbedSqlEditors.test.jsx | 14 +--
.../SqlLab/components/TabbedSqlEditors/index.jsx | 5 +-
superset-frontend/src/SqlLab/index.tsx | 23 -----
superset-frontend/src/SqlLab/reducers/common.js | 21 -----
.../src/SqlLab/reducers/localStorageUsage.js | 21 -----
.../src/components/Chart/chartAction.js | 12 +--
.../components/ExploreChartHeader/index.jsx | 13 ++-
.../DatasourceControl/DatasourceControl.test.tsx | 84 ++++++++++++------
.../controls/DatasourceControl/index.jsx | 50 +++++------
.../components/controls/ViewQueryModalFooter.tsx | 11 ++-
.../src/features/databases/DatabaseModal/index.tsx | 14 +--
.../AddDataset/DatasetPanel/DatasetPanel.test.tsx | 19 ++++-
.../AddDataset/DatasetPanel/MessageContent.tsx | 15 ++--
.../datasets/DatasetLayout/DatasetLayout.test.tsx | 15 ++--
.../src/features/home/ActivityTable.tsx | 2 +-
superset-frontend/src/features/home/EmptyState.tsx | 28 +++---
superset-frontend/src/features/home/Menu.test.tsx | 2 +-
.../src/features/home/RightMenu.test.tsx | 2 +-
superset-frontend/src/features/home/RightMenu.tsx | 2 +-
.../src/features/home/SavedQueries.tsx | 25 ++----
superset-frontend/src/features/home/SubMenu.tsx | 2 +-
.../src/features/home/commonMenuData.ts | 2 +-
.../src/hooks/apiResources/queryApi.ts | 9 +-
.../pages/DatasetCreation/DatasetCreation.test.tsx | 2 +-
.../src/pages/QueryHistoryList/index.tsx | 10 +--
.../src/pages/SavedQueryList/index.tsx | 17 ++--
.../index.js => pages/SqlLab/LocationContext.tsx} | 31 ++++---
superset-frontend/src/pages/SqlLab/SqlLab.test.tsx | 99 ++++++++++++++++++++++
superset-frontend/src/pages/SqlLab/index.tsx | 78 +++++++++++++++++
superset-frontend/src/views/CRUD/hooks.ts | 4 +-
superset-frontend/src/views/routes.tsx | 10 ++-
superset-frontend/webpack.config.js | 1 -
superset/initialization/__init__.py | 6 +-
superset/jinja_context.py | 2 +-
superset/models/core.py | 2 +-
superset/models/sql_lab.py | 4 +-
superset/sqllab/api.py | 2 +
superset/views/core.py | 27 ++----
superset/views/sqllab.py | 46 ++++++++++
tests/integration_tests/core_tests.py | 21 ++++-
tests/integration_tests/sqllab_tests.py | 4 +-
54 files changed, 518 insertions(+), 361 deletions(-)
diff --git a/superset-frontend/cypress-base/cypress/e2e/sqllab/_skip.sourcePanel.index.test.js b/superset-frontend/cypress-base/cypress/e2e/sqllab/_skip.sourcePanel.index.test.js
index be455a4a99..ece1581714 100644
--- a/superset-frontend/cypress-base/cypress/e2e/sqllab/_skip.sourcePanel.index.test.js
+++ b/superset-frontend/cypress-base/cypress/e2e/sqllab/_skip.sourcePanel.index.test.js
@@ -20,7 +20,7 @@ import { selectResultsTab } from './sqllab.helper';
describe.skip('SqlLab datasource panel', () => {
beforeEach(() => {
- cy.visit('/superset/sqllab');
+ cy.visit('/sqllab');
});
// TODO the test bellow is flaky, and has been disabled for the time being
diff --git a/superset-frontend/cypress-base/cypress/e2e/sqllab/query.test.ts b/superset-frontend/cypress-base/cypress/e2e/sqllab/query.test.ts
index 0d36692b2a..86502e8655 100644
--- a/superset-frontend/cypress-base/cypress/e2e/sqllab/query.test.ts
+++ b/superset-frontend/cypress-base/cypress/e2e/sqllab/query.test.ts
@@ -25,7 +25,7 @@ function parseClockStr(node: JQuery) {
describe('SqlLab query panel', () => {
beforeEach(() => {
- cy.visit('/superset/sqllab');
+ cy.visit('/sqllab');
});
it.skip('supports entering and running a query', () => {
diff --git a/superset-frontend/cypress-base/cypress/e2e/sqllab/sqllab.applitools.test.ts b/superset-frontend/cypress-base/cypress/e2e/sqllab/sqllab.applitools.test.ts
index fdbaefb158..cc4cf7ac03 100644
--- a/superset-frontend/cypress-base/cypress/e2e/sqllab/sqllab.applitools.test.ts
+++ b/superset-frontend/cypress-base/cypress/e2e/sqllab/sqllab.applitools.test.ts
@@ -19,7 +19,7 @@
describe('SqlLab view', () => {
beforeEach(() => {
- cy.visit('/superset/sqllab');
+ cy.visit('/sqllab');
});
it('should load the SqlLab', () => {
diff --git a/superset-frontend/cypress-base/cypress/e2e/sqllab/tabs.test.ts b/superset-frontend/cypress-base/cypress/e2e/sqllab/tabs.test.ts
index b2c7a180ad..0deeabde8d 100644
--- a/superset-frontend/cypress-base/cypress/e2e/sqllab/tabs.test.ts
+++ b/superset-frontend/cypress-base/cypress/e2e/sqllab/tabs.test.ts
@@ -18,7 +18,7 @@
*/
describe('SqlLab query tabs', () => {
beforeEach(() => {
- cy.visit('/superset/sqllab');
+ cy.visit('/sqllab');
});
const tablistSelector = '[data-test="sql-editor-tabs"] > [role="tablist"]';
diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json
index 694cfd9193..4c0fa255e1 100644
--- a/superset-frontend/package-lock.json
+++ b/superset-frontend/package-lock.json
@@ -77984,7 +77984,7 @@
"@mapbox/geojson-extent": "^1.0.1",
"@math.gl/web-mercator": "^3.2.2",
"@types/d3-array": "^2.0.0",
- "@types/mapbox__geojson-extent": "*",
+ "@types/mapbox__geojson-extent": "^1.0.0",
"@types/underscore": "^1.11.6",
"@types/urijs": "^1.19.19",
"bootstrap-slider": "^10.0.0",
diff --git a/superset-frontend/spec/helpers/reducerIndex.ts b/superset-frontend/spec/helpers/reducerIndex.ts
index a9cadc4f81..95fe4d3f1c 100644
--- a/superset-frontend/spec/helpers/reducerIndex.ts
+++ b/superset-frontend/spec/helpers/reducerIndex.ts
@@ -29,7 +29,6 @@ import messageToasts from 'src/components/MessageToasts/reducers';
import saveModal from 'src/explore/reducers/saveModalReducer';
import explore from 'src/explore/reducers/exploreReducer';
import sqlLab from 'src/SqlLab/reducers/sqlLab';
-import localStorageUsageInKilobytes from 'src/SqlLab/reducers/localStorageUsage';
import reports from 'src/features/reports/ReportModal/reducer';
import getBootstrapData from 'src/utils/getBootstrapData';
@@ -59,7 +58,7 @@ export default {
saveModal,
explore,
sqlLab,
- localStorageUsageInKilobytes,
+ localStorageUsageInKilobytes: noopReducer(0),
reports,
common: noopReducer(common),
user: noopReducer(user),
diff --git a/superset-frontend/src/SqlLab/App.jsx b/superset-frontend/src/SqlLab/App.jsx
deleted file mode 100644
index ae8b81f4a8..0000000000
--- a/superset-frontend/src/SqlLab/App.jsx
+++ /dev/null
@@ -1,84 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import React from 'react';
-import { Provider } from 'react-redux';
-import { hot } from 'react-hot-loader/root';
-import {
- FeatureFlag,
- ThemeProvider,
- initFeatureFlags,
- isFeatureEnabled,
-} from '@superset-ui/core';
-import { GlobalStyles } from 'src/GlobalStyles';
-import { setupStore, userReducer } from 'src/views/store';
-import setupExtensions from 'src/setup/setupExtensions';
-import getBootstrapData from 'src/utils/getBootstrapData';
-import { persistSqlLabStateEnhancer } from 'src/SqlLab/middlewares/persistSqlLabStateEnhancer';
-import getInitialState from './reducers/getInitialState';
-import { reducers } from './reducers/index';
-import App from './components/App';
-import { rehydratePersistedState } from './utils/reduxStateToLocalStorageHelper';
-import setupApp from '../setup/setupApp';
-
-import '../assets/stylesheets/reactable-pagination.less';
-import { theme } from '../preamble';
-import { SqlLabGlobalStyles } from './SqlLabGlobalStyles';
-
-setupApp();
-setupExtensions();
-
-const bootstrapData = getBootstrapData();
-
-initFeatureFlags(bootstrapData.common.feature_flags);
-
-const initialState = getInitialState(bootstrapData);
-
-export const store = setupStore({
- initialState,
- rootReducers: { ...reducers, user: userReducer },
- ...(!isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE) && {
- enhancers: [persistSqlLabStateEnhancer],
- }),
-});
-
-rehydratePersistedState(store.dispatch, initialState);
-
-// Highlight the navbar menu
-const menus = document.querySelectorAll('.nav.navbar-nav li.dropdown');
-const sqlLabMenu = Array.prototype.slice
- .apply(menus)
- .find(element => element.innerText.trim() === 'SQL Lab');
-if (sqlLabMenu) {
- const classes = sqlLabMenu.getAttribute('class');
- if (classes.indexOf('active') === -1) {
- sqlLabMenu.setAttribute('class', `${classes} active`);
- }
-}
-
-const Application = () => (
- <Provider store={store}>
- <ThemeProvider theme={theme}>
- <GlobalStyles />
- <SqlLabGlobalStyles />
- <App />
- </ThemeProvider>
- </Provider>
-);
-
-export default hot(Application);
diff --git a/superset-frontend/src/SqlLab/components/AceEditorWrapper/useKeywords.test.ts b/superset-frontend/src/SqlLab/components/AceEditorWrapper/useKeywords.test.ts
index 12bd95b402..7aa306d8bc 100644
--- a/superset-frontend/src/SqlLab/components/AceEditorWrapper/useKeywords.test.ts
+++ b/superset-frontend/src/SqlLab/components/AceEditorWrapper/useKeywords.test.ts
@@ -28,7 +28,7 @@ import { schemaApiUtil } from 'src/hooks/apiResources/schemas';
import { tableApiUtil } from 'src/hooks/apiResources/tables';
import { addTable } from 'src/SqlLab/actions/sqlLab';
import { initialState } from 'src/SqlLab/fixtures';
-import { reducers } from 'src/SqlLab/reducers';
+import reducers from 'spec/helpers/reducerIndex';
import {
SCHEMA_AUTOCOMPLETE_SCORE,
TABLE_AUTOCOMPLETE_SCORE,
diff --git a/superset-frontend/src/SqlLab/components/App/App.test.jsx b/superset-frontend/src/SqlLab/components/App/App.test.jsx
index d56ea4780e..d3db1d5fb8 100644
--- a/superset-frontend/src/SqlLab/components/App/App.test.jsx
+++ b/superset-frontend/src/SqlLab/components/App/App.test.jsx
@@ -17,12 +17,13 @@
* under the License.
*/
import React from 'react';
+import { combineReducers } from 'redux';
import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { render } from 'spec/helpers/testing-library';
import App from 'src/SqlLab/components/App';
-import sqlLabReducer from 'src/SqlLab/reducers/index';
+import reducers from 'spec/helpers/reducerIndex';
import { LOCALSTORAGE_MAX_USAGE_KB } from 'src/SqlLab/constants';
import { LOG_EVENT } from 'src/logger/actions';
import {
@@ -37,6 +38,8 @@ jest.mock('src/SqlLab/components/QueryAutoRefresh', () => () => (
<div data-test="mock-query-auto-refresh" />
));
+const sqlLabReducer = combineReducers(reducers);
+
describe('SqlLab App', () => {
const middlewares = [thunk];
const mockStore = configureStore(middlewares);
diff --git a/superset-frontend/src/SqlLab/components/App/index.jsx b/superset-frontend/src/SqlLab/components/App/index.jsx
index ff47e6173b..aab4e78d4f 100644
--- a/superset-frontend/src/SqlLab/components/App/index.jsx
+++ b/superset-frontend/src/SqlLab/components/App/index.jsx
@@ -20,9 +20,9 @@ import React from 'react';
import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
+import { Redirect } from 'react-router-dom';
import { css, styled, t } from '@superset-ui/core';
import throttle from 'lodash/throttle';
-import ToastContainer from 'src/components/MessageToasts/ToastContainer';
import {
LOCALSTORAGE_MAX_USAGE_KB,
LOCALSTORAGE_WARNING_THRESHOLD,
@@ -186,7 +186,14 @@ class App extends React.PureComponent {
render() {
const { queries, queriesLastUpdate } = this.props;
if (this.state.hash && this.state.hash === '#search') {
- return window.location.replace('/superset/sqllab/history/');
+ return (
+ <Redirect
+ to={{
+ pathname: '/sqllab/history/',
+ replace: true,
+ }}
+ />
+ );
}
return (
<SqlLabStyles data-test="SqlLabApp" className="App SqlLab">
@@ -195,7 +202,6 @@ class App extends React.PureComponent {
queriesLastUpdate={queriesLastUpdate}
/>
<TabbedSqlEditors />
- <ToastContainer />
</SqlLabStyles>
);
}
diff --git a/superset-frontend/src/SqlLab/components/QueryTable/index.tsx b/superset-frontend/src/SqlLab/components/QueryTable/index.tsx
index b5eaeb01e6..6ddae08e68 100644
--- a/superset-frontend/src/SqlLab/components/QueryTable/index.tsx
+++ b/superset-frontend/src/SqlLab/components/QueryTable/index.tsx
@@ -61,7 +61,7 @@ interface QueryTableProps {
}
const openQuery = (id: number) => {
- const url = `/superset/sqllab?queryId=${id}`;
+ const url = `/sqllab?queryId=${id}`;
window.open(url);
};
diff --git a/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.jsx b/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.jsx
index 23424ff264..ff335e14ea 100644
--- a/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.jsx
+++ b/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.jsx
@@ -20,7 +20,7 @@ import React from 'react';
import { act } from 'react-dom/test-utils';
import { fireEvent, render, waitFor } from 'spec/helpers/testing-library';
import fetchMock from 'fetch-mock';
-import { reducers } from 'src/SqlLab/reducers';
+import reducers from 'spec/helpers/reducerIndex';
import SqlEditor from 'src/SqlLab/components/SqlEditor';
import { setupStore } from 'src/views/store';
import {
diff --git a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/SqlEditorLeftBar.test.jsx b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/SqlEditorLeftBar.test.jsx
index d12938a235..6665091572 100644
--- a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/SqlEditorLeftBar.test.jsx
+++ b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/SqlEditorLeftBar.test.jsx
@@ -26,7 +26,7 @@ import SqlEditorLeftBar from 'src/SqlLab/components/SqlEditorLeftBar';
import { table, initialState, defaultQueryEditor } from 'src/SqlLab/fixtures';
import { api } from 'src/hooks/apiResources/queryApi';
import { setupStore } from 'src/views/store';
-import { reducers } from 'src/SqlLab/reducers';
+import reducers from 'spec/helpers/reducerIndex';
const mockedProps = {
tables: [table],
diff --git a/superset-frontend/src/SqlLab/components/TabbedSqlEditors/TabbedSqlEditors.test.jsx b/superset-frontend/src/SqlLab/components/TabbedSqlEditors/TabbedSqlEditors.test.jsx
index 90d1de2528..5d782590a1 100644
--- a/superset-frontend/src/SqlLab/components/TabbedSqlEditors/TabbedSqlEditors.test.jsx
+++ b/superset-frontend/src/SqlLab/components/TabbedSqlEditors/TabbedSqlEditors.test.jsx
@@ -110,23 +110,17 @@ describe('TabbedSqlEditors', () => {
it('should handle id', async () => {
uriStub.returns({ id: 1 });
await mountWithAct();
- expect(window.history.replaceState.getCall(0).args[2]).toBe(
- '/superset/sqllab',
- );
+ expect(window.history.replaceState.getCall(0).args[2]).toBe('/sqllab');
});
it('should handle savedQueryId', async () => {
uriStub.returns({ savedQueryId: 1 });
await mountWithAct();
- expect(window.history.replaceState.getCall(0).args[2]).toBe(
- '/superset/sqllab',
- );
+ expect(window.history.replaceState.getCall(0).args[2]).toBe('/sqllab');
});
it('should handle sql', async () => {
uriStub.returns({ sql: 1, dbid: 1 });
await mountWithAct();
- expect(window.history.replaceState.getCall(0).args[2]).toBe(
- '/superset/sqllab',
- );
+ expect(window.history.replaceState.getCall(0).args[2]).toBe('/sqllab');
});
it('should handle custom url params', async () => {
uriStub.returns({
@@ -137,7 +131,7 @@ describe('TabbedSqlEditors', () => {
});
await mountWithAct();
expect(window.history.replaceState.getCall(0).args[2]).toBe(
- '/superset/sqllab?custom_value=str&extra_attr1=true',
+ '/sqllab?custom_value=str&extra_attr1=true',
);
});
});
diff --git a/superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.jsx b/superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.jsx
index 95d0c2529b..166cce18f9 100644
--- a/superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.jsx
+++ b/superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.jsx
@@ -29,6 +29,7 @@ import { detectOS } from 'src/utils/common';
import * as Actions from 'src/SqlLab/actions/sqlLab';
import { EmptyStateBig } from 'src/components/EmptyState';
import getBootstrapData from 'src/utils/getBootstrapData';
+import { locationContext } from 'src/pages/SqlLab/LocationContext';
import SqlEditor from '../SqlEditor';
import SqlEditorTabHeader from '../SqlEditorTabHeader';
@@ -75,7 +76,7 @@ const userOS = detectOS();
class TabbedSqlEditors extends React.PureComponent {
constructor(props) {
super(props);
- const sqlLabUrl = '/superset/sqllab';
+ const sqlLabUrl = '/sqllab';
this.state = {
sqlLabUrl,
};
@@ -132,6 +133,7 @@ class TabbedSqlEditors extends React.PureComponent {
new: isNewQuery,
...urlParams
} = {
+ ...this.context.requestedQuery,
...bootstrapData.requested_query,
...queryParameters,
};
@@ -332,6 +334,7 @@ class TabbedSqlEditors extends React.PureComponent {
}
TabbedSqlEditors.propTypes = propTypes;
TabbedSqlEditors.defaultProps = defaultProps;
+TabbedSqlEditors.contextType = locationContext;
function mapStateToProps({ sqlLab, common }) {
return {
diff --git a/superset-frontend/src/SqlLab/index.tsx b/superset-frontend/src/SqlLab/index.tsx
deleted file mode 100644
index c257009e64..0000000000
--- a/superset-frontend/src/SqlLab/index.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import React from 'react';
-import ReactDOM from 'react-dom';
-import App from './App';
-
-ReactDOM.render(<App />, document.getElementById('app'));
diff --git a/superset-frontend/src/SqlLab/reducers/common.js b/superset-frontend/src/SqlLab/reducers/common.js
deleted file mode 100644
index 05a7968a88..0000000000
--- a/superset-frontend/src/SqlLab/reducers/common.js
+++ /dev/null
@@ -1,21 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-export default function commonReducer(state = {}) {
- return state;
-}
diff --git a/superset-frontend/src/SqlLab/reducers/localStorageUsage.js b/superset-frontend/src/SqlLab/reducers/localStorageUsage.js
deleted file mode 100644
index eafbb07816..0000000000
--- a/superset-frontend/src/SqlLab/reducers/localStorageUsage.js
+++ /dev/null
@@ -1,21 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-export default function localStorageUsageReducer(state = 0) {
- return state;
-}
diff --git a/superset-frontend/src/components/Chart/chartAction.js b/superset-frontend/src/components/Chart/chartAction.js
index d1dcfd3a00..fcf45a4946 100644
--- a/superset-frontend/src/components/Chart/chartAction.js
+++ b/superset-frontend/src/components/Chart/chartAction.js
@@ -39,7 +39,6 @@ import { addDangerToast } from 'src/components/MessageToasts/actions';
import { logEvent } from 'src/logger/actions';
import { Logger, LOG_ACTIONS_LOAD_CHART } from 'src/logger/LogUtils';
import { getClientErrorObject } from 'src/utils/getClientErrorObject';
-import { safeStringify } from 'src/utils/safeStringify';
import { allowCrossDomain as domainShardingEnabled } from 'src/utils/hostNamesConfig';
import { updateDataMask } from 'src/dataMask/actions';
import { waitForAsyncData } from 'src/middleware/asyncEvent';
@@ -571,17 +570,20 @@ export function postChartFormData(
);
}
-export function redirectSQLLab(formData) {
+export function redirectSQLLab(formData, history) {
return dispatch => {
getChartDataRequest({ formData, resultFormat: 'json', resultType: 'query' })
.then(({ json }) => {
- const redirectUrl = '/superset/sqllab/';
+ const redirectUrl = '/sqllab/';
const payload = {
datasourceKey: formData.datasource,
sql: json.result[0].query,
};
- SupersetClient.postForm(redirectUrl, {
- form_data: safeStringify(payload),
+ history.push({
+ pathname: redirectUrl,
+ state: {
+ requestedQuery: payload,
+ },
});
})
.catch(() =>
diff --git a/superset-frontend/src/explore/components/ExploreChartHeader/index.jsx b/superset-frontend/src/explore/components/ExploreChartHeader/index.jsx
index 958aa16a31..6e11eaf1c5 100644
--- a/superset-frontend/src/explore/components/ExploreChartHeader/index.jsx
+++ b/superset-frontend/src/explore/components/ExploreChartHeader/index.jsx
@@ -17,6 +17,7 @@
* under the License.
*/
import React, { useCallback, useEffect, useMemo, useState } from 'react';
+import { useHistory } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import PropTypes from 'prop-types';
import { Tooltip } from 'src/components/Tooltip';
@@ -151,12 +152,22 @@ export const ExploreChartHeader = ({
[dispatch],
);
+ const history = useHistory();
+ const { redirectSQLLab } = actions;
+
+ const redirectToSQLLab = useCallback(
+ formData => {
+ redirectSQLLab(formData, history);
+ },
+ [redirectSQLLab, history],
+ );
+
const [menu, isDropdownVisible, setIsDropdownVisible] =
useExploreAdditionalActionsMenu(
latestQueryFormData,
canDownload,
slice,
- actions.redirectSQLLab,
+ redirectToSQLLab,
openPropertiesModal,
ownState,
metadata?.dashboards,
diff --git a/superset-frontend/src/explore/components/controls/DatasourceControl/DatasourceControl.test.tsx b/superset-frontend/src/explore/components/controls/DatasourceControl/DatasourceControl.test.tsx
index 6def65d7d2..4531719246 100644
--- a/superset-frontend/src/explore/components/controls/DatasourceControl/DatasourceControl.test.tsx
+++ b/superset-frontend/src/explore/components/controls/DatasourceControl/DatasourceControl.test.tsx
@@ -18,6 +18,7 @@
*/
import React from 'react';
+import { Route } from 'react-router-dom';
import fetchMock from 'fetch-mock';
import userEvent from '@testing-library/user-event';
import { DatasourceType, JsonObject, SupersetClient } from '@superset-ui/core';
@@ -27,6 +28,17 @@ import DatasourceControl from '.';
const SupersetClientGet = jest.spyOn(SupersetClient, 'get');
+const mockDatasource = {
+ id: 25,
+ database: {
+ name: 'examples',
+ },
+ name: 'channels',
+ type: 'table',
+ columns: [],
+ owners: [{ first_name: 'john', last_name: 'doe', id: 1, username: 'jd' }],
+ sql: 'SELECT * FROM mock_datasource_sql',
+};
const createProps = (overrides: JsonObject = {}) => ({
hovered: false,
type: 'DatasourceControl',
@@ -35,16 +47,7 @@ const createProps = (overrides: JsonObject = {}) => ({
description: null,
value: '25__table',
form_data: {},
- datasource: {
- id: 25,
- database: {
- name: 'examples',
- },
- name: 'channels',
- type: 'table',
- columns: [],
- owners: [{ first_name: 'john', last_name: 'doe', id: 1, username: 'jd' }],
- },
+ datasource: mockDatasource,
validationErrors: [],
name: 'datasource',
actions: {
@@ -91,20 +94,20 @@ async function openAndSaveChanges(datasource: any) {
test('Should render', async () => {
const props = createProps();
- render(<DatasourceControl {...props} />);
+ render(<DatasourceControl {...props} />, { useRouter: true });
expect(await screen.findByTestId('datasource-control')).toBeVisible();
});
test('Should have elements', async () => {
const props = createProps();
- render(<DatasourceControl {...props} />);
+ render(<DatasourceControl {...props} />, { useRouter: true });
expect(await screen.findByText('channels')).toBeVisible();
expect(screen.getByTestId('datasource-menu-trigger')).toBeVisible();
});
test('Should open a menu', async () => {
const props = createProps();
- render(<DatasourceControl {...props} />);
+ render(<DatasourceControl {...props} />, { useRouter: true });
expect(screen.queryByText('Edit dataset')).not.toBeInTheDocument();
expect(screen.queryByText('Swap dataset')).not.toBeInTheDocument();
@@ -131,7 +134,7 @@ test('Should not show SQL Lab for non sql_lab role', async () => {
username: 'gamma',
},
});
- render(<DatasourceControl {...props} />);
+ render(<DatasourceControl {...props} />, { useRouter: true });
userEvent.click(screen.getByTestId('datasource-menu-trigger'));
@@ -154,7 +157,7 @@ test('Should show SQL Lab for sql_lab role', async () => {
username: 'sql',
},
});
- render(<DatasourceControl {...props} />);
+ render(<DatasourceControl {...props} />, { useRouter: true });
userEvent.click(screen.getByTestId('datasource-menu-trigger'));
@@ -178,6 +181,7 @@ test('Click on Swap dataset option', async () => {
render(<DatasourceControl {...props} />, {
useRedux: true,
+ useRouter: true,
});
userEvent.click(screen.getByTestId('datasource-menu-trigger'));
@@ -198,6 +202,7 @@ test('Click on Edit dataset', async () => {
);
render(<DatasourceControl {...props} />, {
useRedux: true,
+ useRouter: true,
});
userEvent.click(screen.getByTestId('datasource-menu-trigger'));
@@ -223,6 +228,7 @@ test('Edit dataset should be disabled when user is not admin', async () => {
render(<DatasourceControl {...props} />, {
useRedux: true,
+ useRouter: true,
});
userEvent.click(screen.getByTestId('datasource-menu-trigger'));
@@ -235,21 +241,41 @@ test('Edit dataset should be disabled when user is not admin', async () => {
test('Click on View in SQL Lab', async () => {
const props = createProps();
- const postFormSpy = jest.spyOn(SupersetClient, 'postForm');
- postFormSpy.mockImplementation(jest.fn());
- render(<DatasourceControl {...props} />, {
- useRedux: true,
- });
+ const { queryByTestId, getByTestId } = render(
+ <>
+ <Route
+ path="/sqllab"
+ render={({ location }) => (
+ <div data-test="mock-sqllab-route">
+ {JSON.stringify(location.state)}
+ </div>
+ )}
+ />
+ <DatasourceControl {...props} />
+ </>,
+ {
+ useRedux: true,
+ useRouter: true,
+ },
+ );
userEvent.click(screen.getByTestId('datasource-menu-trigger'));
- expect(postFormSpy).toBeCalledTimes(0);
+ expect(queryByTestId('mock-sqllab-route')).not.toBeInTheDocument();
await act(async () => {
userEvent.click(screen.getByText('View in SQL Lab'));
});
- expect(postFormSpy).toBeCalledTimes(1);
+ expect(getByTestId('mock-sqllab-route')).toBeInTheDocument();
+ expect(JSON.parse(`${getByTestId('mock-sqllab-route').textContent}`)).toEqual(
+ {
+ requestedQuery: {
+ datasourceKey: `${mockDatasource.id}__${mockDatasource.type}`,
+ sql: mockDatasource.sql,
+ },
+ },
+ );
});
test('Should open a different menu when datasource=query', async () => {
@@ -261,7 +287,7 @@ test('Should open a different menu when datasource=query', async () => {
type: DatasourceType.Query,
},
};
- render(<DatasourceControl {...queryProps} />);
+ render(<DatasourceControl {...queryProps} />, { useRouter: true });
expect(screen.queryByText('Query preview')).not.toBeInTheDocument();
expect(screen.queryByText('View in SQL Lab')).not.toBeInTheDocument();
@@ -284,7 +310,10 @@ test('Click on Save as dataset', async () => {
},
};
- render(<DatasourceControl {...queryProps} />, { useRedux: true });
+ render(<DatasourceControl {...queryProps} />, {
+ useRedux: true,
+ useRouter: true,
+ });
userEvent.click(screen.getByTestId('datasource-menu-trigger'));
userEvent.click(screen.getByText('Save as dataset'));
@@ -327,6 +356,7 @@ test('should set the default temporal column', async () => {
};
render(<DatasourceControl {...props} {...overrideProps} />, {
useRedux: true,
+ useRouter: true,
});
await openAndSaveChanges(overrideProps.datasource);
@@ -362,6 +392,7 @@ test('should set the first available temporal column', async () => {
};
render(<DatasourceControl {...props} {...overrideProps} />, {
useRedux: true,
+ useRouter: true,
});
await openAndSaveChanges(overrideProps.datasource);
@@ -397,6 +428,7 @@ test('should not set the temporal column', async () => {
};
render(<DatasourceControl {...props} {...overrideProps} />, {
useRedux: true,
+ useRouter: true,
});
await openAndSaveChanges(overrideProps.datasource);
@@ -410,7 +442,7 @@ test('should not set the temporal column', async () => {
test('should show missing params state', () => {
const props = createProps({ datasource: fallbackExploreInitialData.dataset });
- render(<DatasourceControl {...props} />, { useRedux: true });
+ render(<DatasourceControl {...props} />, { useRedux: true, useRouter: true });
expect(screen.getByText(/missing dataset/i)).toBeVisible();
expect(screen.getByText(/missing url parameters/i)).toBeVisible();
expect(
@@ -426,7 +458,7 @@ test('should show missing dataset state', () => {
// @ts-ignore
window.location = { search: '?slice_id=152' };
const props = createProps({ datasource: fallbackExploreInitialData.dataset });
- render(<DatasourceControl {...props} />, { useRedux: true });
+ render(<DatasourceControl {...props} />, { useRedux: true, useRouter: true });
expect(screen.getAllByText(/missing dataset/i)).toHaveLength(2);
expect(
screen.getByText(
diff --git a/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx b/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx
index bf85716206..707138d506 100644
--- a/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx
+++ b/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx
@@ -20,13 +20,7 @@
import React from 'react';
import PropTypes from 'prop-types';
-import {
- DatasourceType,
- SupersetClient,
- styled,
- t,
- withTheme,
-} from '@superset-ui/core';
+import { DatasourceType, 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';
@@ -50,8 +44,8 @@ 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';
const propTypes = {
actions: PropTypes.object.isRequired,
@@ -126,7 +120,6 @@ 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';
@@ -238,19 +231,6 @@ 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('/superset/sqllab/', {
- form_data: safeStringify(payload),
- });
- }
- break;
-
case SAVE_AS_DATASET:
this.toggleSaveDatasetModal();
break;
@@ -286,6 +266,10 @@ class DatasourceControl extends React.PureComponent {
const canAccessSqlLab = userHasPermission(user, 'SQL Lab', 'menu_access');
const editText = t('Edit dataset');
+ const requestedQuery = {
+ datasourceKey: `${datasource.id}__${datasource.type}`,
+ sql: datasource.sql,
+ };
const defaultDatasourceMenu = (
<Menu onClick={this.handleMenuItemClick}>
@@ -310,7 +294,16 @@ class DatasourceControl extends React.PureComponent {
)}
<Menu.Item key={CHANGE_DATASET}>{t('Swap dataset')}</Menu.Item>
{!isMissingDatasource && canAccessSqlLab && (
- <Menu.Item key={VIEW_IN_SQL_LAB}>{t('View in SQL Lab')}</Menu.Item>
+ <Menu.Item>
+ <Link
+ to={{
+ pathname: '/sqllab',
+ state: { requestedQuery },
+ }}
+ >
+ {t('View in SQL Lab')}
+ </Link>
+ </Menu.Item>
)}
</Menu>
);
@@ -340,7 +333,16 @@ class DatasourceControl extends React.PureComponent {
/>
</Menu.Item>
{canAccessSqlLab && (
- <Menu.Item key={VIEW_IN_SQL_LAB}>{t('View in SQL Lab')}</Menu.Item>
+ <Menu.Item>
+ <Link
+ to={{
+ pathname: '/sqllab',
+ state: { requestedQuery },
+ }}
+ >
+ {t('View in SQL Lab')}
+ </Link>
+ </Menu.Item>
)}
<Menu.Item key={SAVE_AS_DATASET}>{t('Save as dataset')}</Menu.Item>
</Menu>
diff --git a/superset-frontend/src/explore/components/controls/ViewQueryModalFooter.tsx b/superset-frontend/src/explore/components/controls/ViewQueryModalFooter.tsx
index 4f4af039b1..fbc87d7f9f 100644
--- a/superset-frontend/src/explore/components/controls/ViewQueryModalFooter.tsx
+++ b/superset-frontend/src/explore/components/controls/ViewQueryModalFooter.tsx
@@ -18,8 +18,9 @@
*/
import React from 'react';
import { isObject } from 'lodash';
-import { t, SupersetClient } from '@superset-ui/core';
+import { t } from '@superset-ui/core';
import Button from 'src/components/Button';
+import { useHistory } from 'react-router-dom';
interface SimpleDataSource {
id: string;
@@ -42,12 +43,18 @@ const ViewQueryModalFooter: React.FC<ViewQueryModalFooterProps> = (props: {
changeDatasource: () => void;
datasource: SimpleDataSource;
}) => {
+ const history = useHistory();
const viewInSQLLab = (id: string, type: string, sql: string) => {
const payload = {
datasourceKey: `${id}__${type}`,
sql,
};
- SupersetClient.postForm('/superset/sqllab/', payload);
+ history.push({
+ pathname: '/sqllab',
+ state: {
+ requestedQuery: payload,
+ },
+ });
};
const openSQL = () => {
diff --git a/superset-frontend/src/features/databases/DatabaseModal/index.tsx b/superset-frontend/src/features/databases/DatabaseModal/index.tsx
index 555b21be79..0c1ac56369 100644
--- a/superset-frontend/src/features/databases/DatabaseModal/index.tsx
+++ b/superset-frontend/src/features/databases/DatabaseModal/index.tsx
@@ -32,6 +32,7 @@ import React, {
useReducer,
Reducer,
} from 'react';
+import { useHistory } from 'react-router-dom';
import { setItem, LocalStorageKeys } from 'src/utils/localStorageHelpers';
import { UploadChangeParam, UploadFile } from 'antd/lib/upload/interface';
import Tabs from 'src/components/Tabs';
@@ -141,7 +142,6 @@ interface DatabaseModalProps {
show: boolean;
databaseId: number | undefined; // If included, will go into edit mode
dbEngine: string | undefined; // if included goto step 2 with engine already set
- history?: any;
}
export enum ActionType {
@@ -526,7 +526,6 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
show,
databaseId,
dbEngine,
- history,
}) => {
const [db, setDB] = useReducer<
Reducer<Partial<DatabaseObject> | null, DBReducerActionType>
@@ -627,6 +626,7 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
(DB: DatabaseObject) => DB.backend === engine || DB.engine === engine,
)?.parameters !== undefined;
const showDBError = validationErrors || dbErrors;
+ const history = useHistory();
const dbModel: DatabaseForm =
availableDbs?.databases?.find(
@@ -700,13 +700,7 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
};
const redirectURL = (url: string) => {
- /* TODO (lyndsiWilliams): This check and passing history
- as a prop can be removed once SQL Lab is in the SPA */
- if (!isEmpty(history)) {
- history?.push(url);
- } else {
- window.location.href = url;
- }
+ history.push(url);
};
// Database import logic
@@ -1583,7 +1577,7 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
onClick={() => {
setLoading(true);
fetchAndSetDB();
- redirectURL(`/superset/sqllab/?db=true`);
+ redirectURL(`/sqllab?db=true`);
}}
>
{t('QUERY DATA IN SQL LAB')}
diff --git a/superset-frontend/src/features/datasets/AddDataset/DatasetPanel/DatasetPanel.test.tsx b/superset-frontend/src/features/datasets/AddDataset/DatasetPanel/DatasetPanel.test.tsx
index 19262c91bc..62fdc0dfd0 100644
--- a/superset-frontend/src/features/datasets/AddDataset/DatasetPanel/DatasetPanel.test.tsx
+++ b/superset-frontend/src/features/datasets/AddDataset/DatasetPanel/DatasetPanel.test.tsx
@@ -45,7 +45,9 @@ jest.mock(
describe('DatasetPanel', () => {
test('renders a blank state DatasetPanel', () => {
- render(<DatasetPanel hasError={false} columnList={[]} loading={false} />);
+ render(<DatasetPanel hasError={false} columnList={[]} loading={false} />, {
+ useRouter: true,
+ });
const blankDatasetImg = screen.getByRole('img', { name: /empty/i });
expect(blankDatasetImg).toBeVisible();
@@ -73,6 +75,9 @@ describe('DatasetPanel', () => {
columnList={[]}
loading={false}
/>,
+ {
+ useRouter: true,
+ },
);
const blankDatasetImg = screen.getByRole('img', { name: /empty/i });
@@ -91,6 +96,9 @@ describe('DatasetPanel', () => {
columnList={[]}
loading
/>,
+ {
+ useRouter: true,
+ },
);
const blankDatasetImg = screen.getByAltText(ALT_LOADING);
@@ -107,6 +115,9 @@ describe('DatasetPanel', () => {
columnList={[]}
loading={false}
/>,
+ {
+ useRouter: true,
+ },
);
const errorTitle = screen.getByText(ERROR_TITLE);
@@ -124,6 +135,9 @@ describe('DatasetPanel', () => {
columnList={exampleColumns}
loading={false}
/>,
+ {
+ useRouter: true,
+ },
);
expect(await screen.findByText(tableName)).toBeVisible();
expect(screen.getByText(COLUMN_TITLE)).toBeVisible();
@@ -148,6 +162,9 @@ describe('DatasetPanel', () => {
loading={false}
datasets={exampleDataset}
/>,
+ {
+ useRouter: true,
+ },
);
// This is text in the info banner
diff --git a/superset-frontend/src/features/datasets/AddDataset/DatasetPanel/MessageContent.tsx b/superset-frontend/src/features/datasets/AddDataset/DatasetPanel/MessageContent.tsx
index 5d0ef5eda7..6824e1c501 100644
--- a/superset-frontend/src/features/datasets/AddDataset/DatasetPanel/MessageContent.tsx
+++ b/superset-frontend/src/features/datasets/AddDataset/DatasetPanel/MessageContent.tsx
@@ -20,6 +20,7 @@
import React from 'react';
import { t, styled } from '@superset-ui/core';
import { EmptyStateBig } from 'src/components/EmptyState';
+import { Link } from 'react-router-dom';
const StyledContainer = styled.div`
padding: ${({ theme }) => theme.gridUnit * 8}px
@@ -50,15 +51,11 @@ export const VIEW_DATASET_MESSAGE = t(
const renderEmptyDescription = () => (
<>
{SELECT_MESSAGE}
- <span
- role="button"
- onClick={() => {
- window.location.href = `/superset/sqllab`;
- }}
- tabIndex={0}
- >
- {CREATE_MESSAGE}
- </span>
+ <Link to="/sqllab">
+ <span role="button" tabIndex={0}>
+ {CREATE_MESSAGE}
+ </span>
+ </Link>
{VIEW_DATASET_MESSAGE}
</>
);
diff --git a/superset-frontend/src/features/datasets/DatasetLayout/DatasetLayout.test.tsx b/superset-frontend/src/features/datasets/DatasetLayout/DatasetLayout.test.tsx
index 66cbf6f0c4..36278ed3dd 100644
--- a/superset-frontend/src/features/datasets/DatasetLayout/DatasetLayout.test.tsx
+++ b/superset-frontend/src/features/datasets/DatasetLayout/DatasetLayout.test.tsx
@@ -35,7 +35,7 @@ jest.mock('react-router-dom', () => ({
describe('DatasetLayout', () => {
it('renders nothing when no components are passed in', () => {
- render(<DatasetLayout />);
+ render(<DatasetLayout />, { useRouter: true });
const layoutWrapper = screen.getByTestId('dataset-layout-wrapper');
expect(layoutWrapper).toHaveTextContent('');
@@ -55,7 +55,7 @@ describe('DatasetLayout', () => {
it('renders a LeftPanel when passed in', async () => {
render(
<DatasetLayout leftPanel={<LeftPanel setDataset={() => null} />} />,
- { useRedux: true },
+ { useRedux: true, useRouter: true },
);
expect(
@@ -65,7 +65,9 @@ describe('DatasetLayout', () => {
});
it('renders a DatasetPanel when passed in', () => {
- render(<DatasetLayout datasetPanel={<DatasetPanel />} />);
+ render(<DatasetLayout datasetPanel={<DatasetPanel />} />, {
+ useRouter: true,
+ });
const blankDatasetImg = screen.getByRole('img', { name: /empty/i });
const blankDatasetTitle = screen.getByText(/select dataset source/i);
@@ -75,13 +77,16 @@ describe('DatasetLayout', () => {
});
it('renders a RightPanel when passed in', () => {
- render(<DatasetLayout rightPanel={RightPanel()} />);
+ render(<DatasetLayout rightPanel={RightPanel()} />, { useRouter: true });
expect(screen.getByText(/right panel/i)).toBeVisible();
});
it('renders a Footer when passed in', () => {
- render(<DatasetLayout footer={<Footer url="" />} />, { useRedux: true });
+ render(<DatasetLayout footer={<Footer url="" />} />, {
+ useRedux: true,
+ useRouter: true,
+ });
expect(screen.getByText(/Cancel/i)).toBeVisible();
});
diff --git a/superset-frontend/src/features/home/ActivityTable.tsx b/superset-frontend/src/features/home/ActivityTable.tsx
index cd38c021f8..b3f43eac5e 100644
--- a/superset-frontend/src/features/home/ActivityTable.tsx
+++ b/superset-frontend/src/features/home/ActivityTable.tsx
@@ -105,7 +105,7 @@ const getEntityIcon = (entity: ActivityObject) => {
};
const getEntityUrl = (entity: ActivityObject) => {
- if ('sql' in entity) return `/superset/sqllab?savedQueryId=${entity.id}`;
+ if ('sql' in entity) return `/sqllab?savedQueryId=${entity.id}`;
if ('url' in entity) return entity.url;
return entity.item_url;
};
diff --git a/superset-frontend/src/features/home/EmptyState.tsx b/superset-frontend/src/features/home/EmptyState.tsx
index 47e7817ae3..d36d1bdbd6 100644
--- a/superset-frontend/src/features/home/EmptyState.tsx
+++ b/superset-frontend/src/features/home/EmptyState.tsx
@@ -17,6 +17,7 @@
* under the License.
*/
import React from 'react';
+import { Link } from 'react-router-dom';
import Button from 'src/components/Button';
import { Empty } from 'src/components';
import { TableTab } from 'src/views/CRUD/types';
@@ -81,7 +82,7 @@ export default function EmptyState({
const mineRedirects: Redirects = {
[WelcomeTable.Charts]: '/chart/add',
[WelcomeTable.Dashboards]: '/dashboard/new',
- [WelcomeTable.SavedQueries]: '/superset/sqllab?new=true',
+ [WelcomeTable.SavedQueries]: '/sqllab?new=true',
};
const favRedirects: Redirects = {
[WelcomeTable.Charts]: '/chart/list',
@@ -140,20 +141,17 @@ export default function EmptyState({
>
{tableName !== WelcomeTable.Recents && (
<ButtonContainer>
- <Button
- buttonStyle="primary"
- onClick={() => {
- window.location.href = mineRedirects[tableName];
- }}
- >
- <i className="fa fa-plus" />
- {tableName === WelcomeTable.SavedQueries
- ? t('SQL query')
- : tableName
- .split('')
- .slice(0, tableName.length - 1)
- .join('')}
- </Button>
+ <Link to={mineRedirects[tableName]}>
+ <Button buttonStyle="primary">
+ <i className="fa fa-plus" />
+ {tableName === WelcomeTable.SavedQueries
+ ? t('SQL query')
+ : tableName
+ .split('')
+ .slice(0, tableName.length - 1)
+ .join('')}
+ </Button>
+ </Link>
</ButtonContainer>
)}
</Empty>
diff --git a/superset-frontend/src/features/home/Menu.test.tsx b/superset-frontend/src/features/home/Menu.test.tsx
index b40a5ab075..428a7366f0 100644
--- a/superset-frontend/src/features/home/Menu.test.tsx
+++ b/superset-frontend/src/features/home/Menu.test.tsx
@@ -62,7 +62,7 @@ const dropdownItems = [
},
{
label: 'SQL query',
- url: '/superset/sqllab?new=true',
+ url: '/sqllab?new=true',
icon: 'fa-fw fa-search',
perm: 'can_sqllab',
view: 'Superset',
diff --git a/superset-frontend/src/features/home/RightMenu.test.tsx b/superset-frontend/src/features/home/RightMenu.test.tsx
index 95d61def4c..97b9fb20bd 100644
--- a/superset-frontend/src/features/home/RightMenu.test.tsx
+++ b/superset-frontend/src/features/home/RightMenu.test.tsx
@@ -73,7 +73,7 @@ const dropdownItems = [
},
{
label: 'SQL query',
- url: '/superset/sqllab?new=true',
+ url: '/sqllab?new=true',
icon: 'fa-fw fa-search',
perm: 'can_sqllab',
view: 'Superset',
diff --git a/superset-frontend/src/features/home/RightMenu.tsx b/superset-frontend/src/features/home/RightMenu.tsx
index 831ae85ba3..b79ebb65f8 100644
--- a/superset-frontend/src/features/home/RightMenu.tsx
+++ b/superset-frontend/src/features/home/RightMenu.tsx
@@ -210,7 +210,7 @@ const RightMenu = ({
},
{
label: t('SQL query'),
- url: '/superset/sqllab?new=true',
+ url: '/sqllab?new=true',
icon: 'fa-fw fa-search',
perm: 'can_sqllab',
view: 'Superset',
diff --git a/superset-frontend/src/features/home/SavedQueries.tsx b/superset-frontend/src/features/home/SavedQueries.tsx
index 9417f03bea..f5ac37563f 100644
--- a/superset-frontend/src/features/home/SavedQueries.tsx
+++ b/superset-frontend/src/features/home/SavedQueries.tsx
@@ -17,6 +17,7 @@
* under the License.
*/
import React, { useState } from 'react';
+import { Link } from 'react-router-dom';
import { styled, SupersetClient, t, useTheme } from '@superset-ui/core';
import SyntaxHighlighter from 'react-syntax-highlighter/dist/cjs/light';
import sql from 'react-syntax-highlighter/dist/cjs/languages/hljs/sql';
@@ -193,12 +194,8 @@ const SavedQueries = ({
const renderMenu = (query: Query) => (
<Menu>
{canEdit && (
- <Menu.Item
- onClick={() => {
- window.location.href = `/superset/sqllab?savedQueryId=${query.id}`;
- }}
- >
- {t('Edit')}
+ <Menu.Item>
+ <Link to={`/sqllab?savedQueryId=${query.id}`}>{t('Edit')}</Link>
</Menu.Item>
)}
<Menu.Item
@@ -256,15 +253,12 @@ const SavedQueries = ({
buttons={[
{
name: (
- <>
+ <Link to="/sqllab?new=true">
<i className="fa fa-plus" />
{t('SQL Query')}
- </>
+ </Link>
),
buttonStyle: 'tertiary',
- onClick: () => {
- window.location.href = '/superset/sqllab?new=true';
- },
},
{
name: t('View All ยป'),
@@ -278,15 +272,10 @@ const SavedQueries = ({
{queries.length > 0 ? (
<CardContainer showThumbnails={showThumbnails}>
{queries.map(q => (
- <CardStyles
- onClick={() => {
- window.location.href = `/superset/sqllab?savedQueryId=${q.id}`;
- }}
- key={q.id}
- >
+ <CardStyles key={q.id}>
<ListViewCard
imgURL=""
- url={`/superset/sqllab?savedQueryId=${q.id}`}
+ url={`/sqllab?savedQueryId=${q.id}`}
title={q.label}
imgFallbackURL="/static/assets/images/empty-query.svg"
description={t('Ran %s', q.changed_on_delta_humanized)}
diff --git a/superset-frontend/src/features/home/SubMenu.tsx b/superset-frontend/src/features/home/SubMenu.tsx
index e5b9f70900..f03396de8d 100644
--- a/superset-frontend/src/features/home/SubMenu.tsx
+++ b/superset-frontend/src/features/home/SubMenu.tsx
@@ -180,7 +180,7 @@ type MenuChild = {
export interface ButtonProps {
name: ReactNode;
- onClick: OnClickHandler;
+ onClick?: OnClickHandler;
'data-test'?: string;
buttonStyle:
| 'primary'
diff --git a/superset-frontend/src/features/home/commonMenuData.ts b/superset-frontend/src/features/home/commonMenuData.ts
index 634176145b..1a60189e62 100644
--- a/superset-frontend/src/features/home/commonMenuData.ts
+++ b/superset-frontend/src/features/home/commonMenuData.ts
@@ -30,7 +30,7 @@ export const commonMenuData = {
{
name: 'Query history',
label: t('Query history'),
- url: '/superset/sqllab/history/',
+ url: '/sqllab/history/',
usesRouter: true,
},
],
diff --git a/superset-frontend/src/hooks/apiResources/queryApi.ts b/superset-frontend/src/hooks/apiResources/queryApi.ts
index 3461c8443e..b7bf7f5b5d 100644
--- a/superset-frontend/src/hooks/apiResources/queryApi.ts
+++ b/superset-frontend/src/hooks/apiResources/queryApi.ts
@@ -17,7 +17,10 @@
* under the License.
*/
import rison from 'rison';
-import { getClientErrorObject } from 'src/utils/getClientErrorObject';
+import {
+ ClientErrorObject,
+ getClientErrorObject,
+} from 'src/utils/getClientErrorObject';
import { createApi, BaseQueryFn } from '@reduxjs/toolkit/query/react';
import {
SupersetClient,
@@ -35,7 +38,9 @@ export const supersetClientQuery: BaseQueryFn<
parseMethod?: ParseMethod;
transformResponse?: (response: SupersetClientResponse) => JsonValue;
urlParams?: Record<string, number | string | undefined | boolean>;
- }
+ },
+ JsonValue,
+ ClientErrorObject
> = (
{
endpoint,
diff --git a/superset-frontend/src/pages/DatasetCreation/DatasetCreation.test.tsx b/superset-frontend/src/pages/DatasetCreation/DatasetCreation.test.tsx
index 41b32965e8..8f41228315 100644
--- a/superset-frontend/src/pages/DatasetCreation/DatasetCreation.test.tsx
+++ b/superset-frontend/src/pages/DatasetCreation/DatasetCreation.test.tsx
@@ -31,7 +31,7 @@ jest.mock('react-router-dom', () => ({
describe('AddDataset', () => {
it('renders a blank state AddDataset', async () => {
- render(<AddDataset />, { useRedux: true });
+ render(<AddDataset />, { useRedux: true, useRouter: true });
const blankeStateImgs = screen.getAllByRole('img', { name: /empty/i });
diff --git a/superset-frontend/src/pages/QueryHistoryList/index.tsx b/superset-frontend/src/pages/QueryHistoryList/index.tsx
index 1d735fd69e..63e916e399 100644
--- a/superset-frontend/src/pages/QueryHistoryList/index.tsx
+++ b/superset-frontend/src/pages/QueryHistoryList/index.tsx
@@ -17,6 +17,7 @@
* under the License.
*/
import React, { useMemo, useState, useCallback, ReactElement } from 'react';
+import { Link, useHistory } from 'react-router-dom';
import {
QueryState,
styled,
@@ -102,6 +103,7 @@ function QueryList({ addDangerToast }: QueryListProps) {
useState<QueryObject>();
const theme = useTheme();
+ const history = useHistory();
const handleQueryPreview = useCallback(
(id: number) => {
@@ -334,9 +336,9 @@ function QueryList({ addDangerToast }: QueryListProps) {
},
}: any) => (
<Tooltip title={t('Open query in SQL Lab')} placement="bottom">
- <a href={`/superset/sqllab?queryId=${id}`}>
+ <Link to={`/sqllab?queryId=${id}`}>
<Icons.Full iconColor={theme.colors.grayscale.base} />
- </a>
+ </Link>
</Tooltip>
),
},
@@ -427,9 +429,7 @@ function QueryList({ addDangerToast }: QueryListProps) {
query={queryCurrentlyPreviewing}
queries={queries}
fetchData={handleQueryPreview}
- openInSqlLab={(id: number) =>
- window.location.assign(`/superset/sqllab?queryId=${id}`)
- }
+ openInSqlLab={(id: number) => history.push(`/sqllab?queryId=${id}`)}
show
/>
)}
diff --git a/superset-frontend/src/pages/SavedQueryList/index.tsx b/superset-frontend/src/pages/SavedQueryList/index.tsx
index a2f3479b90..8c1ce2b3dd 100644
--- a/superset-frontend/src/pages/SavedQueryList/index.tsx
+++ b/superset-frontend/src/pages/SavedQueryList/index.tsx
@@ -25,6 +25,7 @@ import {
t,
} from '@superset-ui/core';
import React, { useState, useMemo, useCallback } from 'react';
+import { Link, useHistory } from 'react-router-dom';
import rison from 'rison';
import moment from 'moment';
import {
@@ -127,6 +128,7 @@ function SavedQueryList({
sshTunnelPrivateKeyPasswordFields,
setSSHTunnelPrivateKeyPasswordFields,
] = useState<string[]>([]);
+ const history = useHistory();
const openSavedQueryImportModal = () => {
showImportModal(true);
@@ -148,10 +150,6 @@ function SavedQueryList({
const canExport =
hasPerm('can_export') && isFeatureEnabled(FeatureFlag.VERSIONED_EXPORT);
- const openNewQuery = () => {
- window.open(`${window.location.origin}/superset/sqllab?new=true`);
- };
-
const handleSavedQueryPreview = useCallback(
(id: number) => {
SupersetClient.get({
@@ -187,11 +185,10 @@ function SavedQueryList({
subMenuButtons.push({
name: (
- <>
+ <Link to="/sqllab?new=true">
<i className="fa fa-plus" /> {t('Query')}
- </>
+ </Link>
),
- onClick: openNewQuery,
buttonStyle: 'primary',
});
@@ -217,15 +214,13 @@ function SavedQueryList({
// Action methods
const openInSqlLab = (id: number) => {
- window.open(`${window.location.origin}/superset/sqllab?savedQueryId=${id}`);
+ history.push(`/sqllab?savedQueryId=${id}`);
};
const copyQueryLink = useCallback(
(id: number) => {
copyTextToClipboard(() =>
- Promise.resolve(
- `${window.location.origin}/superset/sqllab?savedQueryId=${id}`,
- ),
+ Promise.resolve(`${window.location.origin}/sqllab?savedQueryId=${id}`),
)
.then(() => {
addSuccessToast(t('Link Copied!'));
diff --git a/superset-frontend/src/SqlLab/reducers/index.js b/superset-frontend/src/pages/SqlLab/LocationContext.tsx
similarity index 56%
rename from superset-frontend/src/SqlLab/reducers/index.js
rename to superset-frontend/src/pages/SqlLab/LocationContext.tsx
index 35c16ba2e1..a67b887c99 100644
--- a/superset-frontend/src/SqlLab/reducers/index.js
+++ b/superset-frontend/src/pages/SqlLab/LocationContext.tsx
@@ -16,17 +16,26 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { combineReducers } from 'redux';
-import messageToasts from 'src/components/MessageToasts/reducers';
-import sqlLab from './sqlLab';
-import localStorageUsageInKilobytes from './localStorageUsage';
-import common from './common';
-export const reducers = {
- sqlLab,
- localStorageUsageInKilobytes,
- messageToasts,
- common,
+import React, { createContext, useContext } from 'react';
+import { useLocation } from 'react-router-dom';
+
+export type LocationState = {
+ requestedQuery?: Record<string, any>;
+};
+
+export const locationContext = createContext<LocationState>({});
+const { Provider } = locationContext;
+
+const EMPTY_STATE: LocationState = {};
+
+export const LocationProvider: React.FC = ({
+ children,
+}: {
+ children: React.ReactNode;
+}) => {
+ const location = useLocation<LocationState>();
+ return <Provider value={location.state || EMPTY_STATE}>{children}</Provider>;
};
-export default combineReducers(reducers);
+export const useLocationState = () => useContext(locationContext);
diff --git a/superset-frontend/src/pages/SqlLab/SqlLab.test.tsx b/superset-frontend/src/pages/SqlLab/SqlLab.test.tsx
new file mode 100644
index 0000000000..0eec7156d1
--- /dev/null
+++ b/superset-frontend/src/pages/SqlLab/SqlLab.test.tsx
@@ -0,0 +1,99 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import fetchMock from 'fetch-mock';
+import React from 'react';
+import { omit } from 'lodash';
+import {
+ render,
+ act,
+ waitFor,
+ defaultStore as store,
+ createStore,
+} from 'spec/helpers/testing-library';
+import reducers from 'spec/helpers/reducerIndex';
+import { api } from 'src/hooks/apiResources/queryApi';
+import { DEFAULT_COMMON_BOOTSTRAP_DATA } from 'src/constants';
+import getInitialState from 'src/SqlLab/reducers/getInitialState';
+
+import SqlLab from '.';
+
+const fakeApiResult = {
+ result: {
+ common: DEFAULT_COMMON_BOOTSTRAP_DATA,
+ tab_state_ids: [],
+ databases: [],
+ queries: {},
+ user: {
+ userId: 1,
+ username: 'some name',
+ isActive: true,
+ isAnonymous: false,
+ firstName: 'first name',
+ lastName: 'last name',
+ permissions: {},
+ roles: {},
+ },
+ },
+};
+
+const expectedResult = fakeApiResult.result;
+const sqlLabInitialStateApiRoute = `glob:*/api/v1/sqllab/`;
+
+afterEach(() => {
+ fetchMock.reset();
+ act(() => {
+ store.dispatch(api.util.resetApiState());
+ });
+});
+
+beforeEach(() => {
+ fetchMock.get(sqlLabInitialStateApiRoute, fakeApiResult);
+});
+
+jest.mock('src/SqlLab/components/App', () => () => (
+ <div data-test="mock-sqllab-app" />
+));
+
+test('is valid', () => {
+ expect(React.isValidElement(<SqlLab />)).toBe(true);
+});
+
+test('fetches initial data and renders', async () => {
+ expect(fetchMock.calls(sqlLabInitialStateApiRoute).length).toBe(0);
+ const storeWithSqlLab = createStore({}, reducers);
+ const { getByTestId } = render(<SqlLab />, {
+ useRedux: true,
+ useRouter: true,
+ store: storeWithSqlLab,
+ });
+
+ await waitFor(() =>
+ expect(fetchMock.calls(sqlLabInitialStateApiRoute).length).toBe(1),
+ );
+
+ expect(getByTestId('mock-sqllab-app')).toBeInTheDocument();
+ const { sqlLab } = getInitialState(expectedResult);
+ expect(storeWithSqlLab.getState()).toEqual(
+ expect.objectContaining({
+ sqlLab: expect.objectContaining(
+ omit(sqlLab, ['queriesLastUpdate', 'editorTabLastUpdatedAt']),
+ ),
+ }),
+ );
+});
diff --git a/superset-frontend/src/pages/SqlLab/index.tsx b/superset-frontend/src/pages/SqlLab/index.tsx
new file mode 100644
index 0000000000..e9f84f1b1d
--- /dev/null
+++ b/superset-frontend/src/pages/SqlLab/index.tsx
@@ -0,0 +1,78 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import React, { useEffect } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { css } from '@superset-ui/core';
+import { useSqlLabInitialState } from 'src/hooks/apiResources/sqlLab';
+import type { InitialState } from 'src/hooks/apiResources/sqlLab';
+import { resetState } from 'src/SqlLab/actions/sqlLab';
+import { addDangerToast } from 'src/components/MessageToasts/actions';
+import type { SqlLabRootState } from 'src/SqlLab/types';
+import { SqlLabGlobalStyles } from 'src/SqlLab//SqlLabGlobalStyles';
+import App from 'src/SqlLab/components/App';
+import Loading from 'src/components/Loading';
+import useEffectEvent from 'src/hooks/useEffectEvent';
+import { LocationProvider } from './LocationContext';
+
+export default function SqlLab() {
+ const editorTabLastUpdatedAt = useSelector<SqlLabRootState, number>(
+ state => state.sqlLab.editorTabLastUpdatedAt || 0,
+ );
+ const { data, isLoading, isError, error, fulfilledTimeStamp } =
+ useSqlLabInitialState();
+ const shouldInitialize = editorTabLastUpdatedAt <= (fulfilledTimeStamp || 0);
+ const dispatch = useDispatch();
+
+ const initBootstrapData = useEffectEvent(
+ (sqlLabInitialState: InitialState) => {
+ if (shouldInitialize) {
+ dispatch(resetState(sqlLabInitialState));
+ }
+ },
+ );
+
+ useEffect(() => {
+ if (data) {
+ initBootstrapData(data);
+ }
+ }, [data, initBootstrapData]);
+
+ if (isLoading || shouldInitialize) return <Loading />;
+
+ if (isError && error?.message) {
+ dispatch(addDangerToast(error?.message));
+ return null;
+ }
+
+ return (
+ <LocationProvider>
+ <div
+ css={css`
+ flex: 1 1 auto;
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ `}
+ >
+ <SqlLabGlobalStyles />
+ <App />
+ </div>
+ </LocationProvider>
+ );
+}
diff --git a/superset-frontend/src/views/CRUD/hooks.ts b/superset-frontend/src/views/CRUD/hooks.ts
index b539ca126f..85f7c60252 100644
--- a/superset-frontend/src/views/CRUD/hooks.ts
+++ b/superset-frontend/src/views/CRUD/hooks.ts
@@ -674,9 +674,7 @@ export const copyQueryLink = (
addSuccessToast: (arg0: string) => void,
) => {
copyTextToClipboard(() =>
- Promise.resolve(
- `${window.location.origin}/superset/sqllab?savedQueryId=${id}`,
- ),
+ Promise.resolve(`${window.location.origin}/sqllab?savedQueryId=${id}`),
)
.then(() => {
addSuccessToast(t('Link Copied!'));
diff --git a/superset-frontend/src/views/routes.tsx b/superset-frontend/src/views/routes.tsx
index 197284d3ac..2e5b987e2e 100644
--- a/superset-frontend/src/views/routes.tsx
+++ b/superset-frontend/src/views/routes.tsx
@@ -104,6 +104,10 @@ const SavedQueryList = lazy(
import(/* webpackChunkName: "SavedQueryList" */ 'src/pages/SavedQueryList'),
);
+const SqlLab = lazy(
+ () => import(/* webpackChunkName: "SqlLab" */ 'src/pages/SqlLab'),
+);
+
const AllEntities = lazy(
() => import(/* webpackChunkName: "AllEntities" */ 'src/pages/AllEntities'),
);
@@ -176,7 +180,7 @@ export const routes: Routes = [
Component: AnnotationList,
},
{
- path: '/superset/sqllab/history/',
+ path: '/sqllab/history/',
Component: QueryHistoryList,
},
{
@@ -225,6 +229,10 @@ export const routes: Routes = [
path: '/profile',
Component: Profile,
},
+ {
+ path: '/sqllab/',
+ Component: SqlLab,
+ },
];
if (isFeatureEnabled(FeatureFlag.TAGGING_SYSTEM)) {
diff --git a/superset-frontend/webpack.config.js b/superset-frontend/webpack.config.js
index 7bad2ea875..dea99be2cf 100644
--- a/superset-frontend/webpack.config.js
+++ b/superset-frontend/webpack.config.js
@@ -211,7 +211,6 @@ const config = {
menu: addPreamble('src/views/menu.tsx'),
spa: addPreamble('/src/views/index.tsx'),
embedded: addPreamble('/src/embedded/index.tsx'),
- sqllab: addPreamble('/src/SqlLab/index.tsx'),
},
output,
stats: 'minimal',
diff --git a/superset/initialization/__init__.py b/superset/initialization/__init__.py
index 1cab4b1bf5..e84689994a 100644
--- a/superset/initialization/__init__.py
+++ b/superset/initialization/__init__.py
@@ -192,6 +192,7 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods
TableSchemaView,
TabStateView,
)
+ from superset.views.sqllab import SqllabView
from superset.views.tags import TagModelView, TagView
from superset.views.users.api import CurrentUserRestApi
@@ -316,6 +317,7 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods
appbuilder.add_view_no_menu(SavedQueryViewApi)
appbuilder.add_view_no_menu(SliceAsync)
appbuilder.add_view_no_menu(SqlLab)
+ appbuilder.add_view_no_menu(SqllabView)
appbuilder.add_view_no_menu(SqlMetricInlineView)
appbuilder.add_view_no_menu(Superset)
appbuilder.add_view_no_menu(TableColumnInlineView)
@@ -347,7 +349,7 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods
appbuilder.add_link(
"SQL Editor",
label=__("SQL Lab"),
- href="/superset/sqllab/",
+ href="/sqllab/",
category_icon="fa-flask",
icon="fa-flask",
category="SQL Lab",
@@ -364,7 +366,7 @@ class SupersetAppInitializer: # pylint: disable=too-many-public-methods
appbuilder.add_link(
"Query Search",
label=__("Query History"),
- href="/superset/sqllab/history/",
+ href="/sqllab/history/",
icon="fa-search",
category_icon="fa-flask",
category="SQL Lab",
diff --git a/superset/jinja_context.py b/superset/jinja_context.py
index 89f9c8ddcc..71ebf0d29a 100644
--- a/superset/jinja_context.py
+++ b/superset/jinja_context.py
@@ -157,7 +157,7 @@ class ExtraCache:
When in SQL Lab, it's possible to add arbitrary URL "query string" parameters,
and use those in your SQL code. For instance you can alter your url and add
- `?foo=bar`, as in `{domain}/superset/sqllab?foo=bar`. Then if your query is
+ `?foo=bar`, as in `{domain}/sqllab?foo=bar`. Then if your query is
something like SELECT * FROM foo = '{{ url_param('foo') }}', it will be parsed
at runtime and replaced by the value in the URL.
diff --git a/superset/models/core.py b/superset/models/core.py
index 332e5bb513..f6e4b972b4 100755
--- a/superset/models/core.py
+++ b/superset/models/core.py
@@ -492,7 +492,7 @@ class Database(
source = utils.QuerySource.DASHBOARD
elif "/explore/" in request.referrer:
source = utils.QuerySource.CHART
- elif "/superset/sqllab" in request.referrer:
+ elif "/sqllab/" in request.referrer:
source = utils.QuerySource.SQL_LAB
sqlalchemy_url, params = DB_CONNECTION_MUTATOR(
diff --git a/superset/models/sql_lab.py b/superset/models/sql_lab.py
index ffd19fb0e4..7e63e984df 100644
--- a/superset/models/sql_lab.py
+++ b/superset/models/sql_lab.py
@@ -408,7 +408,7 @@ class SavedQuery(Model, AuditMixinNullable, ExtraJSONMixin, ImportExportMixin):
def pop_tab_link(self) -> Markup:
return Markup(
f"""
- <a href="/superset/sqllab?savedQueryId={self.id}">
+ <a href="/sqllab?savedQueryId={self.id}">
<i class="fa fa-link"></i>
</a>
"""
@@ -423,7 +423,7 @@ class SavedQuery(Model, AuditMixinNullable, ExtraJSONMixin, ImportExportMixin):
return self.database.sqlalchemy_uri
def url(self) -> str:
- return f"/superset/sqllab?savedQueryId={self.id}"
+ return f"/sqllab?savedQueryId={self.id}"
@property
def sql_tables(self) -> list[Table]:
diff --git a/superset/sqllab/api.py b/superset/sqllab/api.py
index d085174b5f..b3363e2e9a 100644
--- a/superset/sqllab/api.py
+++ b/superset/sqllab/api.py
@@ -25,6 +25,7 @@ from flask_appbuilder.models.sqla.interface import SQLAInterface
from marshmallow import ValidationError
from superset import app, is_feature_enabled
+from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP
from superset.daos.database import DatabaseDAO
from superset.daos.query import QueryDAO
from superset.extensions import event_logger
@@ -67,6 +68,7 @@ logger = logging.getLogger(__name__)
class SqlLabRestApi(BaseSupersetApi):
+ method_permission_name = MODEL_API_RW_METHOD_PERMISSION_MAP
datamodel = SQLAInterface(Query)
resource_name = "sqllab"
diff --git a/superset/views/core.py b/superset/views/core.py
index e67a255da2..95636de6ad 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -72,7 +72,6 @@ from superset.models.dashboard import Dashboard
from superset.models.slice import Slice
from superset.models.sql_lab import Query
from superset.models.user_attributes import UserAttribute
-from superset.sqllab.utils import bootstrap_sqllab_data
from superset.superset_typing import FlaskResponse
from superset.utils import core as utils
from superset.utils.cache import etag_cache
@@ -982,28 +981,18 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
"POST",
),
)
+ @deprecated(new_target="/sqllab")
def sqllab(self) -> FlaskResponse:
"""SQL Editor"""
- payload = {
- "common": common_bootstrap_payload(g.user),
- **bootstrap_sqllab_data(get_user_id()),
- }
-
- if form_data := request.form.get("form_data"):
- with contextlib.suppress(json.JSONDecodeError):
- payload["requested_query"] = json.loads(form_data)
- payload["user"] = bootstrap_user_data(g.user, include_perms=True)
- bootstrap_data = json.dumps(
- payload, default=utils.pessimistic_json_iso_dttm_ser
- )
-
- return self.render_template(
- "superset/basic.html", entry="sqllab", bootstrap_data=bootstrap_data
- )
+ url = "/sqllab"
+ if url_params := request.args:
+ params = parse.urlencode(url_params)
+ url = f"{url}?{params}"
+ return redirect(url)
@has_access
@event_logger.log_this
@expose("/sqllab/history/", methods=("GET",))
- @event_logger.log_this
+ @deprecated(new_target="/sqllab/history")
def sqllab_history(self) -> FlaskResponse:
- return super().render_app_template()
+ return redirect("/sqllab/history")
diff --git a/superset/views/sqllab.py b/superset/views/sqllab.py
new file mode 100644
index 0000000000..708716511f
--- /dev/null
+++ b/superset/views/sqllab.py
@@ -0,0 +1,46 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from flask_appbuilder import permission_name
+from flask_appbuilder.api import expose
+from flask_appbuilder.security.decorators import has_access
+
+from superset import event_logger
+from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP
+from superset.superset_typing import FlaskResponse
+
+from .base import BaseSupersetView
+
+
+class SqllabView(BaseSupersetView):
+ route_base = "/sqllab"
+ class_permission_name = "SQLLab"
+
+ method_permission_name = MODEL_API_RW_METHOD_PERMISSION_MAP
+
+ @expose("/")
+ @has_access
+ @permission_name("read")
+ @event_logger.log_this
+ def root(self) -> FlaskResponse:
+ return self.render_app_template()
+
+ @expose("/history/", methods=("GET",))
+ @has_access
+ @permission_name("read")
+ @event_logger.log_this
+ def history(self) -> FlaskResponse:
+ return self.render_app_template()
diff --git a/tests/integration_tests/core_tests.py b/tests/integration_tests/core_tests.py
index 5f379e2c47..6d06e46fa3 100644
--- a/tests/integration_tests/core_tests.py
+++ b/tests/integration_tests/core_tests.py
@@ -49,7 +49,6 @@ from superset.models.dashboard import Dashboard
from superset.models.slice import Slice
from superset.models.sql_lab import Query
from superset.result_set import SupersetResultSet
-from superset.sqllab.utils import bootstrap_sqllab_data
from superset.utils import core as utils
from superset.utils.core import backend
from superset.utils.database import get_example_database
@@ -956,7 +955,6 @@ class TestCore(SupersetTestCase):
dash_id = db.session.query(Dashboard.id).first()[0]
tbl_id = self.table_ids.get("wb_health_population")
urls = [
- "/superset/sqllab",
"/superset/welcome",
f"/superset/dashboard/{dash_id}/",
"/superset/profile/",
@@ -1161,6 +1159,25 @@ class TestCore(SupersetTestCase):
resp = self.client.get("/superset/profile/")
assert resp.status_code == 302
+ def test_redirect_new_sqllab(self):
+ self.login(username="admin")
+ resp = self.client.get(
+ "/superset/sqllab?savedQueryId=1&testParams=2",
+ follow_redirects=True,
+ )
+ assert resp.request.path == "/sqllab/"
+ assert (
+ resp.request.query_string.decode("utf-8") == "savedQueryId=1&testParams=2"
+ )
+
+ resp = self.client.post("/superset/sqllab/")
+ assert resp.status_code == 302
+
+ def test_redirect_new_sqllab_history(self):
+ self.login(username="admin")
+ resp = self.client.get("/superset/sqllab/history/")
+ assert resp.status_code == 302
+
if __name__ == "__main__":
unittest.main()
diff --git a/tests/integration_tests/sqllab_tests.py b/tests/integration_tests/sqllab_tests.py
index fbab4d98d2..3b8941e556 100644
--- a/tests/integration_tests/sqllab_tests.py
+++ b/tests/integration_tests/sqllab_tests.py
@@ -259,7 +259,7 @@ class TestSqlLab(SupersetTestCase):
def test_sqllab_has_access(self):
for username in ("admin", "gamma_sqllab"):
self.login(username)
- for endpoint in ("/superset/sqllab/", "/superset/sqllab/history/"):
+ for endpoint in ("/sqllab/", "/sqllab/history/"):
resp = self.client.get(endpoint)
self.assertEqual(200, resp.status_code)
@@ -267,7 +267,7 @@ class TestSqlLab(SupersetTestCase):
def test_sqllab_no_access(self):
self.login("gamma")
- for endpoint in ("/superset/sqllab/", "/superset/sqllab/history/"):
+ for endpoint in ("/sqllab/", "/sqllab/history/"):
resp = self.client.get(endpoint)
# Redirects to the main page
self.assertEqual(302, resp.status_code)