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)