You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by hu...@apache.org on 2022/09/01 18:01:40 UTC

[superset] 01/01: Revert "perf(sqllab): Rendering perf improvement using immutable state (#20877)"

This is an automated email from the ASF dual-hosted git repository.

hugh pushed a commit to branch revert-f77b910e2cc9f1bd90ac0f3a9097ec5d394b582d
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 15c713e90fca3478e48544a95377cbd0f180c4ff
Author: hughhhh <hu...@gmail.com>
AuthorDate: Thu Sep 1 10:58:40 2022 -0700

    Revert "perf(sqllab): Rendering perf improvement using immutable state (#20877)"
    
    This reverts commit f77b910e2cc9f1bd90ac0f3a9097ec5d394b582d.
---
 .../cypress/integration/sqllab/tabs.test.ts        |   6 +-
 superset-frontend/src/SqlLab/App.jsx               |   9 -
 superset-frontend/src/SqlLab/actions/sqlLab.js     | 102 +-----
 .../src/SqlLab/actions/sqlLab.test.js              | 122 +++----
 .../AceEditorWrapper/AceEditorWrapper.test.tsx     | 129 -------
 .../SqlLab/components/AceEditorWrapper/index.tsx   |  51 +--
 .../EstimateQueryCostButton.test.tsx               |  93 -----
 .../components/EstimateQueryCostButton/index.tsx   |  32 +-
 .../QueryLimitSelect/QueryLimitSelect.test.tsx     | 137 --------
 .../SqlLab/components/QueryLimitSelect/index.tsx   | 118 -------
 .../RunQueryActionButton.test.jsx                  |  53 +++
 .../RunQueryActionButton.test.tsx                  | 151 ---------
 .../components/RunQueryActionButton/index.tsx      |  32 +-
 .../SqlLab/components/SaveQuery/SaveQuery.test.jsx |  47 +--
 .../src/SqlLab/components/SaveQuery/index.tsx      |  35 +-
 .../SqlLab/components/SqlEditor/SqlEditor.test.jsx |  53 +--
 .../src/SqlLab/components/SqlEditor/index.jsx      | 165 ++++++---
 .../SqlLab/components/SqlEditorLeftBar/index.tsx   |  19 +-
 .../SqlEditorTabHeader/SqlEditorTabHeader.test.tsx | 220 ------------
 .../SqlLab/components/SqlEditorTabHeader/index.tsx | 147 --------
 .../TabbedSqlEditors/TabbedSqlEditors.test.jsx     |  17 +-
 .../SqlLab/components/TabbedSqlEditors/index.jsx   | 142 ++++++--
 .../TemplateParamsEditor.test.tsx                  |  90 +----
 .../components/TemplateParamsEditor/index.tsx      |  22 +-
 superset-frontend/src/SqlLab/fixtures.ts           |  29 +-
 .../src/SqlLab/reducers/getInitialState.js         |  38 +--
 superset-frontend/src/SqlLab/reducers/sqlLab.js    | 376 ++++++---------------
 .../src/SqlLab/reducers/sqlLab.test.js             |  67 +---
 superset-frontend/src/SqlLab/types.ts              |  26 +-
 .../src/SqlLab/utils/newQueryTabName.test.ts       |   2 -
 .../src/components/Dropdown/index.tsx              |   2 +-
 .../src/components/TableSelector/index.tsx         |   3 +-
 32 files changed, 606 insertions(+), 1929 deletions(-)

diff --git a/superset-frontend/cypress-base/cypress/integration/sqllab/tabs.test.ts b/superset-frontend/cypress-base/cypress/integration/sqllab/tabs.test.ts
index c7f1a23e47..0e85664cb7 100644
--- a/superset-frontend/cypress-base/cypress/integration/sqllab/tabs.test.ts
+++ b/superset-frontend/cypress-base/cypress/integration/sqllab/tabs.test.ts
@@ -30,10 +30,8 @@ describe('SqlLab query tabs', () => {
       const initialUntitledCount = Math.max(
         0,
         ...tabs
-          .map(
-            (i, tabItem) =>
-              Number(tabItem.textContent?.match(/Untitled Query (\d+)/)?.[1]) ||
-              0,
+          .map((i, tabItem) =>
+            Number(tabItem.textContent?.match(/Untitled Query (\d+)/)?.[1]),
           )
           .toArray(),
       );
diff --git a/superset-frontend/src/SqlLab/App.jsx b/superset-frontend/src/SqlLab/App.jsx
index 39f784a5c5..02a4df2a6f 100644
--- a/superset-frontend/src/SqlLab/App.jsx
+++ b/superset-frontend/src/SqlLab/App.jsx
@@ -67,9 +67,6 @@ const sqlLabPersistStateConfig = {
             ...state[path],
             queries: emptyQueryResults(state[path].queries),
             queryEditors: clearQueryEditors(state[path].queryEditors),
-            unsavedQueryEditor: clearQueryEditors([
-              state[path].unsavedQueryEditor,
-            ])[0],
           };
         }
       });
@@ -94,12 +91,6 @@ const sqlLabPersistStateConfig = {
       const result = {
         ...initialState,
         ...persistedState,
-        sqlLab: {
-          ...(persistedState?.sqlLab || {}),
-          // Overwrite initialState over persistedState for sqlLab
-          // since a logic in getInitialState overrides the value from persistedState
-          ...initialState.sqlLab,
-        },
       };
       // Filter out any user data that may have been persisted in an older version.
       // Get user from bootstrap data instead, every time
diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.js b/superset-frontend/src/SqlLab/actions/sqlLab.js
index cb6f3c4095..df21e32df3 100644
--- a/superset-frontend/src/SqlLab/actions/sqlLab.js
+++ b/superset-frontend/src/SqlLab/actions/sqlLab.js
@@ -123,17 +123,6 @@ const fieldConverter = mapping => obj =>
 const convertQueryToServer = fieldConverter(queryServerMapping);
 const convertQueryToClient = fieldConverter(queryClientMapping);
 
-export function getUpToDateQuery(rootState, queryEditor, key) {
-  const {
-    sqlLab: { unsavedQueryEditor },
-  } = rootState;
-  const id = key ?? queryEditor.id;
-  return {
-    ...queryEditor,
-    ...(id === unsavedQueryEditor.id && unsavedQueryEditor),
-  };
-}
-
 export function resetState() {
   return { type: RESET_STATE };
 }
@@ -178,26 +167,24 @@ export function scheduleQuery(query) {
       );
 }
 
-export function estimateQueryCost(queryEditor) {
-  return (dispatch, getState) => {
-    const { dbId, schema, sql, selectedText, templateParams } =
-      getUpToDateQuery(getState(), queryEditor);
-    const requestSql = selectedText || sql;
-    const endpoint =
-      schema === null
-        ? `/superset/estimate_query_cost/${dbId}/`
-        : `/superset/estimate_query_cost/${dbId}/${schema}/`;
-    return Promise.all([
-      dispatch({ type: COST_ESTIMATE_STARTED, query: queryEditor }),
+export function estimateQueryCost(query) {
+  const { dbId, schema, sql, templateParams } = query;
+  const endpoint =
+    schema === null
+      ? `/superset/estimate_query_cost/${dbId}/`
+      : `/superset/estimate_query_cost/${dbId}/${schema}/`;
+  return dispatch =>
+    Promise.all([
+      dispatch({ type: COST_ESTIMATE_STARTED, query }),
       SupersetClient.post({
         endpoint,
         postPayload: {
-          sql: requestSql,
+          sql,
           templateParams: JSON.parse(templateParams || '{}'),
         },
       })
         .then(({ json }) =>
-          dispatch({ type: COST_ESTIMATE_RETURNED, query: queryEditor, json }),
+          dispatch({ type: COST_ESTIMATE_RETURNED, query, json }),
         )
         .catch(response =>
           getClientErrorObject(response).then(error => {
@@ -207,13 +194,12 @@ export function estimateQueryCost(queryEditor) {
               t('Failed at retrieving results');
             return dispatch({
               type: COST_ESTIMATE_FAILED,
-              query: queryEditor,
+              query,
               error: message,
             });
           }),
         ),
     ]);
-  };
 }
 
 export function startQuery(query) {
@@ -371,34 +357,6 @@ export function runQuery(query) {
   };
 }
 
-export function runQueryFromSqlEditor(
-  database,
-  queryEditor,
-  defaultQueryLimit,
-  tempTable,
-  ctas,
-  ctasMethod,
-) {
-  return function (dispatch, getState) {
-    const qe = getUpToDateQuery(getState(), queryEditor, queryEditor.id);
-    const query = {
-      dbId: qe.dbId,
-      sql: qe.selectedText || qe.sql,
-      sqlEditorId: qe.id,
-      tab: qe.name,
-      schema: qe.schema,
-      tempTable,
-      templateParams: qe.templateParams,
-      queryLimit: qe.queryLimit || defaultQueryLimit,
-      runAsync: database ? database.allow_run_async : false,
-      ctas,
-      ctas_method: ctasMethod,
-      updateTabState: !qe.selectedText,
-    };
-    dispatch(runQuery(query));
-  };
-}
-
 export function reRunQuery(query) {
   // run Query with a new id
   return function (dispatch) {
@@ -406,23 +364,8 @@ export function reRunQuery(query) {
   };
 }
 
-export function validateQuery(queryEditor, sql) {
-  return function (dispatch, getState) {
-    const {
-      sqlLab: { unsavedQueryEditor },
-    } = getState();
-    const qe = {
-      ...queryEditor,
-      ...(queryEditor.id === unsavedQueryEditor.id && unsavedQueryEditor),
-    };
-
-    const query = {
-      dbId: qe.dbId,
-      sql,
-      sqlEditorId: qe.id,
-      schema: qe.schema,
-      templateParams: qe.templateParams,
-    };
+export function validateQuery(query) {
+  return function (dispatch) {
     dispatch(startQueryValidation(query));
 
     const postPayload = {
@@ -677,7 +620,6 @@ export function switchQueryEditor(queryEditor, displayLimit) {
   return function (dispatch) {
     if (
       isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE) &&
-      queryEditor &&
       !queryEditor.loaded
     ) {
       SupersetClient.get({
@@ -781,17 +723,6 @@ export function removeQueryEditor(queryEditor) {
   };
 }
 
-export function removeAllOtherQueryEditors(queryEditor) {
-  return function (dispatch, getState) {
-    const { sqlLab } = getState();
-    sqlLab.queryEditors?.forEach(otherQueryEditor => {
-      if (otherQueryEditor.id !== queryEditor.id) {
-        dispatch(removeQueryEditor(otherQueryEditor));
-      }
-    });
-  };
-}
-
 export function removeQuery(query) {
   return function (dispatch) {
     const sync = isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)
@@ -990,9 +921,8 @@ export function queryEditorSetSql(queryEditor, sql) {
   return { type: QUERY_EDITOR_SET_SQL, queryEditor, sql };
 }
 
-export function queryEditorSetAndSaveSql(targetQueryEditor, sql) {
-  return function (dispatch, getState) {
-    const queryEditor = getUpToDateQuery(getState(), targetQueryEditor);
+export function queryEditorSetAndSaveSql(queryEditor, sql) {
+  return function (dispatch) {
     // saved query and set tab state use this action
     dispatch(queryEditorSetSql(queryEditor, sql));
     if (isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)) {
diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.test.js b/superset-frontend/src/SqlLab/actions/sqlLab.test.js
index 4dadb9ea89..1c4509b3a1 100644
--- a/superset-frontend/src/SqlLab/actions/sqlLab.test.js
+++ b/superset-frontend/src/SqlLab/actions/sqlLab.test.js
@@ -24,7 +24,7 @@ import thunk from 'redux-thunk';
 import shortid from 'shortid';
 import * as featureFlags from 'src/featureFlags';
 import * as actions from 'src/SqlLab/actions/sqlLab';
-import { defaultQueryEditor, query, initialState } from 'src/SqlLab/fixtures';
+import { defaultQueryEditor, query } from '../fixtures';
 
 const middlewares = [thunk];
 const mockStore = configureMockStore(middlewares);
@@ -32,13 +32,14 @@ const mockStore = configureMockStore(middlewares);
 describe('async actions', () => {
   const mockBigNumber = '9223372036854775807';
   const queryEditor = {
-    ...defaultQueryEditor,
     id: 'abcd',
     autorun: false,
+    dbId: null,
     latestQueryId: null,
+    selectedText: null,
     sql: 'SELECT *\nFROM\nWHERE',
     name: 'Untitled Query 1',
-    schemaOptions: [{ value: 'main', label: 'main', title: 'main' }],
+    schemaOptions: [{ value: 'main', label: 'main', name: 'main' }],
   };
 
   let dispatch;
@@ -64,20 +65,20 @@ describe('async actions', () => {
 
     const makeRequest = () => {
       const request = actions.saveQuery(query);
-      return request(dispatch, () => initialState);
+      return request(dispatch);
     };
 
     it('posts to the correct url', () => {
       expect.assertions(1);
 
-      const store = mockStore(initialState);
+      const store = mockStore({});
       return store.dispatch(actions.saveQuery(query)).then(() => {
         expect(fetchMock.calls(saveQueryEndpoint)).toHaveLength(1);
       });
     });
 
     it('posts the correct query object', () => {
-      const store = mockStore(initialState);
+      const store = mockStore({});
       return store.dispatch(actions.saveQuery(query)).then(() => {
         const call = fetchMock.calls(saveQueryEndpoint)[0];
         const formData = call[1].body;
@@ -106,7 +107,7 @@ describe('async actions', () => {
     it('onSave calls QUERY_EDITOR_SAVED and QUERY_EDITOR_SET_TITLE', () => {
       expect.assertions(1);
 
-      const store = mockStore(initialState);
+      const store = mockStore({});
       const expectedActionTypes = [
         actions.QUERY_EDITOR_SAVED,
         actions.QUERY_EDITOR_SET_TITLE,
@@ -190,7 +191,7 @@ describe('async actions', () => {
   describe('runQuery without query params', () => {
     const makeRequest = () => {
       const request = actions.runQuery(query);
-      return request(dispatch, () => initialState);
+      return request(dispatch);
     };
 
     it('makes the fetch request', () => {
@@ -223,9 +224,7 @@ describe('async actions', () => {
 
       const store = mockStore({});
       const expectedActionTypes = [actions.START_QUERY, actions.QUERY_SUCCESS];
-      const { dispatch } = store;
-      const request = actions.runQuery(query);
-      return request(dispatch, () => initialState).then(() => {
+      return store.dispatch(actions.runQuery(query)).then(() => {
         expect(store.getActions().map(a => a.type)).toEqual(
           expectedActionTypes,
         );
@@ -243,9 +242,7 @@ describe('async actions', () => {
 
       const store = mockStore({});
       const expectedActionTypes = [actions.START_QUERY, actions.QUERY_FAILED];
-      const { dispatch } = store;
-      const request = actions.runQuery(query);
-      return request(dispatch, () => initialState).then(() => {
+      return store.dispatch(actions.runQuery(query)).then(() => {
         expect(store.getActions().map(a => a.type)).toEqual(
           expectedActionTypes,
         );
@@ -268,19 +265,15 @@ describe('async actions', () => {
 
     const makeRequest = () => {
       const request = actions.runQuery(query);
-      return request(dispatch, () => initialState);
+      return request(dispatch);
     };
 
-    it('makes the fetch request', async () => {
-      const runQueryEndpointWithParams = 'glob:*/superset/sql_json/?foo=bar';
-      fetchMock.post(
-        runQueryEndpointWithParams,
-        `{ "data": ${mockBigNumber} }`,
-      );
-      await makeRequest().then(() => {
-        expect(fetchMock.calls(runQueryEndpointWithParams)).toHaveLength(1);
-      });
-    });
+    it('makes the fetch request', () =>
+      makeRequest().then(() => {
+        expect(
+          fetchMock.calls('glob:*/superset/sql_json/?foo=bar'),
+        ).toHaveLength(1);
+      }));
   });
 
   describe('reRunQuery', () => {
@@ -298,12 +291,10 @@ describe('async actions', () => {
         sqlLab: {
           tabHistory: [id],
           queryEditors: [{ id, name: 'Dummy query editor' }],
-          unsavedQueryEditor: {},
         },
       };
       const store = mockStore(state);
-      const request = actions.reRunQuery(query);
-      request(store.dispatch, store.getState);
+      store.dispatch(actions.reRunQuery(query));
       expect(store.getActions()[0].query.id).toEqual('abcd');
     });
   });
@@ -360,7 +351,6 @@ describe('async actions', () => {
         sqlLab: {
           tabHistory: [id],
           queryEditors: [{ id, name: 'Dummy query editor' }],
-          unsavedQueryEditor: {},
         },
       };
       const store = mockStore(state);
@@ -379,10 +369,11 @@ describe('async actions', () => {
           },
         },
       ];
-      const request = actions.cloneQueryToNewTab(query, true);
-      return request(store.dispatch, store.getState).then(() => {
-        expect(store.getActions()).toEqual(expectedActions);
-      });
+      return store
+        .dispatch(actions.cloneQueryToNewTab(query, true))
+        .then(() => {
+          expect(store.getActions()).toEqual(expectedActions);
+        });
     });
   });
 
@@ -398,17 +389,18 @@ describe('async actions', () => {
     it('creates new query editor', () => {
       expect.assertions(1);
 
-      const store = mockStore(initialState);
+      const store = mockStore({});
       const expectedActions = [
         {
           type: actions.ADD_QUERY_EDITOR,
           queryEditor,
         },
       ];
-      const request = actions.addQueryEditor(defaultQueryEditor);
-      return request(store.dispatch, store.getState).then(() => {
-        expect(store.getActions()).toEqual(expectedActions);
-      });
+      return store
+        .dispatch(actions.addQueryEditor(defaultQueryEditor))
+        .then(() => {
+          expect(store.getActions()).toEqual(expectedActions);
+        });
     });
   });
 
@@ -656,12 +648,14 @@ describe('async actions', () => {
         it('updates the tab state in the backend', () => {
           expect.assertions(2);
 
-          const store = mockStore(initialState);
-          const request = actions.queryEditorSetAndSaveSql(queryEditor, sql);
-          return request(store.dispatch, store.getState).then(() => {
-            expect(store.getActions()).toEqual(expectedActions);
-            expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(1);
-          });
+          const store = mockStore({});
+
+          return store
+            .dispatch(actions.queryEditorSetAndSaveSql(queryEditor, sql))
+            .then(() => {
+              expect(store.getActions()).toEqual(expectedActions);
+              expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(1);
+            });
         });
       });
       describe('with backend persistence flag off', () => {
@@ -672,9 +666,9 @@ describe('async actions', () => {
               feature => !(feature === 'SQLLAB_BACKEND_PERSISTENCE'),
             );
 
-          const store = mockStore(initialState);
-          const request = actions.queryEditorSetAndSaveSql(queryEditor, sql);
-          request(store.dispatch, store.getState);
+          const store = mockStore({});
+
+          store.dispatch(actions.queryEditorSetAndSaveSql(queryEditor, sql));
 
           expect(store.getActions()).toEqual(expectedActions);
           expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(0);
@@ -776,7 +770,7 @@ describe('async actions', () => {
         const database = { disable_data_preview: false, id: 1 };
         const tableName = 'table';
         const schemaName = 'schema';
-        const store = mockStore(initialState);
+        const store = mockStore({});
         const expectedActionTypes = [
           actions.MERGE_TABLE, // addTable
           actions.MERGE_TABLE, // getTableMetadata
@@ -786,24 +780,20 @@ describe('async actions', () => {
           actions.MERGE_TABLE, // addTable
           actions.QUERY_SUCCESS, // querySuccess
         ];
-        const request = actions.addTable(
-          query,
-          database,
-          tableName,
-          schemaName,
-        );
-        return request(store.dispatch, store.getState).then(() => {
-          expect(store.getActions().map(a => a.type)).toEqual(
-            expectedActionTypes,
-          );
-          expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(1);
-          expect(fetchMock.calls(getTableMetadataEndpoint)).toHaveLength(1);
-          expect(fetchMock.calls(getExtraTableMetadataEndpoint)).toHaveLength(
-            1,
-          );
-          // tab state is not updated, since the query is a data preview
-          expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(0);
-        });
+        return store
+          .dispatch(actions.addTable(query, database, tableName, schemaName))
+          .then(() => {
+            expect(store.getActions().map(a => a.type)).toEqual(
+              expectedActionTypes,
+            );
+            expect(fetchMock.calls(updateTableSchemaEndpoint)).toHaveLength(1);
+            expect(fetchMock.calls(getTableMetadataEndpoint)).toHaveLength(1);
+            expect(fetchMock.calls(getExtraTableMetadataEndpoint)).toHaveLength(
+              1,
+            );
+            // tab state is not updated, since the query is a data preview
+            expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(0);
+          });
       });
     });
 
diff --git a/superset-frontend/src/SqlLab/components/AceEditorWrapper/AceEditorWrapper.test.tsx b/superset-frontend/src/SqlLab/components/AceEditorWrapper/AceEditorWrapper.test.tsx
deleted file mode 100644
index 6bfefc0a3d..0000000000
--- a/superset-frontend/src/SqlLab/components/AceEditorWrapper/AceEditorWrapper.test.tsx
+++ /dev/null
@@ -1,129 +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 configureStore from 'redux-mock-store';
-import thunk from 'redux-thunk';
-import { render, waitFor } from 'spec/helpers/testing-library';
-import { QueryEditor } from 'src/SqlLab/types';
-import { Store } from 'redux';
-import { initialState, defaultQueryEditor } from 'src/SqlLab/fixtures';
-import {
-  queryEditorSetSelectedText,
-  queryEditorSetFunctionNames,
-  addTable,
-} from 'src/SqlLab/actions/sqlLab';
-import AceEditorWrapper from 'src/SqlLab/components/AceEditorWrapper';
-import { AsyncAceEditorProps } from 'src/components/AsyncAceEditor';
-
-const middlewares = [thunk];
-const mockStore = configureStore(middlewares);
-
-jest.mock('src/components/Select', () => () => (
-  <div data-test="mock-deprecated-select" />
-));
-jest.mock('src/components/Select/Select', () => () => (
-  <div data-test="mock-deprecated-select-select" />
-));
-jest.mock('src/components/Select/AsyncSelect', () => () => (
-  <div data-test="mock-deprecated-async-select" />
-));
-
-jest.mock('src/components/AsyncAceEditor', () => ({
-  FullSQLEditor: (props: AsyncAceEditorProps) => (
-    <div data-test="react-ace">{JSON.stringify(props)}</div>
-  ),
-}));
-
-const setup = (queryEditor: QueryEditor, store?: Store) =>
-  render(
-    <AceEditorWrapper
-      queryEditor={queryEditor}
-      actions={{
-        queryEditorSetSelectedText,
-        queryEditorSetFunctionNames,
-        addTable,
-      }}
-      height="100px"
-      hotkeys={[]}
-      database={{}}
-      onChange={jest.fn()}
-      onBlur={jest.fn()}
-      autocomplete
-    />,
-    {
-      useRedux: true,
-      ...(store && { store }),
-    },
-  );
-
-describe('AceEditorWrapper', () => {
-  it('renders ace editor including sql value', async () => {
-    const { getByTestId } = setup(defaultQueryEditor, mockStore(initialState));
-    await waitFor(() => expect(getByTestId('react-ace')).toBeInTheDocument());
-
-    expect(getByTestId('react-ace')).toHaveTextContent(
-      JSON.stringify({ value: defaultQueryEditor.sql }).slice(1, -1),
-    );
-  });
-
-  it('renders sql from unsaved change', () => {
-    const expectedSql = 'SELECT updated_column\nFROM updated_table\nWHERE';
-    const { getByTestId } = setup(
-      defaultQueryEditor,
-      mockStore({
-        ...initialState,
-        sqlLab: {
-          ...initialState.sqlLab,
-          unsavedQueryEditor: {
-            id: defaultQueryEditor.id,
-            sql: expectedSql,
-          },
-        },
-      }),
-    );
-
-    expect(getByTestId('react-ace')).toHaveTextContent(
-      JSON.stringify({ value: expectedSql }).slice(1, -1),
-    );
-  });
-
-  it('renders current sql for unrelated unsaved changes', () => {
-    const expectedSql = 'SELECT updated_column\nFROM updated_table\nWHERE';
-    const { getByTestId } = setup(
-      defaultQueryEditor,
-      mockStore({
-        ...initialState,
-        sqlLab: {
-          ...initialState.sqlLab,
-          unsavedQueryEditor: {
-            id: `${defaultQueryEditor.id}-other`,
-            sql: expectedSql,
-          },
-        },
-      }),
-    );
-
-    expect(getByTestId('react-ace')).not.toHaveTextContent(
-      JSON.stringify({ value: expectedSql }).slice(1, -1),
-    );
-    expect(getByTestId('react-ace')).toHaveTextContent(
-      JSON.stringify({ value: defaultQueryEditor.sql }).slice(1, -1),
-    );
-  });
-});
diff --git a/superset-frontend/src/SqlLab/components/AceEditorWrapper/index.tsx b/superset-frontend/src/SqlLab/components/AceEditorWrapper/index.tsx
index 6b70be228b..53ec3f808a 100644
--- a/superset-frontend/src/SqlLab/components/AceEditorWrapper/index.tsx
+++ b/superset-frontend/src/SqlLab/components/AceEditorWrapper/index.tsx
@@ -17,7 +17,6 @@
  * under the License.
  */
 import React from 'react';
-import { connect } from 'react-redux';
 import { areArraysShallowEqual } from 'src/reduxUtils';
 import sqlKeywords from 'src/SqlLab/utils/sqlKeywords';
 import {
@@ -31,7 +30,7 @@ import {
   AceCompleterKeyword,
   FullSQLEditor as AceEditor,
 } from 'src/components/AsyncAceEditor';
-import { QueryEditor, SchemaOption, SqlLabRootState } from 'src/SqlLab/types';
+import { QueryEditor } from 'src/SqlLab/types';
 
 type HotKey = {
   key: string;
@@ -40,13 +39,7 @@ type HotKey = {
   func: () => void;
 };
 
-type OwnProps = {
-  queryEditor: QueryEditor;
-  extendedTables: Array<{ name: string; columns: any[] }>;
-  autocomplete: boolean;
-  onChange: (sql: string) => void;
-  onBlur: (sql: string) => void;
-  database: any;
+interface Props {
   actions: {
     queryEditorSetSelectedText: (edit: any, text: null | string) => void;
     queryEditorSetFunctionNames: (queryEditor: object, dbId: number) => void;
@@ -57,19 +50,19 @@ type OwnProps = {
       schema: any,
     ) => void;
   };
-  hotkeys: HotKey[];
-  height: string;
-};
-
-type ReduxProps = {
-  queryEditor: QueryEditor;
+  autocomplete: boolean;
+  onBlur: (sql: string) => void;
   sql: string;
-  schemas: SchemaOption[];
+  database: any;
+  schemas: any[];
   tables: any[];
   functionNames: string[];
-};
-
-type Props = ReduxProps & OwnProps;
+  extendedTables: Array<{ name: string; columns: any[] }>;
+  queryEditor: QueryEditor;
+  height: string;
+  hotkeys: HotKey[];
+  onChange: (sql: string) => void;
+}
 
 interface State {
   sql: string;
@@ -293,22 +286,4 @@ class AceEditorWrapper extends React.PureComponent<Props, State> {
   }
 }
 
-function mapStateToProps(
-  { sqlLab: { unsavedQueryEditor } }: SqlLabRootState,
-  { queryEditor }: OwnProps,
-) {
-  const currentQueryEditor = {
-    ...queryEditor,
-    ...(queryEditor.id === unsavedQueryEditor.id && unsavedQueryEditor),
-  };
-  return {
-    queryEditor: currentQueryEditor,
-    sql: currentQueryEditor.sql,
-    schemas: currentQueryEditor.schemaOptions || [],
-    tables: currentQueryEditor.tableOptions,
-    functionNames: currentQueryEditor.functionNames,
-  };
-}
-export default connect<ReduxProps, {}, OwnProps>(mapStateToProps)(
-  AceEditorWrapper,
-);
+export default AceEditorWrapper;
diff --git a/superset-frontend/src/SqlLab/components/EstimateQueryCostButton/EstimateQueryCostButton.test.tsx b/superset-frontend/src/SqlLab/components/EstimateQueryCostButton/EstimateQueryCostButton.test.tsx
deleted file mode 100644
index 5a8eaeaa11..0000000000
--- a/superset-frontend/src/SqlLab/components/EstimateQueryCostButton/EstimateQueryCostButton.test.tsx
+++ /dev/null
@@ -1,93 +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 configureStore from 'redux-mock-store';
-import thunk from 'redux-thunk';
-import { render } from 'spec/helpers/testing-library';
-import { Store } from 'redux';
-import { initialState, defaultQueryEditor } from 'src/SqlLab/fixtures';
-
-import EstimateQueryCostButton, {
-  EstimateQueryCostButtonProps,
-} from 'src/SqlLab/components/EstimateQueryCostButton';
-
-const middlewares = [thunk];
-const mockStore = configureStore(middlewares);
-
-jest.mock('src/components/Select', () => () => (
-  <div data-test="mock-deprecated-select" />
-));
-jest.mock('src/components/Select/Select', () => () => (
-  <div data-test="mock-deprecated-select-select" />
-));
-jest.mock('src/components/Select/AsyncSelect', () => () => (
-  <div data-test="mock-deprecated-async-select" />
-));
-
-const setup = (props: Partial<EstimateQueryCostButtonProps>, store?: Store) =>
-  render(
-    <EstimateQueryCostButton
-      queryEditor={defaultQueryEditor}
-      getEstimate={jest.fn()}
-      {...props}
-    />,
-    {
-      useRedux: true,
-      ...(store && { store }),
-    },
-  );
-
-describe('EstimateQueryCostButton', () => {
-  it('renders EstimateQueryCostButton', async () => {
-    const { queryByText } = setup({}, mockStore(initialState));
-
-    expect(queryByText('Estimate cost')).toBeTruthy();
-  });
-
-  it('renders label for selected query', async () => {
-    const queryEditorWithSelectedText = {
-      ...defaultQueryEditor,
-      selectedText: 'SELECT',
-    };
-    const { queryByText } = setup(
-      { queryEditor: queryEditorWithSelectedText },
-      mockStore(initialState),
-    );
-
-    expect(queryByText('Estimate selected query cost')).toBeTruthy();
-  });
-
-  it('renders label for selected query from unsaved', async () => {
-    const { queryByText } = setup(
-      {},
-      mockStore({
-        ...initialState,
-        sqlLab: {
-          ...initialState.sqlLab,
-          unsavedQueryEditor: {
-            id: defaultQueryEditor.id,
-            selectedText: 'SELECT',
-          },
-        },
-      }),
-    );
-
-    expect(queryByText('Estimate selected query cost')).toBeTruthy();
-  });
-});
diff --git a/superset-frontend/src/SqlLab/components/EstimateQueryCostButton/index.tsx b/superset-frontend/src/SqlLab/components/EstimateQueryCostButton/index.tsx
index dc2c23c406..d7f2d7dd6d 100644
--- a/superset-frontend/src/SqlLab/components/EstimateQueryCostButton/index.tsx
+++ b/superset-frontend/src/SqlLab/components/EstimateQueryCostButton/index.tsx
@@ -24,37 +24,23 @@ import Button from 'src/components/Button';
 import Loading from 'src/components/Loading';
 import ModalTrigger from 'src/components/ModalTrigger';
 import { EmptyWrapperType } from 'src/components/TableView/TableView';
-import {
-  SqlLabRootState,
-  QueryCostEstimate,
-  QueryEditor,
-} from 'src/SqlLab/types';
-import { getUpToDateQuery } from 'src/SqlLab/actions/sqlLab';
-import { useSelector } from 'react-redux';
 
-export interface EstimateQueryCostButtonProps {
+interface EstimateQueryCostButtonProps {
   getEstimate: Function;
-  queryEditor: QueryEditor;
+  queryCostEstimate: Record<string, any>;
+  selectedText?: string;
   tooltip?: string;
   disabled?: boolean;
 }
 
 const EstimateQueryCostButton = ({
   getEstimate,
-  queryEditor,
+  queryCostEstimate = {},
+  selectedText,
   tooltip = '',
   disabled = false,
 }: EstimateQueryCostButtonProps) => {
-  const queryCostEstimate = useSelector<
-    SqlLabRootState,
-    QueryCostEstimate | undefined
-  >(state => state.sqlLab.queryCostEstimates?.[queryEditor.id]);
-  const selectedText = useSelector<SqlLabRootState, string | undefined>(
-    rootState =>
-      (getUpToDateQuery(rootState, queryEditor) as unknown as QueryEditor)
-        .selectedText,
-  );
-  const { cost } = queryCostEstimate || {};
+  const { cost } = queryCostEstimate;
   const tableData = useMemo(() => (Array.isArray(cost) ? cost : []), [cost]);
   const columns = useMemo(
     () =>
@@ -71,16 +57,16 @@ const EstimateQueryCostButton = ({
   };
 
   const renderModalBody = () => {
-    if (queryCostEstimate?.error) {
+    if (queryCostEstimate.error !== null) {
       return (
         <Alert
           key="query-estimate-error"
           type="error"
-          message={queryCostEstimate?.error}
+          message={queryCostEstimate.error}
         />
       );
     }
-    if (queryCostEstimate?.completed) {
+    if (queryCostEstimate.completed) {
       return (
         <TableView
           columns={columns}
diff --git a/superset-frontend/src/SqlLab/components/QueryLimitSelect/QueryLimitSelect.test.tsx b/superset-frontend/src/SqlLab/components/QueryLimitSelect/QueryLimitSelect.test.tsx
deleted file mode 100644
index 8b3d1b7f09..0000000000
--- a/superset-frontend/src/SqlLab/components/QueryLimitSelect/QueryLimitSelect.test.tsx
+++ /dev/null
@@ -1,137 +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 configureStore from 'redux-mock-store';
-import thunk from 'redux-thunk';
-import { Store } from 'redux';
-
-import { render, fireEvent, waitFor } from 'spec/helpers/testing-library';
-import userEvent from '@testing-library/user-event';
-import { initialState, defaultQueryEditor } from 'src/SqlLab/fixtures';
-import QueryLimitSelect, {
-  LIMIT_DROPDOWN,
-  QueryLimitSelectProps,
-  convertToNumWithSpaces,
-} from 'src/SqlLab/components/QueryLimitSelect';
-
-const middlewares = [thunk];
-const mockStore = configureStore(middlewares);
-
-jest.mock('src/components/Select', () => () => (
-  <div data-test="mock-deprecated-select" />
-));
-jest.mock('src/components/Select/Select', () => () => (
-  <div data-test="mock-deprecated-select-select" />
-));
-jest.mock('src/components/Select/AsyncSelect', () => () => (
-  <div data-test="mock-deprecated-async-select" />
-));
-jest.mock('src/components/Icons/Icon', () => () => (
-  <div data-test="mock-icons-icon" />
-));
-
-const defaultQueryLimit = 100;
-
-const setup = (props?: Partial<QueryLimitSelectProps>, store?: Store) =>
-  render(
-    <QueryLimitSelect
-      queryEditor={defaultQueryEditor}
-      maxRow={100000}
-      defaultQueryLimit={defaultQueryLimit}
-      {...props}
-    />,
-    {
-      useRedux: true,
-      ...(store && { store }),
-    },
-  );
-
-describe('QueryLimitSelect', () => {
-  it('renders current query limit size', () => {
-    const queryLimit = 10;
-    const { getByText } = setup(
-      {
-        queryEditor: {
-          ...defaultQueryEditor,
-          queryLimit,
-        },
-      },
-      mockStore(initialState),
-    );
-    expect(getByText(queryLimit)).toBeInTheDocument();
-  });
-
-  it('renders default query limit for initial queryEditor', () => {
-    const { getByText } = setup({}, mockStore(initialState));
-    expect(getByText(defaultQueryLimit)).toBeInTheDocument();
-  });
-
-  it('renders queryLimit from unsavedQueryEditor', () => {
-    const queryLimit = 10000;
-    const { getByText } = setup(
-      {},
-      mockStore({
-        ...initialState,
-        sqlLab: {
-          ...initialState.sqlLab,
-          unsavedQueryEditor: {
-            id: defaultQueryEditor.id,
-            queryLimit,
-          },
-        },
-      }),
-    );
-    expect(getByText(convertToNumWithSpaces(queryLimit))).toBeInTheDocument();
-  });
-
-  it('renders dropdown select', async () => {
-    const { baseElement, getByRole } = setup({}, mockStore(initialState));
-    const dropdown = baseElement.getElementsByClassName(
-      'ant-dropdown-trigger',
-    )[0];
-
-    userEvent.click(dropdown);
-    await waitFor(() => expect(getByRole('menu')).toBeInTheDocument());
-  });
-
-  it('dispatches QUERY_EDITOR_SET_QUERY_LIMIT action on dropdown menu click', async () => {
-    const store = mockStore(initialState);
-    const expectedIndex = 1;
-    const { baseElement, getAllByRole, getByRole } = setup({}, store);
-    const dropdown = baseElement.getElementsByClassName(
-      'ant-dropdown-trigger',
-    )[0];
-
-    userEvent.click(dropdown);
-    await waitFor(() => expect(getByRole('menu')).toBeInTheDocument());
-
-    const menu = getAllByRole('menuitem')[expectedIndex];
-    expect(store.getActions()).toEqual([]);
-    fireEvent.click(menu);
-    await waitFor(() =>
-      expect(store.getActions()).toEqual([
-        {
-          type: 'QUERY_EDITOR_SET_QUERY_LIMIT',
-          queryLimit: LIMIT_DROPDOWN[expectedIndex],
-          queryEditor: defaultQueryEditor,
-        },
-      ]),
-    );
-  });
-});
diff --git a/superset-frontend/src/SqlLab/components/QueryLimitSelect/index.tsx b/superset-frontend/src/SqlLab/components/QueryLimitSelect/index.tsx
deleted file mode 100644
index f438ebc59e..0000000000
--- a/superset-frontend/src/SqlLab/components/QueryLimitSelect/index.tsx
+++ /dev/null
@@ -1,118 +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 { useSelector, useDispatch } from 'react-redux';
-import { styled, useTheme } from '@superset-ui/core';
-import { AntdDropdown } from 'src/components';
-import { Menu } from 'src/components/Menu';
-import Icons from 'src/components/Icons';
-import { SqlLabRootState, QueryEditor } from 'src/SqlLab/types';
-import { queryEditorSetQueryLimit } from 'src/SqlLab/actions/sqlLab';
-
-export interface QueryLimitSelectProps {
-  queryEditor: QueryEditor;
-  maxRow: number;
-  defaultQueryLimit: number;
-}
-
-export const LIMIT_DROPDOWN = [10, 100, 1000, 10000, 100000];
-
-export function convertToNumWithSpaces(num: number) {
-  return num.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1 ');
-}
-
-const LimitSelectStyled = styled.span`
-  ${({ theme }) => `
-    .ant-dropdown-trigger {
-      align-items: center;
-      color: ${theme.colors.grayscale.dark2};
-      display: flex;
-      font-size: 12px;
-      margin-right: ${theme.gridUnit * 2}px;
-      text-decoration: none;
-      border: 0;
-      background: transparent;
-      span {
-        display: inline-block;
-        margin-right: ${theme.gridUnit * 2}px;
-        &:last-of-type: {
-          margin-right: ${theme.gridUnit * 4}px;
-        }
-      }
-    }
-  `}
-`;
-
-function renderQueryLimit(
-  maxRow: number,
-  setQueryLimit: (limit: number) => void,
-) {
-  // Adding SQL_MAX_ROW value to dropdown
-  LIMIT_DROPDOWN.push(maxRow);
-
-  return (
-    <Menu>
-      {[...new Set(LIMIT_DROPDOWN)].map(limit => (
-        <Menu.Item key={`${limit}`} onClick={() => setQueryLimit(limit)}>
-          {/* // eslint-disable-line no-use-before-define */}
-          <a role="button">{convertToNumWithSpaces(limit)}</a>{' '}
-        </Menu.Item>
-      ))}
-    </Menu>
-  );
-}
-
-const QueryLimitSelect = ({
-  queryEditor,
-  maxRow,
-  defaultQueryLimit,
-}: QueryLimitSelectProps) => {
-  const queryLimit = useSelector<SqlLabRootState, number>(
-    ({ sqlLab: { unsavedQueryEditor } }) => {
-      const updatedQueryEditor = {
-        ...queryEditor,
-        ...(unsavedQueryEditor.id === queryEditor.id && unsavedQueryEditor),
-      };
-      return updatedQueryEditor.queryLimit || defaultQueryLimit;
-    },
-  );
-  const dispatch = useDispatch();
-  const setQueryLimit = (updatedQueryLimit: number) =>
-    dispatch(queryEditorSetQueryLimit(queryEditor, updatedQueryLimit));
-  const theme = useTheme();
-
-  return (
-    <LimitSelectStyled>
-      <AntdDropdown
-        overlay={renderQueryLimit(maxRow, setQueryLimit)}
-        trigger={['click']}
-      >
-        <button type="button" onClick={e => e.preventDefault()}>
-          <span>LIMIT:</span>
-          <span className="limitDropdown">
-            {convertToNumWithSpaces(queryLimit)}
-          </span>
-          <Icons.TriangleDown iconColor={theme.colors.grayscale.base} />
-        </button>
-      </AntdDropdown>
-    </LimitSelectStyled>
-  );
-};
-
-export default QueryLimitSelect;
diff --git a/superset-frontend/src/SqlLab/components/RunQueryActionButton/RunQueryActionButton.test.jsx b/superset-frontend/src/SqlLab/components/RunQueryActionButton/RunQueryActionButton.test.jsx
new file mode 100644
index 0000000000..823f10741a
--- /dev/null
+++ b/superset-frontend/src/SqlLab/components/RunQueryActionButton/RunQueryActionButton.test.jsx
@@ -0,0 +1,53 @@
+/**
+ * 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 { mount } from 'enzyme';
+import { supersetTheme, ThemeProvider } from '@superset-ui/core';
+import RunQueryActionButton from 'src/SqlLab/components/RunQueryActionButton';
+import Button from 'src/components/Button';
+
+describe('RunQueryActionButton', () => {
+  let wrapper;
+  const defaultProps = {
+    allowAsync: false,
+    dbId: 1,
+    queryState: 'pending',
+    runQuery: () => {}, // eslint-disable-line
+    selectedText: null,
+    stopQuery: () => {}, // eslint-disable-line
+    sql: '',
+  };
+
+  beforeEach(() => {
+    wrapper = mount(<RunQueryActionButton {...defaultProps} />, {
+      wrappingComponent: ThemeProvider,
+      wrappingComponentProps: { theme: supersetTheme },
+    });
+  });
+
+  it('is a valid react element', () => {
+    expect(
+      React.isValidElement(<RunQueryActionButton {...defaultProps} />),
+    ).toBe(true);
+  });
+
+  it('renders a single Button', () => {
+    expect(wrapper.find(Button)).toExist();
+  });
+});
diff --git a/superset-frontend/src/SqlLab/components/RunQueryActionButton/RunQueryActionButton.test.tsx b/superset-frontend/src/SqlLab/components/RunQueryActionButton/RunQueryActionButton.test.tsx
deleted file mode 100644
index 7802989689..0000000000
--- a/superset-frontend/src/SqlLab/components/RunQueryActionButton/RunQueryActionButton.test.tsx
+++ /dev/null
@@ -1,151 +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 configureStore from 'redux-mock-store';
-import thunk from 'redux-thunk';
-import { Store } from 'redux';
-
-import { render, fireEvent, waitFor } from 'spec/helpers/testing-library';
-import { initialState, defaultQueryEditor } from 'src/SqlLab/fixtures';
-import RunQueryActionButton, {
-  Props,
-} from 'src/SqlLab/components/RunQueryActionButton';
-
-const middlewares = [thunk];
-const mockStore = configureStore(middlewares);
-
-jest.mock('src/components/Select', () => () => (
-  <div data-test="mock-deprecated-select" />
-));
-jest.mock('src/components/Select/Select', () => () => (
-  <div data-test="mock-deprecated-select-select" />
-));
-jest.mock('src/components/Select/AsyncSelect', () => () => (
-  <div data-test="mock-deprecated-async-select" />
-));
-
-const defaultProps = {
-  queryEditor: defaultQueryEditor,
-  allowAsync: false,
-  dbId: 1,
-  queryState: 'ready',
-  runQuery: jest.fn(),
-  selectedText: null,
-  stopQuery: jest.fn(),
-  overlayCreateAsMenu: null,
-};
-
-const setup = (props?: Partial<Props>, store?: Store) =>
-  render(<RunQueryActionButton {...defaultProps} {...props} />, {
-    useRedux: true,
-    ...(store && { store }),
-  });
-
-describe('RunQueryActionButton', () => {
-  beforeEach(() => {
-    defaultProps.runQuery.mockReset();
-    defaultProps.stopQuery.mockReset();
-  });
-
-  it('renders a single Button', () => {
-    const { getByRole } = setup({}, mockStore(initialState));
-    expect(getByRole('button')).toBeInTheDocument();
-  });
-
-  it('renders a label for Run Query', () => {
-    const { getByText } = setup({}, mockStore(initialState));
-    expect(getByText('Run')).toBeInTheDocument();
-  });
-
-  it('renders a label for Selected Query', () => {
-    const { getByText } = setup(
-      {},
-      mockStore({
-        ...initialState,
-        sqlLab: {
-          ...initialState.sqlLab,
-          unsavedQueryEditor: {
-            id: defaultQueryEditor.id,
-            selectedText: 'FROM',
-          },
-        },
-      }),
-    );
-    expect(getByText('Run selection')).toBeInTheDocument();
-  });
-
-  it('disable button when sql from unsaved changes is empty', () => {
-    const { getByRole } = setup(
-      {},
-      mockStore({
-        ...initialState,
-        sqlLab: {
-          ...initialState.sqlLab,
-          unsavedQueryEditor: {
-            id: defaultQueryEditor.id,
-            sql: '',
-          },
-        },
-      }),
-    );
-    const button = getByRole('button');
-    expect(button).toBeDisabled();
-  });
-
-  it('enable default button for unrelated unsaved changes', () => {
-    const { getByRole } = setup(
-      {},
-      mockStore({
-        ...initialState,
-        sqlLab: {
-          ...initialState.sqlLab,
-          unsavedQueryEditor: {
-            id: `${defaultQueryEditor.id}-other`,
-            sql: '',
-          },
-        },
-      }),
-    );
-    const button = getByRole('button');
-    expect(button).toBeEnabled();
-  });
-
-  it('dispatch runQuery on click', async () => {
-    const { getByRole } = setup({}, mockStore(initialState));
-    const button = getByRole('button');
-    expect(defaultProps.runQuery).toHaveBeenCalledTimes(0);
-    fireEvent.click(button);
-    await waitFor(() => expect(defaultProps.runQuery).toHaveBeenCalledTimes(1));
-  });
-
-  describe('on running state', () => {
-    it('dispatch stopQuery on click', async () => {
-      const { getByRole } = setup(
-        { queryState: 'running' },
-        mockStore(initialState),
-      );
-      const button = getByRole('button');
-      expect(defaultProps.stopQuery).toHaveBeenCalledTimes(0);
-      fireEvent.click(button);
-      await waitFor(() =>
-        expect(defaultProps.stopQuery).toHaveBeenCalledTimes(1),
-      );
-    });
-  });
-});
diff --git a/superset-frontend/src/SqlLab/components/RunQueryActionButton/index.tsx b/superset-frontend/src/SqlLab/components/RunQueryActionButton/index.tsx
index 5cc453f5ee..19bf778756 100644
--- a/superset-frontend/src/SqlLab/components/RunQueryActionButton/index.tsx
+++ b/superset-frontend/src/SqlLab/components/RunQueryActionButton/index.tsx
@@ -24,20 +24,15 @@ import Button from 'src/components/Button';
 import Icons from 'src/components/Icons';
 import { DropdownButton } from 'src/components/DropdownButton';
 import { detectOS } from 'src/utils/common';
-import { shallowEqual, useSelector } from 'react-redux';
-import {
-  QueryEditor,
-  SqlLabRootState,
-  QueryButtonProps,
-} from 'src/SqlLab/types';
-import { getUpToDateQuery } from 'src/SqlLab/actions/sqlLab';
+import { QueryButtonProps } from 'src/SqlLab/types';
 
-export interface Props {
-  queryEditor: QueryEditor;
+interface Props {
   allowAsync: boolean;
   queryState?: string;
   runQuery: (c?: boolean) => void;
+  selectedText?: string;
   stopQuery: () => void;
+  sql: string;
   overlayCreateAsMenu: typeof Menu | null;
 }
 
@@ -88,27 +83,16 @@ const StyledButton = styled.span`
 
 const RunQueryActionButton = ({
   allowAsync = false,
-  queryEditor,
   queryState,
+  selectedText,
+  sql = '',
   overlayCreateAsMenu,
   runQuery,
   stopQuery,
 }: Props) => {
   const theme = useTheme();
+
   const userOS = detectOS();
-  const { selectedText, sql } = useSelector<
-    SqlLabRootState,
-    Pick<QueryEditor, 'selectedText' | 'sql'>
-  >(rootState => {
-    const currentQueryEditor = getUpToDateQuery(
-      rootState,
-      queryEditor,
-    ) as unknown as QueryEditor;
-    return {
-      selectedText: currentQueryEditor.selectedText,
-      sql: currentQueryEditor.sql,
-    };
-  }, shallowEqual);
 
   const shouldShowStopBtn =
     !!queryState && ['running', 'pending'].indexOf(queryState) > -1;
@@ -117,7 +101,7 @@ const RunQueryActionButton = ({
     ? (DropdownButton as React.FC)
     : Button;
 
-  const isDisabled = !sql || !sql.trim();
+  const isDisabled = !sql.trim();
 
   const stopButtonTooltipText = useMemo(
     () =>
diff --git a/superset-frontend/src/SqlLab/components/SaveQuery/SaveQuery.test.jsx b/superset-frontend/src/SqlLab/components/SaveQuery/SaveQuery.test.jsx
index 2a5fcf3eb7..94b7e2780b 100644
--- a/superset-frontend/src/SqlLab/components/SaveQuery/SaveQuery.test.jsx
+++ b/superset-frontend/src/SqlLab/components/SaveQuery/SaveQuery.test.jsx
@@ -17,19 +17,18 @@
  * under the License.
  */
 import React from 'react';
-import configureStore from 'redux-mock-store';
-import thunk from 'redux-thunk';
 import { render, screen, waitFor } from 'spec/helpers/testing-library';
 import userEvent from '@testing-library/user-event';
 import SaveQuery from 'src/SqlLab/components/SaveQuery';
-import { initialState, databases } from 'src/SqlLab/fixtures';
+import { databases } from 'src/SqlLab/fixtures';
 
 const mockedProps = {
-  queryEditor: {
+  query: {
     dbId: 1,
     schema: 'main',
     sql: 'SELECT * FROM t',
   },
+  defaultLabel: 'untitled',
   animation: false,
   database: databases.result[0],
   onUpdate: () => {},
@@ -44,15 +43,9 @@ const splitSaveBtnProps = {
   },
 };
 
-const middlewares = [thunk];
-const mockStore = configureStore(middlewares);
-
 describe('SavedQuery', () => {
   it('renders a non-split save button when allows_virtual_table_explore is not enabled', () => {
-    render(<SaveQuery {...mockedProps} />, {
-      useRedux: true,
-      store: mockStore(initialState),
-    });
+    render(<SaveQuery {...mockedProps} />, { useRedux: true });
 
     const saveBtn = screen.getByRole('button', { name: /save/i });
 
@@ -60,10 +53,7 @@ describe('SavedQuery', () => {
   });
 
   it('renders a save query modal when user clicks save button', () => {
-    render(<SaveQuery {...mockedProps} />, {
-      useRedux: true,
-      store: mockStore(initialState),
-    });
+    render(<SaveQuery {...mockedProps} />, { useRedux: true });
 
     const saveBtn = screen.getByRole('button', { name: /save/i });
     userEvent.click(saveBtn);
@@ -76,10 +66,7 @@ describe('SavedQuery', () => {
   });
 
   it('renders the save query modal UI', () => {
-    render(<SaveQuery {...mockedProps} />, {
-      useRedux: true,
-      store: mockStore(initialState),
-    });
+    render(<SaveQuery {...mockedProps} />, { useRedux: true });
 
     const saveBtn = screen.getByRole('button', { name: /save/i });
     userEvent.click(saveBtn);
@@ -113,15 +100,12 @@ describe('SavedQuery', () => {
   it('renders a "save as new" and "update" button if query already exists', () => {
     const props = {
       ...mockedProps,
-      queryEditor: {
+      query: {
         ...mockedProps.query,
         remoteId: '42',
       },
     };
-    render(<SaveQuery {...props} />, {
-      useRedux: true,
-      store: mockStore(initialState),
-    });
+    render(<SaveQuery {...props} />, { useRedux: true });
 
     const saveBtn = screen.getByRole('button', { name: /save/i });
     userEvent.click(saveBtn);
@@ -134,10 +118,7 @@ describe('SavedQuery', () => {
   });
 
   it('renders a split save button when allows_virtual_table_explore is enabled', async () => {
-    render(<SaveQuery {...splitSaveBtnProps} />, {
-      useRedux: true,
-      store: mockStore(initialState),
-    });
+    render(<SaveQuery {...splitSaveBtnProps} />, { useRedux: true });
 
     await waitFor(() => {
       const saveBtn = screen.getByRole('button', { name: /save/i });
@@ -149,10 +130,7 @@ describe('SavedQuery', () => {
   });
 
   it('renders a save dataset modal when user clicks "save dataset" menu item', async () => {
-    render(<SaveQuery {...splitSaveBtnProps} />, {
-      useRedux: true,
-      store: mockStore(initialState),
-    });
+    render(<SaveQuery {...splitSaveBtnProps} />, { useRedux: true });
 
     await waitFor(() => {
       const caretBtn = screen.getByRole('button', { name: /caret-down/i });
@@ -168,10 +146,7 @@ describe('SavedQuery', () => {
   });
 
   it('renders the save dataset modal UI', async () => {
-    render(<SaveQuery {...splitSaveBtnProps} />, {
-      useRedux: true,
-      store: mockStore(initialState),
-    });
+    render(<SaveQuery {...splitSaveBtnProps} />, { useRedux: true });
 
     await waitFor(() => {
       const caretBtn = screen.getByRole('button', { name: /caret-down/i });
diff --git a/superset-frontend/src/SqlLab/components/SaveQuery/index.tsx b/superset-frontend/src/SqlLab/components/SaveQuery/index.tsx
index 38cd5625ec..554514f4db 100644
--- a/superset-frontend/src/SqlLab/components/SaveQuery/index.tsx
+++ b/superset-frontend/src/SqlLab/components/SaveQuery/index.tsx
@@ -17,7 +17,6 @@
  * under the License.
  */
 import React, { useState, useEffect } from 'react';
-import { useSelector, shallowEqual } from 'react-redux';
 import { Row, Col } from 'src/components';
 import { Input, TextArea } from 'src/components/Input';
 import { t, styled } from '@superset-ui/core';
@@ -26,16 +25,12 @@ import { Menu } from 'src/components/Menu';
 import { Form, FormItem } from 'src/components/Form';
 import Modal from 'src/components/Modal';
 import SaveDatasetActionButton from 'src/SqlLab/components/SaveDatasetActionButton';
-import {
-  SaveDatasetModal,
-  ISaveableDatasource,
-} from 'src/SqlLab/components/SaveDatasetModal';
+import { SaveDatasetModal } from 'src/SqlLab/components/SaveDatasetModal';
 import { getDatasourceAsSaveableDataset } from 'src/utils/datasourceUtils';
-import { QueryEditor, SqlLabRootState } from 'src/SqlLab/types';
 
 interface SaveQueryProps {
-  queryEditor: QueryEditor;
-  columns: ISaveableDatasource['columns'];
+  query: QueryPayload;
+  defaultLabel: string;
   onSave: (arg0: QueryPayload) => void;
   onUpdate: (arg0: QueryPayload) => void;
   saveQueryWarning: string | null;
@@ -81,22 +76,13 @@ const Styles = styled.span`
 `;
 
 export default function SaveQuery({
-  queryEditor,
+  query,
+  defaultLabel = t('Undefined'),
   onSave = () => {},
   onUpdate,
   saveQueryWarning = null,
   database,
-  columns,
 }: SaveQueryProps) {
-  const query = useSelector<SqlLabRootState, QueryEditor>(
-    ({ sqlLab: { unsavedQueryEditor } }) => ({
-      ...queryEditor,
-      ...(queryEditor.id === unsavedQueryEditor.id && unsavedQueryEditor),
-      columns,
-    }),
-    shallowEqual,
-  );
-  const defaultLabel = query.name || query.description || t('Undefined');
   const [description, setDescription] = useState<string>(
     query.description || '',
   );
@@ -114,12 +100,11 @@ export default function SaveQuery({
     </Menu>
   );
 
-  const queryPayload = () =>
-    ({
-      ...query,
-      name: label,
-      description,
-    } as any as QueryPayload);
+  const queryPayload = () => ({
+    ...query,
+    name: label,
+    description,
+  });
 
   useEffect(() => {
     if (!isSaved) setLabel(defaultLabel);
diff --git a/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.jsx b/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.jsx
index 735264b957..d946c675cc 100644
--- a/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.jsx
+++ b/superset-frontend/src/SqlLab/components/SqlEditor/SqlEditor.test.jsx
@@ -40,12 +40,7 @@ import {
 } from 'src/SqlLab/actions/sqlLab';
 import { EmptyStateBig } from 'src/components/EmptyState';
 import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
-import {
-  initialState,
-  queries,
-  table,
-  defaultQueryEditor,
-} from 'src/SqlLab/fixtures';
+import { initialState, queries, table } from 'src/SqlLab/fixtures';
 
 const MOCKED_SQL_EDITOR_HEIGHT = 500;
 
@@ -53,31 +48,7 @@ fetchMock.get('glob:*/api/v1/database/*', { result: [] });
 
 const middlewares = [thunk];
 const mockStore = configureStore(middlewares);
-const store = mockStore({
-  ...initialState,
-  sqlLab: {
-    ...initialState.sqlLab,
-    databases: {
-      dbid1: {
-        allow_ctas: false,
-        allow_cvas: false,
-        allow_dml: false,
-        allow_file_upload: false,
-        allow_multi_schema_metadata_fetch: false,
-        allow_run_async: false,
-        backend: 'postgresql',
-        database_name: 'examples',
-        expose_in_sqllab: true,
-        force_ctas_schema: null,
-        id: 1,
-      },
-    },
-    unsavedQueryEditor: {
-      id: defaultQueryEditor.id,
-      dbId: 'dbid1',
-    },
-  },
-});
+const store = mockStore(initialState);
 
 describe('SqlEditor', () => {
   const mockedProps = {
@@ -86,9 +57,21 @@ describe('SqlEditor', () => {
       queryEditorSetSelectedText,
       queryEditorSetSchemaOptions,
       addDangerToast: jest.fn(),
-      removeDataPreview: jest.fn(),
     },
-    queryEditor: initialState.sqlLab.queryEditors[0],
+    database: {
+      allow_ctas: false,
+      allow_cvas: false,
+      allow_dml: false,
+      allow_file_upload: false,
+      allow_multi_schema_metadata_fetch: false,
+      allow_run_async: false,
+      backend: 'postgresql',
+      database_name: 'examples',
+      expose_in_sqllab: true,
+      force_ctas_schema: null,
+      id: 1,
+    },
+    queryEditorId: initialState.sqlLab.queryEditors[0].id,
     latestQuery: queries[0],
     tables: [table],
     getHeight: () => '100px',
@@ -111,8 +94,8 @@ describe('SqlEditor', () => {
     );
 
   it('does not render SqlEditor if no db selected', () => {
-    const queryEditor = initialState.sqlLab.queryEditors[1];
-    const updatedProps = { ...mockedProps, queryEditor };
+    const database = {};
+    const updatedProps = { ...mockedProps, database };
     const wrapper = buildWrapper(updatedProps);
     expect(wrapper.find(EmptyStateBig)).toExist();
   });
diff --git a/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx b/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx
index d813b3b52d..bdda237158 100644
--- a/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx
+++ b/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx
@@ -43,10 +43,10 @@ import {
   persistEditorHeight,
   postStopQuery,
   queryEditorSetAutorun,
+  queryEditorSetQueryLimit,
   queryEditorSetSql,
   queryEditorSetAndSaveSql,
   queryEditorSetTemplateParams,
-  runQueryFromSqlEditor,
   runQuery,
   saveQuery,
   addSavedQueryToTabState,
@@ -79,8 +79,8 @@ import SqlEditorLeftBar from '../SqlEditorLeftBar';
 import AceEditorWrapper from '../AceEditorWrapper';
 import RunQueryActionButton from '../RunQueryActionButton';
 import { newQueryTabName } from '../../utils/newQueryTabName';
-import QueryLimitSelect from '../QueryLimitSelect';
 
+const LIMIT_DROPDOWN = [10, 100, 1000, 10000, 100000];
 const SQL_EDITOR_PADDING = 10;
 const INITIAL_NORTH_PERCENT = 30;
 const INITIAL_SOUTH_PERCENT = 70;
@@ -96,6 +96,26 @@ const validatorMap =
   bootstrapData?.common?.conf?.SQL_VALIDATORS_BY_ENGINE || {};
 const scheduledQueriesConf = bootstrapData?.common?.conf?.SCHEDULED_QUERIES;
 
+const LimitSelectStyled = styled.span`
+  ${({ theme }) => `
+    .ant-dropdown-trigger {
+      align-items: center;
+      color: ${theme.colors.grayscale.dark2};
+      display: flex;
+      font-size: 12px;
+      margin-right: ${theme.gridUnit * 2}px;
+      text-decoration: none;
+      span {
+        display: inline-block;
+        margin-right: ${theme.gridUnit * 2}px;
+        &:last-of-type: {
+          margin-right: ${theme.gridUnit * 4}px;
+        }
+      }
+    }
+  `}
+`;
+
 const StyledToolbar = styled.div`
   padding: ${({ theme }) => theme.gridUnit * 2}px;
   background: ${({ theme }) => theme.colors.grayscale.light5};
@@ -134,7 +154,7 @@ const propTypes = {
   tables: PropTypes.array.isRequired,
   editorQueries: PropTypes.array.isRequired,
   dataPreviewQueries: PropTypes.array.isRequired,
-  queryEditor: PropTypes.object.isRequired,
+  queryEditorId: PropTypes.string.isRequired,
   hideLeftBar: PropTypes.bool,
   defaultQueryLimit: PropTypes.number.isRequired,
   maxRow: PropTypes.number.isRequired,
@@ -185,6 +205,7 @@ class SqlEditor extends React.PureComponent {
     );
     this.queryPane = this.queryPane.bind(this);
     this.getHotkeyConfig = this.getHotkeyConfig.bind(this);
+    this.renderQueryLimit = this.renderQueryLimit.bind(this);
     this.getAceEditorAndSouthPaneHeights =
       this.getAceEditorAndSouthPaneHeights.bind(this);
     this.getSqlEditorHeight = this.getSqlEditorHeight.bind(this);
@@ -361,10 +382,21 @@ class SqlEditor extends React.PureComponent {
     this.props.queryEditorSetAndSaveSql(this.props.queryEditor, sql);
   }
 
+  setQueryLimit(queryLimit) {
+    this.props.queryEditorSetQueryLimit(this.props.queryEditor, queryLimit);
+  }
+
   getQueryCostEstimate() {
     if (this.props.database) {
       const qe = this.props.queryEditor;
-      this.props.estimateQueryCost(qe);
+      const query = {
+        dbId: qe.dbId,
+        sql: qe.selectedText ? qe.selectedText : this.props.queryEditor.sql,
+        sqlEditorId: qe.id,
+        schema: qe.schema,
+        templateParams: qe.templateParams,
+      };
+      this.props.estimateQueryCost(query);
     }
   }
 
@@ -393,9 +425,16 @@ class SqlEditor extends React.PureComponent {
   }
 
   requestValidation(sql) {
-    const { database, queryEditor, validateQuery } = this.props;
-    if (database) {
-      validateQuery(queryEditor, sql);
+    if (this.props.database) {
+      const qe = this.props.queryEditor;
+      const query = {
+        dbId: qe.dbId,
+        sql,
+        sqlEditorId: qe.id,
+        schema: qe.schema,
+        templateParams: qe.templateParams,
+      };
+      this.props.validateQuery(query);
     }
   }
 
@@ -419,22 +458,25 @@ class SqlEditor extends React.PureComponent {
   }
 
   startQuery(ctas = false, ctas_method = CtasEnum.TABLE) {
-    const {
-      database,
-      runQueryFromSqlEditor,
-      setActiveSouthPaneTab,
-      queryEditor,
-      defaultQueryLimit,
-    } = this.props;
-    runQueryFromSqlEditor(
-      database,
-      queryEditor,
-      defaultQueryLimit,
-      ctas ? this.state.ctas : '',
+    const qe = this.props.queryEditor;
+    const query = {
+      dbId: qe.dbId,
+      sql: qe.selectedText ? qe.selectedText : qe.sql,
+      sqlEditorId: qe.id,
+      tab: qe.name,
+      schema: qe.schema,
+      tempTable: ctas ? this.state.ctas : '',
+      templateParams: qe.templateParams,
+      queryLimit: qe.queryLimit || this.props.defaultQueryLimit,
+      runAsync: this.props.database
+        ? this.props.database.allow_run_async
+        : false,
       ctas,
       ctas_method,
-    );
-    setActiveSouthPaneTab('Results');
+      updateTabState: !qe.selectedText,
+    };
+    this.props.runQuery(query);
+    this.props.setActiveSouthPaneTab('Results');
   }
 
   stopQuery() {
@@ -487,7 +529,11 @@ class SqlEditor extends React.PureComponent {
             onBlur={this.setQueryEditorSql}
             onChange={this.onSqlChanged}
             queryEditor={this.props.queryEditor}
+            sql={this.props.queryEditor.sql}
             database={this.props.database}
+            schemas={this.props.queryEditor.schemaOptions}
+            tables={this.props.queryEditor.tableOptions}
+            functionNames={this.props.queryEditor.functionNames}
             extendedTables={this.props.tables}
             height={`${aceEditorHeight}px`}
             hotkeys={hotkeys}
@@ -531,7 +577,7 @@ class SqlEditor extends React.PureComponent {
               onChange={params => {
                 this.props.actions.queryEditorSetTemplateParams(qe, params);
               }}
-              queryEditor={qe}
+              code={qe.templateParams}
             />
           </Menu.Item>
         )}
@@ -553,6 +599,25 @@ class SqlEditor extends React.PureComponent {
     );
   }
 
+  renderQueryLimit() {
+    // Adding SQL_MAX_ROW value to dropdown
+    const { maxRow } = this.props;
+    LIMIT_DROPDOWN.push(maxRow);
+
+    return (
+      <Menu>
+        {[...new Set(LIMIT_DROPDOWN)].map(limit => (
+          <Menu.Item key={`${limit}`} onClick={() => this.setQueryLimit(limit)}>
+            {/* // eslint-disable-line no-use-before-define */}
+            <a role="button" styling="link">
+              {this.convertToNumWithSpaces(limit)}
+            </a>{' '}
+          </Menu.Item>
+        ))}
+      </Menu>
+    );
+  }
+
   async saveQuery(query) {
     const { queryEditor: qe, actions } = this.props;
     const savedQuery = await actions.saveQuery(query);
@@ -608,10 +673,11 @@ class SqlEditor extends React.PureComponent {
                   ? this.props.database.allow_run_async
                   : false
               }
-              queryEditor={qe}
               queryState={this.props.latestQuery?.state}
               runQuery={this.runQuery}
+              selectedText={qe.selectedText}
               stopQuery={this.stopQuery}
+              sql={this.props.queryEditor.sql}
               overlayCreateAsMenu={showMenu ? runMenuBtn : null}
             />
           </span>
@@ -621,17 +687,27 @@ class SqlEditor extends React.PureComponent {
               <span>
                 <EstimateQueryCostButton
                   getEstimate={this.getQueryCostEstimate}
-                  queryEditor={qe}
+                  queryCostEstimate={qe.queryCostEstimate}
+                  selectedText={qe.selectedText}
                   tooltip={t('Estimate the cost before running a query')}
                 />
               </span>
             )}
           <span>
-            <QueryLimitSelect
-              queryEditor={this.props.queryEditor}
-              maxRow={this.props.maxRow}
-              defaultQueryLimit={this.props.defaultQueryLimit}
-            />
+            <LimitSelectStyled>
+              <AntdDropdown overlay={this.renderQueryLimit()} trigger="click">
+                <a onClick={e => e.preventDefault()}>
+                  <span>LIMIT:</span>
+                  <span className="limitDropdown">
+                    {this.convertToNumWithSpaces(
+                      this.props.queryEditor.queryLimit ||
+                        this.props.defaultQueryLimit,
+                    )}
+                  </span>
+                  <Icons.TriangleDown iconColor={theme.colors.grayscale.base} />
+                </a>
+              </AntdDropdown>
+            </LimitSelectStyled>
           </span>
           {this.props.latestQuery && (
             <Timer
@@ -645,8 +721,11 @@ class SqlEditor extends React.PureComponent {
         <div className="rightItems">
           <span>
             <SaveQuery
-              queryEditor={qe}
-              columns={this.props.latestQuery?.results?.columns || []}
+              query={{
+                ...qe,
+                columns: this.props.latestQuery?.results?.columns || [],
+              }}
+              defaultLabel={qe.name || qe.description}
               onSave={this.saveQuery}
               onUpdate={this.props.actions.updateSavedQuery}
               saveQueryWarning={this.props.saveQueryWarning}
@@ -753,22 +832,12 @@ class SqlEditor extends React.PureComponent {
 SqlEditor.defaultProps = defaultProps;
 SqlEditor.propTypes = propTypes;
 
-function mapStateToProps({ sqlLab }, { queryEditor }) {
-  let { latestQueryId, dbId } = queryEditor;
-  if (sqlLab.unsavedQueryEditor.id === queryEditor.id) {
-    const { latestQueryId: unsavedQID, dbId: unsavedDBID } =
-      sqlLab.unsavedQueryEditor;
-    latestQueryId = unsavedQID || latestQueryId;
-    dbId = unsavedDBID || dbId;
-  }
-  const database = sqlLab.databases[dbId];
-  const latestQuery = sqlLab.queries[latestQueryId];
-
-  return {
-    queryEditors: sqlLab.queryEditors,
-    latestQuery,
-    database,
-  };
+function mapStateToProps({ sqlLab }, props) {
+  const queryEditor = sqlLab.queryEditors.find(
+    editor => editor.id === props.queryEditorId,
+  );
+
+  return { sqlLab, ...props, queryEditor, queryEditors: sqlLab.queryEditors };
 }
 
 function mapDispatchToProps(dispatch) {
@@ -779,10 +848,10 @@ function mapDispatchToProps(dispatch) {
       persistEditorHeight,
       postStopQuery,
       queryEditorSetAutorun,
+      queryEditorSetQueryLimit,
       queryEditorSetSql,
       queryEditorSetAndSaveSql,
       queryEditorSetTemplateParams,
-      runQueryFromSqlEditor,
       runQuery,
       saveQuery,
       addSavedQueryToTabState,
diff --git a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx
index 97d80a4e88..fbe782534e 100644
--- a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx
+++ b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx
@@ -32,7 +32,7 @@ import Collapse from 'src/components/Collapse';
 import Icons from 'src/components/Icons';
 import { TableSelectorMultiple } from 'src/components/TableSelector';
 import { IconTooltip } from 'src/components/IconTooltip';
-import { QueryEditor, SchemaOption } from 'src/SqlLab/types';
+import { QueryEditor } from 'src/SqlLab/types';
 import { DatabaseObject } from 'src/components/DatabaseSelector';
 import { EmptyStateSmall } from 'src/components/EmptyState';
 import {
@@ -55,10 +55,7 @@ interface actionsTypes {
   setDatabases: (arg0: any) => {};
   addDangerToast: (msg: string) => void;
   queryEditorSetSchema: (queryEditor: QueryEditor, schema?: string) => void;
-  queryEditorSetSchemaOptions: (
-    queryEditor: QueryEditor,
-    options: SchemaOption[],
-  ) => void;
+  queryEditorSetSchemaOptions: () => void;
   queryEditorSetTableOptions: (
     queryEditor: QueryEditor,
     options: Array<any>,
@@ -73,6 +70,7 @@ interface SqlEditorLeftBarProps {
   actions: actionsTypes & TableElementProps['actions'];
   database: DatabaseObject;
   setEmptyState: Dispatch<SetStateAction<boolean>>;
+  showDisabled: boolean;
 }
 
 const StyledScrollbarContainer = styled.div`
@@ -241,15 +239,6 @@ export default function SqlEditorLeftBar({
     [actions],
   );
 
-  const handleSchemasLoad = React.useCallback(
-    (options: Array<any>) => {
-      if (queryEditorRef.current) {
-        actions.queryEditorSetSchemaOptions(queryEditorRef.current, options);
-      }
-    },
-    [actions],
-  );
-
   return (
     <div className="SqlEditorLeftBar">
       <TableSelectorMultiple
@@ -260,7 +249,7 @@ export default function SqlEditorLeftBar({
         handleError={actions.addDangerToast}
         onDbChange={onDbChange}
         onSchemaChange={handleSchemaChange}
-        onSchemasLoad={handleSchemasLoad}
+        onSchemasLoad={actions.queryEditorSetSchemaOptions}
         onTableSelectChange={onTablesChange}
         onTablesLoad={handleTablesLoad}
         schema={queryEditor.schema}
diff --git a/superset-frontend/src/SqlLab/components/SqlEditorTabHeader/SqlEditorTabHeader.test.tsx b/superset-frontend/src/SqlLab/components/SqlEditorTabHeader/SqlEditorTabHeader.test.tsx
deleted file mode 100644
index 5216129bdf..0000000000
--- a/superset-frontend/src/SqlLab/components/SqlEditorTabHeader/SqlEditorTabHeader.test.tsx
+++ /dev/null
@@ -1,220 +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 configureStore from 'redux-mock-store';
-import thunk from 'redux-thunk';
-import {
-  fireEvent,
-  screen,
-  render,
-  waitFor,
-} from 'spec/helpers/testing-library';
-import userEvent from '@testing-library/user-event';
-import { QueryEditor } from 'src/SqlLab/types';
-import {
-  initialState,
-  defaultQueryEditor,
-  extraQueryEditor1,
-  extraQueryEditor2,
-} from 'src/SqlLab/fixtures';
-import { Store } from 'redux';
-import {
-  REMOVE_QUERY_EDITOR,
-  QUERY_EDITOR_SET_TITLE,
-  ADD_QUERY_EDITOR,
-  QUERY_EDITOR_TOGGLE_LEFT_BAR,
-} from 'src/SqlLab/actions/sqlLab';
-import SqlEditorTabHeader from 'src/SqlLab/components/SqlEditorTabHeader';
-
-jest.mock('src/components/Select', () => () => (
-  <div data-test="mock-deprecated-select" />
-));
-jest.mock('src/components/Select/Select', () => () => (
-  <div data-test="mock-deprecated-select-select" />
-));
-jest.mock('src/components/Select/AsyncSelect', () => () => (
-  <div data-test="mock-async-select" />
-));
-
-const middlewares = [thunk];
-const mockStore = configureStore(middlewares);
-const setup = (queryEditor: QueryEditor, store?: Store) =>
-  render(<SqlEditorTabHeader queryEditor={queryEditor} />, {
-    useRedux: true,
-    ...(store && { store }),
-  });
-
-describe('SqlEditorTabHeader', () => {
-  it('renders name', () => {
-    const { queryByText } = setup(defaultQueryEditor, mockStore(initialState));
-    expect(queryByText(defaultQueryEditor.name)).toBeTruthy();
-    expect(queryByText(extraQueryEditor1.name)).toBeFalsy();
-    expect(queryByText(extraQueryEditor2.name)).toBeFalsy();
-  });
-
-  it('renders name from unsaved changes', () => {
-    const expectedTitle = 'updated title';
-    const { queryByText } = setup(
-      defaultQueryEditor,
-      mockStore({
-        ...initialState,
-        sqlLab: {
-          ...initialState.sqlLab,
-          unsavedQueryEditor: {
-            id: defaultQueryEditor.id,
-            name: expectedTitle,
-          },
-        },
-      }),
-    );
-    expect(queryByText(expectedTitle)).toBeTruthy();
-    expect(queryByText(defaultQueryEditor.name)).toBeFalsy();
-    expect(queryByText(extraQueryEditor1.name)).toBeFalsy();
-    expect(queryByText(extraQueryEditor2.name)).toBeFalsy();
-  });
-
-  it('renders current name for unrelated unsaved changes', () => {
-    const unrelatedTitle = 'updated title';
-    const { queryByText } = setup(
-      defaultQueryEditor,
-      mockStore({
-        ...initialState,
-        sqlLab: {
-          ...initialState.sqlLab,
-          unsavedQueryEditor: {
-            id: `${defaultQueryEditor.id}-other`,
-            name: unrelatedTitle,
-          },
-        },
-      }),
-    );
-    expect(queryByText(defaultQueryEditor.name)).toBeTruthy();
-    expect(queryByText(unrelatedTitle)).toBeFalsy();
-    expect(queryByText(extraQueryEditor1.name)).toBeFalsy();
-    expect(queryByText(extraQueryEditor2.name)).toBeFalsy();
-  });
-
-  describe('with dropdown menus', () => {
-    let store = mockStore();
-    beforeEach(async () => {
-      store = mockStore(initialState);
-      const { getByTestId } = setup(defaultQueryEditor, store);
-      const dropdown = getByTestId('dropdown-trigger');
-
-      userEvent.click(dropdown);
-    });
-
-    it('should dispatch removeQueryEditor action', async () => {
-      await waitFor(() =>
-        expect(screen.getByTestId('close-tab-menu-option')).toBeInTheDocument(),
-      );
-
-      fireEvent.click(screen.getByTestId('close-tab-menu-option'));
-
-      const actions = store.getActions();
-      await waitFor(() =>
-        expect(actions[0]).toEqual({
-          type: REMOVE_QUERY_EDITOR,
-          queryEditor: defaultQueryEditor,
-        }),
-      );
-    });
-
-    it('should dispatch queryEditorSetTitle action', async () => {
-      await waitFor(() =>
-        expect(screen.getByTestId('close-tab-menu-option')).toBeInTheDocument(),
-      );
-      const expectedTitle = 'typed text';
-      const mockPrompt = jest
-        .spyOn(window, 'prompt')
-        .mockImplementation(() => expectedTitle);
-      fireEvent.click(screen.getByTestId('rename-tab-menu-option'));
-
-      const actions = store.getActions();
-      await waitFor(() =>
-        expect(actions[0]).toEqual({
-          type: QUERY_EDITOR_SET_TITLE,
-          name: expectedTitle,
-          queryEditor: expect.objectContaining({
-            id: defaultQueryEditor.id,
-          }),
-        }),
-      );
-      mockPrompt.mockClear();
-    });
-
-    it('should dispatch toggleLeftBar action', async () => {
-      await waitFor(() =>
-        expect(screen.getByTestId('close-tab-menu-option')).toBeInTheDocument(),
-      );
-      fireEvent.click(screen.getByTestId('toggle-menu-option'));
-
-      const actions = store.getActions();
-      await waitFor(() =>
-        expect(actions[0]).toEqual({
-          type: QUERY_EDITOR_TOGGLE_LEFT_BAR,
-          hideLeftBar: !defaultQueryEditor.hideLeftBar,
-          queryEditor: expect.objectContaining({
-            id: defaultQueryEditor.id,
-          }),
-        }),
-      );
-    });
-
-    it('should dispatch removeAllOtherQueryEditors action', async () => {
-      await waitFor(() =>
-        expect(screen.getByTestId('close-tab-menu-option')).toBeInTheDocument(),
-      );
-      fireEvent.click(screen.getByTestId('close-all-other-menu-option'));
-
-      const actions = store.getActions();
-      await waitFor(() =>
-        expect(actions).toEqual([
-          {
-            type: REMOVE_QUERY_EDITOR,
-            queryEditor: initialState.sqlLab.queryEditors[1],
-          },
-          {
-            type: REMOVE_QUERY_EDITOR,
-            queryEditor: initialState.sqlLab.queryEditors[2],
-          },
-        ]),
-      );
-    });
-
-    it('should dispatch cloneQueryToNewTab action', async () => {
-      await waitFor(() =>
-        expect(screen.getByTestId('close-tab-menu-option')).toBeInTheDocument(),
-      );
-      fireEvent.click(screen.getByTestId('clone-tab-menu-option'));
-
-      const actions = store.getActions();
-      await waitFor(() =>
-        expect(actions[0]).toEqual({
-          type: ADD_QUERY_EDITOR,
-          queryEditor: expect.objectContaining({
-            name: `Copy of ${defaultQueryEditor.name}`,
-            sql: defaultQueryEditor.sql,
-            autorun: false,
-          }),
-        }),
-      );
-    });
-  });
-});
diff --git a/superset-frontend/src/SqlLab/components/SqlEditorTabHeader/index.tsx b/superset-frontend/src/SqlLab/components/SqlEditorTabHeader/index.tsx
deleted file mode 100644
index dce2f2700e..0000000000
--- a/superset-frontend/src/SqlLab/components/SqlEditorTabHeader/index.tsx
+++ /dev/null
@@ -1,147 +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, { useMemo } from 'react';
-import { bindActionCreators } from 'redux';
-import { useSelector, useDispatch, shallowEqual } from 'react-redux';
-import { Dropdown } from 'src/components/Dropdown';
-import { Menu } from 'src/components/Menu';
-import { styled, t, QueryState } from '@superset-ui/core';
-import {
-  removeQueryEditor,
-  removeAllOtherQueryEditors,
-  queryEditorSetTitle,
-  cloneQueryToNewTab,
-  toggleLeftBar,
-} from 'src/SqlLab/actions/sqlLab';
-import { QueryEditor, SqlLabRootState } from 'src/SqlLab/types';
-import TabStatusIcon from '../TabStatusIcon';
-
-const TabTitleWrapper = styled.div`
-  display: flex;
-  align-items: center;
-`;
-const TabTitle = styled.span`
-  margin-right: ${({ theme }) => theme.gridUnit * 2}px;
-  text-transform: none;
-`;
-
-interface Props {
-  queryEditor: QueryEditor;
-}
-
-const SqlEditorTabHeader: React.FC<Props> = ({ queryEditor }) => {
-  const qe = useSelector<SqlLabRootState, QueryEditor>(
-    ({ sqlLab: { unsavedQueryEditor } }) => ({
-      ...queryEditor,
-      ...(queryEditor.id === unsavedQueryEditor.id && unsavedQueryEditor),
-    }),
-    shallowEqual,
-  );
-  const queryStatus = useSelector<SqlLabRootState, QueryState>(
-    ({ sqlLab }) => sqlLab.queries[qe.latestQueryId || '']?.state || '',
-  );
-  const dispatch = useDispatch();
-  const actions = useMemo(
-    () =>
-      bindActionCreators(
-        {
-          removeQueryEditor,
-          removeAllOtherQueryEditors,
-          queryEditorSetTitle,
-          cloneQueryToNewTab,
-          toggleLeftBar,
-        },
-        dispatch,
-      ),
-    [dispatch],
-  );
-
-  function renameTab() {
-    const newTitle = prompt(t('Enter a new title for the tab'));
-    if (newTitle) {
-      actions.queryEditorSetTitle(qe, newTitle);
-    }
-  }
-
-  return (
-    <TabTitleWrapper>
-      <Dropdown
-        trigger={['click']}
-        overlay={
-          <Menu style={{ width: 176 }}>
-            <Menu.Item
-              className="close-btn"
-              key="1"
-              onClick={() => actions.removeQueryEditor(qe)}
-              data-test="close-tab-menu-option"
-            >
-              <div className="icon-container">
-                <i className="fa fa-close" />
-              </div>
-              {t('Close tab')}
-            </Menu.Item>
-            <Menu.Item
-              key="2"
-              onClick={renameTab}
-              data-test="rename-tab-menu-option"
-            >
-              <div className="icon-container">
-                <i className="fa fa-i-cursor" />
-              </div>
-              {t('Rename tab')}
-            </Menu.Item>
-            <Menu.Item
-              key="3"
-              onClick={() => actions.toggleLeftBar(qe)}
-              data-test="toggle-menu-option"
-            >
-              <div className="icon-container">
-                <i className="fa fa-cogs" />
-              </div>
-              {qe.hideLeftBar ? t('Expand tool bar') : t('Hide tool bar')}
-            </Menu.Item>
-            <Menu.Item
-              key="4"
-              onClick={() => actions.removeAllOtherQueryEditors(qe)}
-              data-test="close-all-other-menu-option"
-            >
-              <div className="icon-container">
-                <i className="fa fa-times-circle-o" />
-              </div>
-              {t('Close all other tabs')}
-            </Menu.Item>
-            <Menu.Item
-              key="5"
-              onClick={() => actions.cloneQueryToNewTab(qe, false)}
-              data-test="clone-tab-menu-option"
-            >
-              <div className="icon-container">
-                <i className="fa fa-files-o" />
-              </div>
-              {t('Duplicate tab')}
-            </Menu.Item>
-          </Menu>
-        }
-      />
-      <TabTitle>{qe.name}</TabTitle> <TabStatusIcon tabState={queryStatus} />{' '}
-    </TabTitleWrapper>
-  );
-};
-
-export default SqlEditorTabHeader;
diff --git a/superset-frontend/src/SqlLab/components/TabbedSqlEditors/TabbedSqlEditors.test.jsx b/superset-frontend/src/SqlLab/components/TabbedSqlEditors/TabbedSqlEditors.test.jsx
index bc290673be..5de36bc99a 100644
--- a/superset-frontend/src/SqlLab/components/TabbedSqlEditors/TabbedSqlEditors.test.jsx
+++ b/superset-frontend/src/SqlLab/components/TabbedSqlEditors/TabbedSqlEditors.test.jsx
@@ -30,7 +30,6 @@ import { EditableTabs } from 'src/components/Tabs';
 import TabbedSqlEditors from 'src/SqlLab/components/TabbedSqlEditors';
 import SqlEditor from 'src/SqlLab/components/SqlEditor';
 import { table, initialState } from 'src/SqlLab/fixtures';
-import { newQueryTabName } from 'src/SqlLab/utils/newQueryTabName';
 
 fetchMock.get('glob:*/api/v1/database/*', {});
 fetchMock.get('glob:*/savedqueryviewapi/api/get/*', {});
@@ -151,6 +150,18 @@ describe('TabbedSqlEditors', () => {
       );
     });
   });
+  it('should rename Tab', () => {
+    global.prompt = () => 'new title';
+    wrapper = getWrapper();
+    sinon.stub(wrapper.instance().props.actions, 'queryEditorSetTitle');
+
+    wrapper.instance().renameTab(queryEditors[0]);
+    expect(
+      wrapper.instance().props.actions.queryEditorSetTitle.getCall(0).args[1],
+    ).toBe('new title');
+
+    delete global.prompt;
+  });
   it('should removeQueryEditor', () => {
     wrapper = getWrapper();
     sinon.stub(wrapper.instance().props.actions, 'removeQueryEditor');
@@ -172,11 +183,11 @@ describe('TabbedSqlEditors', () => {
   it('should properly increment query tab name', () => {
     wrapper = getWrapper();
     sinon.stub(wrapper.instance().props.actions, 'addQueryEditor');
-    const newTitle = newQueryTabName(wrapper.instance().props.queryEditors);
+
     wrapper.instance().newQueryEditor();
     expect(
       wrapper.instance().props.actions.addQueryEditor.getCall(0).args[0].name,
-    ).toContain(newTitle);
+    ).toContain('Untitled Query 2');
   });
   it('should duplicate query editor', () => {
     wrapper = getWrapper();
diff --git a/superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.jsx b/superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.jsx
index c5b94f8b35..c97080c6e9 100644
--- a/superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.jsx
+++ b/superset-frontend/src/SqlLab/components/TabbedSqlEditors/index.jsx
@@ -18,7 +18,9 @@
  */
 import React from 'react';
 import PropTypes from 'prop-types';
+import { Dropdown } from 'src/components/Dropdown';
 import { EditableTabs } from 'src/components/Tabs';
+import { Menu } from 'src/components/Menu';
 import { connect } from 'react-redux';
 import { bindActionCreators } from 'redux';
 import URI from 'urijs';
@@ -31,7 +33,7 @@ import * as Actions from 'src/SqlLab/actions/sqlLab';
 import { EmptyStateBig } from 'src/components/EmptyState';
 import { newQueryTabName } from '../../utils/newQueryTabName';
 import SqlEditor from '../SqlEditor';
-import SqlEditorTabHeader from '../SqlEditorTabHeader';
+import TabStatusIcon from '../TabStatusIcon';
 
 const propTypes = {
   actions: PropTypes.object.isRequired,
@@ -42,6 +44,7 @@ const propTypes = {
   databases: PropTypes.object.isRequired,
   queries: PropTypes.object.isRequired,
   queryEditors: PropTypes.array,
+  requestedQuery: PropTypes.object,
   tabHistory: PropTypes.array.isRequired,
   tables: PropTypes.array.isRequired,
   offline: PropTypes.bool,
@@ -51,10 +54,16 @@ const propTypes = {
 const defaultProps = {
   queryEditors: [],
   offline: false,
+  requestedQuery: null,
   saveQueryWarning: null,
   scheduleQueryWarning: null,
 };
 
+const TabTitleWrapper = styled.div`
+  display: flex;
+  align-items: center;
+`;
+
 const StyledTab = styled.span`
   line-height: 24px;
 `;
@@ -77,6 +86,10 @@ class TabbedSqlEditors extends React.PureComponent {
       dataPreviewQueries: [],
     };
     this.removeQueryEditor = this.removeQueryEditor.bind(this);
+    this.renameTab = this.renameTab.bind(this);
+    this.toggleLeftBar = this.toggleLeftBar.bind(this);
+    this.removeAllOtherQueryEditors =
+      this.removeAllOtherQueryEditors.bind(this);
     this.duplicateQueryEditor = this.duplicateQueryEditor.bind(this);
     this.handleSelect = this.handleSelect.bind(this);
     this.handleEdit = this.handleEdit.bind(this);
@@ -223,6 +236,14 @@ class TabbedSqlEditors extends React.PureComponent {
     window.history.replaceState({}, document.title, this.state.sqlLabUrl);
   }
 
+  renameTab(qe) {
+    /* eslint no-alert: 0 */
+    const newTitle = prompt(t('Enter a new title for the tab'));
+    if (newTitle) {
+      this.props.actions.queryEditorSetTitle(qe, newTitle);
+    }
+  }
+
   activeQueryEditor() {
     if (this.props.tabHistory.length === 0) {
       return this.props.queryEditors[0];
@@ -283,34 +304,106 @@ class TabbedSqlEditors extends React.PureComponent {
     this.props.actions.removeQueryEditor(qe);
   }
 
+  removeAllOtherQueryEditors(cqe) {
+    this.props.queryEditors.forEach(
+      qe => qe !== cqe && this.removeQueryEditor(qe),
+    );
+  }
+
   duplicateQueryEditor(qe) {
     this.props.actions.cloneQueryToNewTab(qe, false);
   }
 
+  toggleLeftBar(qe) {
+    this.props.actions.toggleLeftBar(qe);
+  }
+
   render() {
     const noQueryEditors = this.props.queryEditors?.length === 0;
-    const editors = this.props.queryEditors?.map(qe => (
-      <EditableTabs.TabPane
-        key={qe.id}
-        tab={<SqlEditorTabHeader queryEditor={qe} />}
-        // for tests - key prop isn't handled by enzyme well bcs it's a react keyword
-        data-key={qe.id}
-      >
-        <SqlEditor
-          tables={this.props.tables.filter(xt => xt.queryEditorId === qe.id)}
-          queryEditor={qe}
-          editorQueries={this.state.queriesArray}
-          dataPreviewQueries={this.state.dataPreviewQueries}
-          actions={this.props.actions}
-          hideLeftBar={qe.hideLeftBar}
-          defaultQueryLimit={this.props.defaultQueryLimit}
-          maxRow={this.props.maxRow}
-          displayLimit={this.props.displayLimit}
-          saveQueryWarning={this.props.saveQueryWarning}
-          scheduleQueryWarning={this.props.scheduleQueryWarning}
-        />
-      </EditableTabs.TabPane>
-    ));
+    const editors = this.props.queryEditors.map(qe => {
+      let latestQuery;
+      if (qe.latestQueryId) {
+        latestQuery = this.props.queries[qe.latestQueryId];
+      }
+      let database;
+      if (qe.dbId) {
+        database = this.props.databases[qe.dbId];
+      }
+      const state = latestQuery ? latestQuery.state : '';
+
+      const menu = (
+        <Menu style={{ width: 176 }}>
+          <Menu.Item
+            className="close-btn"
+            key="1"
+            onClick={() => this.removeQueryEditor(qe)}
+            data-test="close-tab-menu-option"
+          >
+            <div className="icon-container">
+              <i className="fa fa-close" />
+            </div>
+            {t('Close tab')}
+          </Menu.Item>
+          <Menu.Item key="2" onClick={() => this.renameTab(qe)}>
+            <div className="icon-container">
+              <i className="fa fa-i-cursor" />
+            </div>
+            {t('Rename tab')}
+          </Menu.Item>
+          <Menu.Item key="3" onClick={() => this.toggleLeftBar(qe)}>
+            <div className="icon-container">
+              <i className="fa fa-cogs" />
+            </div>
+            {qe.hideLeftBar ? t('Expand tool bar') : t('Hide tool bar')}
+          </Menu.Item>
+          <Menu.Item
+            key="4"
+            onClick={() => this.removeAllOtherQueryEditors(qe)}
+          >
+            <div className="icon-container">
+              <i className="fa fa-times-circle-o" />
+            </div>
+            {t('Close all other tabs')}
+          </Menu.Item>
+          <Menu.Item key="5" onClick={() => this.duplicateQueryEditor(qe)}>
+            <div className="icon-container">
+              <i className="fa fa-files-o" />
+            </div>
+            {t('Duplicate tab')}
+          </Menu.Item>
+        </Menu>
+      );
+      const tabHeader = (
+        <TabTitleWrapper>
+          <Dropdown overlay={menu} trigger={['click']} />
+          <TabTitle>{qe.name}</TabTitle> <TabStatusIcon tabState={state} />{' '}
+        </TabTitleWrapper>
+      );
+      return (
+        <EditableTabs.TabPane
+          key={qe.id}
+          tab={tabHeader}
+          // for tests - key prop isn't handled by enzyme well bcs it's a react keyword
+          data-key={qe.id}
+        >
+          <SqlEditor
+            tables={this.props.tables.filter(xt => xt.queryEditorId === qe.id)}
+            queryEditorId={qe.id}
+            editorQueries={this.state.queriesArray}
+            dataPreviewQueries={this.state.dataPreviewQueries}
+            latestQuery={latestQuery}
+            database={database}
+            actions={this.props.actions}
+            hideLeftBar={qe.hideLeftBar}
+            defaultQueryLimit={this.props.defaultQueryLimit}
+            maxRow={this.props.maxRow}
+            displayLimit={this.props.displayLimit}
+            saveQueryWarning={this.props.saveQueryWarning}
+            scheduleQueryWarning={this.props.scheduleQueryWarning}
+          />
+        </EditableTabs.TabPane>
+      );
+    });
 
     const emptyTab = (
       <StyledTab>
@@ -379,7 +472,7 @@ class TabbedSqlEditors extends React.PureComponent {
 TabbedSqlEditors.propTypes = propTypes;
 TabbedSqlEditors.defaultProps = defaultProps;
 
-function mapStateToProps({ sqlLab, common }) {
+function mapStateToProps({ sqlLab, common, requestedQuery }) {
   return {
     databases: sqlLab.databases,
     queryEditors: sqlLab.queryEditors,
@@ -393,6 +486,7 @@ function mapStateToProps({ sqlLab, common }) {
     maxRow: common.conf.SQL_MAX_ROW,
     saveQueryWarning: common.conf.SQLLAB_SAVE_WARNING_MESSAGE,
     scheduleQueryWarning: common.conf.SQLLAB_SCHEDULE_WARNING_MESSAGE,
+    requestedQuery,
   };
 }
 function mapDispatchToProps(dispatch) {
diff --git a/superset-frontend/src/SqlLab/components/TemplateParamsEditor/TemplateParamsEditor.test.tsx b/superset-frontend/src/SqlLab/components/TemplateParamsEditor/TemplateParamsEditor.test.tsx
index a82b1e5859..bc04030d28 100644
--- a/superset-frontend/src/SqlLab/components/TemplateParamsEditor/TemplateParamsEditor.test.tsx
+++ b/superset-frontend/src/SqlLab/components/TemplateParamsEditor/TemplateParamsEditor.test.tsx
@@ -17,100 +17,38 @@
  * under the License.
  */
 
-import React from 'react';
-import configureStore from 'redux-mock-store';
-import thunk from 'redux-thunk';
-import { Store } from 'redux';
+import React, { ReactNode } from 'react';
 import {
   render,
   fireEvent,
   getByText,
   waitFor,
 } from 'spec/helpers/testing-library';
-import { initialState, defaultQueryEditor } from 'src/SqlLab/fixtures';
+import { ThemeProvider, supersetTheme } from '@superset-ui/core';
 
-import TemplateParamsEditor, {
-  Props,
-} from 'src/SqlLab/components/TemplateParamsEditor';
+import TemplateParamsEditor from 'src/SqlLab/components/TemplateParamsEditor';
 
-jest.mock('src/components/Select', () => () => (
-  <div data-test="mock-deprecated-select" />
-));
-jest.mock('src/components/Select/Select', () => () => (
-  <div data-test="mock-deprecated-select-select" />
-));
-jest.mock('src/components/Select/AsyncSelect', () => () => (
-  <div data-test="mock-async-select" />
-));
-jest.mock('src/components/AsyncAceEditor', () => ({
-  ConfigEditor: ({ value }: { value: string }) => (
-    <div data-test="mock-async-ace-editor">{value}</div>
-  ),
-}));
-
-const middlewares = [thunk];
-const mockStore = configureStore(middlewares);
-const setup = (otherProps: Partial<Props> = {}, store?: Store) =>
-  render(
-    <TemplateParamsEditor
-      language="json"
-      onChange={() => {}}
-      queryEditor={defaultQueryEditor}
-      {...otherProps}
-    />,
-    {
-      useRedux: true,
-      store: mockStore(initialState),
-      ...(store && { store }),
-    },
-  );
+const ThemeWrapper = ({ children }: { children: ReactNode }) => (
+  <ThemeProvider theme={supersetTheme}>{children}</ThemeProvider>
+);
 
 describe('TemplateParamsEditor', () => {
   it('should render with a title', () => {
-    const { container } = setup();
+    const { container } = render(
+      <TemplateParamsEditor code="FOO" language="json" onChange={() => {}} />,
+      { wrapper: ThemeWrapper },
+    );
     expect(container.querySelector('div[role="button"]')).toBeInTheDocument();
   });
 
   it('should open a modal with the ace editor', async () => {
-    const { container, getByTestId } = setup();
-    fireEvent.click(getByText(container, 'Parameters'));
-    await waitFor(() => {
-      expect(getByTestId('mock-async-ace-editor')).toBeInTheDocument();
-    });
-  });
-
-  it('renders templateParams', async () => {
-    const { container, getByTestId } = setup();
-    fireEvent.click(getByText(container, 'Parameters'));
-    await waitFor(() => {
-      expect(getByTestId('mock-async-ace-editor')).toBeInTheDocument();
-    });
-    expect(getByTestId('mock-async-ace-editor')).toHaveTextContent(
-      defaultQueryEditor.templateParams,
-    );
-  });
-
-  it('renders code from unsaved changes', async () => {
-    const expectedCode = 'custom code value';
-    const { container, getByTestId } = setup(
-      {},
-      mockStore({
-        ...initialState,
-        sqlLab: {
-          ...initialState.sqlLab,
-          unsavedQueryEditor: {
-            id: defaultQueryEditor.id,
-            templateParams: expectedCode,
-          },
-        },
-      }),
+    const { container, baseElement } = render(
+      <TemplateParamsEditor code="FOO" language="json" onChange={() => {}} />,
+      { wrapper: ThemeWrapper },
     );
     fireEvent.click(getByText(container, 'Parameters'));
     await waitFor(() => {
-      expect(getByTestId('mock-async-ace-editor')).toBeInTheDocument();
+      expect(baseElement.querySelector('#ace-editor')).toBeInTheDocument();
     });
-    expect(getByTestId('mock-async-ace-editor')).toHaveTextContent(
-      expectedCode,
-    );
   });
 });
diff --git a/superset-frontend/src/SqlLab/components/TemplateParamsEditor/index.tsx b/superset-frontend/src/SqlLab/components/TemplateParamsEditor/index.tsx
index 4eea10da05..62d0a7209d 100644
--- a/superset-frontend/src/SqlLab/components/TemplateParamsEditor/index.tsx
+++ b/superset-frontend/src/SqlLab/components/TemplateParamsEditor/index.tsx
@@ -26,9 +26,6 @@ import ModalTrigger from 'src/components/ModalTrigger';
 import { ConfigEditor } from 'src/components/AsyncAceEditor';
 import { FAST_DEBOUNCE } from 'src/constants';
 import { Tooltip } from 'src/components/Tooltip';
-import { useSelector } from 'react-redux';
-import { QueryEditor, SqlLabRootState } from 'src/SqlLab/types';
-import { getUpToDateQuery } from 'src/SqlLab/actions/sqlLab';
 
 const StyledConfigEditor = styled(ConfigEditor)`
   &.ace_editor {
@@ -36,24 +33,17 @@ const StyledConfigEditor = styled(ConfigEditor)`
   }
 `;
 
-export type Props = {
-  queryEditor: QueryEditor;
-  language: 'yaml' | 'json';
-  onChange: () => void;
-};
-
 function TemplateParamsEditor({
-  queryEditor,
+  code = '{}',
   language,
   onChange = () => {},
-}: Props) {
+}: {
+  code: string;
+  language: 'yaml' | 'json';
+  onChange: () => void;
+}) {
   const [parsedJSON, setParsedJSON] = useState({});
   const [isValid, setIsValid] = useState(true);
-  const code = useSelector<SqlLabRootState, string>(
-    rootState =>
-      (getUpToDateQuery(rootState, queryEditor) as unknown as QueryEditor)
-        .templateParams || '{}',
-  );
 
   useEffect(() => {
     try {
diff --git a/superset-frontend/src/SqlLab/fixtures.ts b/superset-frontend/src/SqlLab/fixtures.ts
index 72c1c0d50f..ea0fbd1bb8 100644
--- a/superset-frontend/src/SqlLab/fixtures.ts
+++ b/superset-frontend/src/SqlLab/fixtures.ts
@@ -178,38 +178,18 @@ export const table = {
 export const defaultQueryEditor = {
   id: 'dfsadfs',
   autorun: false,
-  dbId: undefined,
+  dbId: null,
   latestQueryId: null,
-  selectedText: undefined,
+  selectedText: null,
   sql: 'SELECT *\nFROM\nWHERE',
   name: 'Untitled Query 1',
-  schema: 'main',
-  remoteId: null,
-  tableOptions: [],
-  functionNames: [],
-  hideLeftBar: false,
   schemaOptions: [
     {
       value: 'main',
       label: 'main',
-      title: 'main',
+      name: 'main',
     },
   ],
-  templateParams: '{}',
-};
-
-export const extraQueryEditor1 = {
-  ...defaultQueryEditor,
-  id: 'diekd23',
-  sql: 'SELECT *\nFROM\nWHERE\nLIMIT',
-  name: 'Untitled Query 2',
-};
-
-export const extraQueryEditor2 = {
-  ...defaultQueryEditor,
-  id: 'owkdi998',
-  sql: 'SELECT *\nFROM\nWHERE\nGROUP BY',
-  name: 'Untitled Query 3',
 };
 
 export const queries = [
@@ -660,14 +640,13 @@ export const initialState = {
     alerts: [],
     queries: {},
     databases: {},
-    queryEditors: [defaultQueryEditor, extraQueryEditor1, extraQueryEditor2],
+    queryEditors: [defaultQueryEditor],
     tabHistory: [defaultQueryEditor.id],
     tables: [],
     workspaceQueries: [],
     queriesLastUpdate: 0,
     activeSouthPaneTab: 'Results',
     user: { user },
-    unsavedQueryEditor: {},
   },
   messageToasts: [],
   common: {
diff --git a/superset-frontend/src/SqlLab/reducers/getInitialState.js b/superset-frontend/src/SqlLab/reducers/getInitialState.js
index 2d00d3e0d6..21c367844d 100644
--- a/superset-frontend/src/SqlLab/reducers/getInitialState.js
+++ b/superset-frontend/src/SqlLab/reducers/getInitialState.js
@@ -37,7 +37,7 @@ export default function getInitialState({
    * To allow for a transparent migration, the initial state is a combination
    * of the backend state (if any) with the browser state (if any).
    */
-  let queryEditors = {};
+  const queryEditors = [];
   const defaultQueryEditor = {
     id: null,
     loaded: true,
@@ -55,9 +55,13 @@ export default function getInitialState({
       errors: [],
       completed: false,
     },
+    queryCostEstimate: {
+      cost: null,
+      completed: false,
+      error: null,
+    },
     hideLeftBar: false,
   };
-  let unsavedQueryEditor = {};
 
   /**
    * Load state from the backend. This will be empty if the feature flag
@@ -98,10 +102,7 @@ export default function getInitialState({
         name: label,
       };
     }
-    queryEditors = {
-      ...queryEditors,
-      [queryEditor.id]: queryEditor,
-    };
+    queryEditors.push(queryEditor);
   });
 
   const tabHistory = activeTab ? [activeTab.id.toString()] : [];
@@ -159,22 +160,15 @@ export default function getInitialState({
       // migration was successful
       localStorage.removeItem('redux');
     } else {
-      unsavedQueryEditor = sqlLab.unsavedQueryEditor || {};
       // add query editors and tables to state with a special flag so they can
       // be migrated if the `SQLLAB_BACKEND_PERSISTENCE` feature flag is on
-      sqlLab.queryEditors.forEach(qe => {
-        queryEditors = {
-          ...queryEditors,
-          [qe.id]: {
-            ...queryEditors[qe.id],
-            ...qe,
-            name: qe.title || qe.name,
-            ...(unsavedQueryEditor.id === qe.id && unsavedQueryEditor),
-            inLocalStorage: true,
-            loaded: true,
-          },
-        };
-      });
+      sqlLab.queryEditors.forEach(qe =>
+        queryEditors.push({
+          ...qe,
+          inLocalStorage: true,
+          loaded: true,
+        }),
+      );
       sqlLab.tables.forEach(table =>
         tables.push({ ...table, inLocalStorage: true }),
       );
@@ -192,13 +186,11 @@ export default function getInitialState({
       databases,
       offline: false,
       queries,
-      queryEditors: Object.values(queryEditors),
+      queryEditors,
       tabHistory,
       tables,
       queriesLastUpdate: Date.now(),
       user,
-      unsavedQueryEditor,
-      queryCostEstimates: {},
     },
     requestedQuery,
     messageToasts: getToastsFromPyFlashMessages(
diff --git a/superset-frontend/src/SqlLab/reducers/sqlLab.js b/superset-frontend/src/SqlLab/reducers/sqlLab.js
index a12db71a37..f87c8ce9c1 100644
--- a/superset-frontend/src/SqlLab/reducers/sqlLab.js
+++ b/superset-frontend/src/SqlLab/reducers/sqlLab.js
@@ -31,28 +31,12 @@ import {
   extendArr,
 } from '../../reduxUtils';
 
-function alterUnsavedQueryEditorState(state, updatedState, id) {
-  return {
-    ...(state.unsavedQueryEditor.id === id && state.unsavedQueryEditor),
-    ...(id ? { id, ...updatedState } : state.unsavedQueryEditor),
-  };
-}
-
 export default function sqlLabReducer(state = {}, action) {
   const actionHandlers = {
     [actions.ADD_QUERY_EDITOR]() {
-      const mergeUnsavedState = alterInArr(
-        state,
-        'queryEditors',
-        state.unsavedQueryEditor,
-        {
-          ...state.unsavedQueryEditor,
-        },
-      );
-      const newState = {
-        ...mergeUnsavedState,
-        tabHistory: [...state.tabHistory, action.queryEditor.id],
-      };
+      const tabHistory = state.tabHistory.slice();
+      tabHistory.push(action.queryEditor.id);
+      const newState = { ...state, tabHistory };
       return addToArr(newState, 'queryEditors', action.queryEditor);
     },
     [actions.QUERY_EDITOR_SAVED]() {
@@ -82,14 +66,9 @@ export default function sqlLabReducer(state = {}, action) {
       );
     },
     [actions.CLONE_QUERY_TO_NEW_TAB]() {
-      const queryEditor = state.queryEditors.find(
+      const progenitor = state.queryEditors.find(
         qe => qe.id === state.tabHistory[state.tabHistory.length - 1],
       );
-      const progenitor = {
-        ...queryEditor,
-        ...(state.unsavedQueryEditor.id === queryEditor.id &&
-          state.unsavedQueryEditor),
-      };
       const qe = {
         remoteId: progenitor.remoteId,
         name: t('Copy of %s', progenitor.name),
@@ -100,14 +79,7 @@ export default function sqlLabReducer(state = {}, action) {
         queryLimit: action.query.queryLimit,
         maxRow: action.query.maxRow,
       };
-      const stateWithoutUnsavedState = {
-        ...state,
-        unsavedQueryEditor: {},
-      };
-      return sqlLabReducer(
-        stateWithoutUnsavedState,
-        actions.addQueryEditor(qe),
-      );
+      return sqlLabReducer(state, actions.addQueryEditor(qe));
     },
     [actions.REMOVE_QUERY_EDITOR]() {
       let newState = removeFromArr(state, 'queryEditors', action.queryEditor);
@@ -211,20 +183,16 @@ export default function sqlLabReducer(state = {}, action) {
       };
     },
     [actions.START_QUERY_VALIDATION]() {
-      return {
-        ...state,
-        unsavedQueryEditor: alterUnsavedQueryEditorState(
-          state,
-          {
-            validationResult: {
-              id: action.query.id,
-              errors: [],
-              completed: false,
-            },
-          },
-          action.query.sqlEditorId,
-        ),
-      };
+      let newState = { ...state };
+      const sqlEditor = { id: action.query.sqlEditorId };
+      newState = alterInArr(newState, 'queryEditors', sqlEditor, {
+        validationResult: {
+          id: action.query.id,
+          errors: [],
+          completed: false,
+        },
+      });
+      return newState;
     },
     [actions.QUERY_VALIDATION_RETURNED]() {
       // If the server is very slow about answering us, we might get validation
@@ -234,29 +202,21 @@ export default function sqlLabReducer(state = {}, action) {
       // We don't care about any but the most recent because validations are
       // only valid for the SQL text they correspond to -- once the SQL has
       // changed, the old validation doesn't tell us anything useful anymore.
-      const qe = {
-        ...getFromArr(state.queryEditors, action.query.sqlEditorId),
-        ...(state.unsavedQueryEditor.id === action.query.sqlEditorId &&
-          state.unsavedQueryEditor),
-      };
+      const qe = getFromArr(state.queryEditors, action.query.sqlEditorId);
       if (qe.validationResult.id !== action.query.id) {
         return state;
       }
       // Otherwise, persist the results on the queryEditor state
-      return {
-        ...state,
-        unsavedQueryEditor: alterUnsavedQueryEditorState(
-          state,
-          {
-            validationResult: {
-              id: action.query.id,
-              errors: action.results,
-              completed: true,
-            },
-          },
-          action.query.sqlEditorId,
-        ),
-      };
+      let newState = { ...state };
+      const sqlEditor = { id: action.query.sqlEditorId };
+      newState = alterInArr(newState, 'queryEditors', sqlEditor, {
+        validationResult: {
+          id: action.query.id,
+          errors: action.results,
+          completed: true,
+        },
+      });
+      return newState;
     },
     [actions.QUERY_VALIDATION_FAILED]() {
       // If the server is very slow about answering us, we might get validation
@@ -290,52 +250,45 @@ export default function sqlLabReducer(state = {}, action) {
       return newState;
     },
     [actions.COST_ESTIMATE_STARTED]() {
-      return {
-        ...state,
-        queryCostEstimates: {
-          ...state.queryCostEstimates,
-          [action.query.sqlEditorId]: {
-            completed: false,
-            cost: null,
-            error: null,
-          },
+      let newState = { ...state };
+      const sqlEditor = { id: action.query.sqlEditorId };
+      newState = alterInArr(newState, 'queryEditors', sqlEditor, {
+        queryCostEstimate: {
+          completed: false,
+          cost: null,
+          error: null,
         },
-      };
+      });
+      return newState;
     },
     [actions.COST_ESTIMATE_RETURNED]() {
-      return {
-        ...state,
-        queryCostEstimates: {
-          ...state.queryCostEstimates,
-          [action.query.sqlEditorId]: {
-            completed: true,
-            cost: action.json,
-            error: null,
-          },
+      let newState = { ...state };
+      const sqlEditor = { id: action.query.sqlEditorId };
+      newState = alterInArr(newState, 'queryEditors', sqlEditor, {
+        queryCostEstimate: {
+          completed: true,
+          cost: action.json,
+          error: null,
         },
-      };
+      });
+      return newState;
     },
     [actions.COST_ESTIMATE_FAILED]() {
-      return {
-        ...state,
-        queryCostEstimates: {
-          ...state.queryCostEstimates,
-          [action.query.sqlEditorId]: {
-            completed: false,
-            cost: null,
-            error: action.error,
-          },
+      let newState = { ...state };
+      const sqlEditor = { id: action.query.sqlEditorId };
+      newState = alterInArr(newState, 'queryEditors', sqlEditor, {
+        queryCostEstimate: {
+          completed: false,
+          cost: null,
+          error: action.error,
         },
-      };
+      });
+      return newState;
     },
     [actions.START_QUERY]() {
       let newState = { ...state };
       if (action.query.sqlEditorId) {
-        const qe = {
-          ...getFromArr(state.queryEditors, action.query.sqlEditorId),
-          ...(action.query.sqlEditorId === state.unsavedQueryEditor.id &&
-            state.unsavedQueryEditor),
-        };
+        const qe = getFromArr(state.queryEditors, action.query.sqlEditorId);
         if (qe.latestQueryId && state.queries[qe.latestQueryId]) {
           const newResults = {
             ...state.queries[qe.latestQueryId].results,
@@ -350,17 +303,10 @@ export default function sqlLabReducer(state = {}, action) {
         newState.activeSouthPaneTab = action.query.id;
       }
       newState = addToObject(newState, 'queries', action.query);
-
-      return {
-        ...newState,
-        unsavedQueryEditor: alterUnsavedQueryEditorState(
-          state,
-          {
-            latestQueryId: action.query.id,
-          },
-          action.query.sqlEditorId,
-        ),
-      };
+      const sqlEditor = { id: action.query.sqlEditorId };
+      return alterInArr(newState, 'queryEditors', sqlEditor, {
+        latestQueryId: action.query.id,
+      });
     },
     [actions.STOP_QUERY]() {
       return alterInObject(state, 'queries', action.query, {
@@ -425,41 +371,14 @@ export default function sqlLabReducer(state = {}, action) {
         qeIds.indexOf(action.queryEditor?.id) > -1 &&
         state.tabHistory[state.tabHistory.length - 1] !== action.queryEditor.id
       ) {
-        const mergeUnsavedState = alterInArr(
-          state,
-          'queryEditors',
-          state.unsavedQueryEditor,
-          {
-            ...state.unsavedQueryEditor,
-          },
-        );
-        return {
-          ...(action.queryEditor.id === state.unsavedQueryEditor.id
-            ? alterInObject(
-                mergeUnsavedState,
-                'queryEditors',
-                action.queryEditor,
-                {
-                  ...action.queryEditor,
-                  ...state.unsavedQueryEditor,
-                },
-              )
-            : mergeUnsavedState),
-          tabHistory: [...state.tabHistory, action.queryEditor.id],
-        };
+        const tabHistory = state.tabHistory.slice();
+        tabHistory.push(action.queryEditor.id);
+        return { ...state, tabHistory };
       }
       return state;
     },
     [actions.LOAD_QUERY_EDITOR]() {
-      const mergeUnsavedState = alterInArr(
-        state,
-        'queryEditors',
-        state.unsavedQueryEditor,
-        {
-          ...state.unsavedQueryEditor,
-        },
-      );
-      return alterInArr(mergeUnsavedState, 'queryEditors', action.queryEditor, {
+      return alterInArr(state, 'queryEditors', action.queryEditor, {
         ...action.queryEditor,
       });
     },
@@ -522,161 +441,70 @@ export default function sqlLabReducer(state = {}, action) {
       return { ...state, queries };
     },
     [actions.QUERY_EDITOR_SETDB]() {
-      return {
-        ...state,
-        unsavedQueryEditor: alterUnsavedQueryEditorState(
-          state,
-          {
-            dbId: action.dbId,
-          },
-          action.queryEditor.id,
-        ),
-      };
+      return alterInArr(state, 'queryEditors', action.queryEditor, {
+        dbId: action.dbId,
+      });
     },
     [actions.QUERY_EDITOR_SET_FUNCTION_NAMES]() {
-      return {
-        ...state,
-        unsavedQueryEditor: alterUnsavedQueryEditorState(
-          state,
-          {
-            functionNames: action.functionNames,
-          },
-          action.queryEditor.id,
-        ),
-      };
+      return alterInArr(state, 'queryEditors', action.queryEditor, {
+        functionNames: action.functionNames,
+      });
     },
     [actions.QUERY_EDITOR_SET_SCHEMA]() {
-      return {
-        ...state,
-        unsavedQueryEditor: alterUnsavedQueryEditorState(
-          state,
-          {
-            schema: action.schema,
-          },
-          action.queryEditor.id,
-        ),
-      };
+      return alterInArr(state, 'queryEditors', action.queryEditor, {
+        schema: action.schema,
+      });
     },
     [actions.QUERY_EDITOR_SET_SCHEMA_OPTIONS]() {
-      return {
-        ...state,
-        unsavedQueryEditor: alterUnsavedQueryEditorState(
-          state,
-          {
-            schemaOptions: action.options,
-          },
-          action.queryEditor.id,
-        ),
-      };
+      return alterInArr(state, 'queryEditors', action.queryEditor, {
+        schemaOptions: action.options,
+      });
     },
     [actions.QUERY_EDITOR_SET_TABLE_OPTIONS]() {
-      return {
-        ...state,
-        unsavedQueryEditor: alterUnsavedQueryEditorState(
-          state,
-          {
-            tableOptions: action.options,
-          },
-          action.queryEditor.id,
-        ),
-      };
+      return alterInArr(state, 'queryEditors', action.queryEditor, {
+        tableOptions: action.options,
+      });
     },
     [actions.QUERY_EDITOR_SET_TITLE]() {
-      return {
-        ...state,
-        unsavedQueryEditor: alterUnsavedQueryEditorState(
-          state,
-          {
-            name: action.name,
-          },
-          action.queryEditor.id,
-        ),
-      };
+      return alterInArr(state, 'queryEditors', action.queryEditor, {
+        name: action.name,
+      });
     },
     [actions.QUERY_EDITOR_SET_SQL]() {
-      return {
-        ...state,
-        unsavedQueryEditor: alterUnsavedQueryEditorState(
-          state,
-          {
-            sql: action.sql,
-          },
-          action.queryEditor.id,
-        ),
-      };
+      return alterInArr(state, 'queryEditors', action.queryEditor, {
+        sql: action.sql,
+      });
     },
     [actions.QUERY_EDITOR_SET_QUERY_LIMIT]() {
-      return {
-        ...state,
-        unsavedQueryEditor: alterUnsavedQueryEditorState(
-          state,
-          {
-            queryLimit: action.queryLimit,
-          },
-          action.queryEditor.id,
-        ),
-      };
+      return alterInArr(state, 'queryEditors', action.queryEditor, {
+        queryLimit: action.queryLimit,
+      });
     },
     [actions.QUERY_EDITOR_SET_TEMPLATE_PARAMS]() {
-      return {
-        ...state,
-        unsavedQueryEditor: alterUnsavedQueryEditorState(
-          state,
-          {
-            templateParams: action.templateParams,
-          },
-          action.queryEditor.id,
-        ),
-      };
+      return alterInArr(state, 'queryEditors', action.queryEditor, {
+        templateParams: action.templateParams,
+      });
     },
     [actions.QUERY_EDITOR_SET_SELECTED_TEXT]() {
-      return {
-        ...state,
-        unsavedQueryEditor: alterUnsavedQueryEditorState(
-          state,
-          {
-            selectedText: action.sql,
-          },
-          action.queryEditor.id,
-        ),
-      };
+      return alterInArr(state, 'queryEditors', action.queryEditor, {
+        selectedText: action.sql,
+      });
     },
     [actions.QUERY_EDITOR_SET_AUTORUN]() {
-      return {
-        ...state,
-        unsavedQueryEditor: alterUnsavedQueryEditorState(
-          state,
-          {
-            autorun: action.autorun,
-          },
-          action.queryEditor.id,
-        ),
-      };
+      return alterInArr(state, 'queryEditors', action.queryEditor, {
+        autorun: action.autorun,
+      });
     },
     [actions.QUERY_EDITOR_PERSIST_HEIGHT]() {
-      return {
-        ...state,
-        unsavedQueryEditor: alterUnsavedQueryEditorState(
-          state,
-          {
-            northPercent: action.northPercent,
-            southPercent: action.southPercent,
-          },
-          action.queryEditor.id,
-        ),
-      };
+      return alterInArr(state, 'queryEditors', action.queryEditor, {
+        northPercent: action.northPercent,
+        southPercent: action.southPercent,
+      });
     },
     [actions.QUERY_EDITOR_TOGGLE_LEFT_BAR]() {
-      return {
-        ...state,
-        unsavedQueryEditor: alterUnsavedQueryEditorState(
-          state,
-          {
-            hideLeftBar: action.hideLeftBar,
-          },
-          action.queryEditor.id,
-        ),
-      };
+      return alterInArr(state, 'queryEditors', action.queryEditor, {
+        hideLeftBar: action.hideLeftBar,
+      });
     },
     [actions.SET_DATABASES]() {
       const databases = {};
diff --git a/superset-frontend/src/SqlLab/reducers/sqlLab.test.js b/superset-frontend/src/SqlLab/reducers/sqlLab.test.js
index 82483712f8..4c92e13247 100644
--- a/superset-frontend/src/SqlLab/reducers/sqlLab.test.js
+++ b/superset-frontend/src/SqlLab/reducers/sqlLab.test.js
@@ -39,55 +39,23 @@ describe('sqlLabReducer', () => {
       qe = newState.queryEditors.find(e => e.id === 'abcd');
     });
     it('should add a query editor', () => {
-      expect(newState.queryEditors).toHaveLength(
-        initialState.queryEditors.length + 1,
-      );
-    });
-    it('should merge the current unsaved changes when adding a query editor', () => {
-      const expectedTitle = 'new updated title';
-      const updateAction = {
-        type: actions.QUERY_EDITOR_SET_TITLE,
-        queryEditor: initialState.queryEditors[0],
-        name: expectedTitle,
-      };
-      newState = sqlLabReducer(newState, updateAction);
-      const addAction = {
-        type: actions.ADD_QUERY_EDITOR,
-        queryEditor: { ...initialState.queryEditors[0], id: 'efgh' },
-      };
-      newState = sqlLabReducer(newState, addAction);
-
-      expect(newState.queryEditors[0].name).toEqual(expectedTitle);
-      expect(
-        newState.queryEditors[newState.queryEditors.length - 1].id,
-      ).toEqual('efgh');
+      expect(newState.queryEditors).toHaveLength(2);
     });
     it('should remove a query editor', () => {
-      expect(newState.queryEditors).toHaveLength(
-        initialState.queryEditors.length + 1,
-      );
+      expect(newState.queryEditors).toHaveLength(2);
       const action = {
         type: actions.REMOVE_QUERY_EDITOR,
         queryEditor: qe,
       };
       newState = sqlLabReducer(newState, action);
-      expect(newState.queryEditors).toHaveLength(
-        initialState.queryEditors.length,
-      );
+      expect(newState.queryEditors).toHaveLength(1);
     });
     it('should set q query editor active', () => {
-      const expectedTitle = 'new updated title';
       const addQueryEditorAction = {
         type: actions.ADD_QUERY_EDITOR,
         queryEditor: { ...initialState.queryEditors[0], id: 'abcd' },
       };
       newState = sqlLabReducer(newState, addQueryEditorAction);
-      const updateAction = {
-        type: actions.QUERY_EDITOR_SET_TITLE,
-        queryEditor: initialState.queryEditors[1],
-        name: expectedTitle,
-      };
-      newState = sqlLabReducer(newState, updateAction);
       const setActiveQueryEditorAction = {
         type: actions.SET_ACTIVE_QUERY_EDITOR,
         queryEditor: defaultQueryEditor,
@@ -96,7 +64,6 @@ describe('sqlLabReducer', () => {
       expect(newState.tabHistory[newState.tabHistory.length - 1]).toBe(
         defaultQueryEditor.id,
       );
-      expect(newState.queryEditors[1].name).toEqual(expectedTitle);
     });
     it('should not fail while setting DB', () => {
       const dbId = 9;
@@ -106,8 +73,7 @@ describe('sqlLabReducer', () => {
         dbId,
       };
       newState = sqlLabReducer(newState, action);
-      expect(newState.unsavedQueryEditor.dbId).toBe(dbId);
-      expect(newState.unsavedQueryEditor.id).toBe(qe.id);
+      expect(newState.queryEditors[1].dbId).toBe(dbId);
     });
     it('should not fail while setting schema', () => {
       const schema = 'foo';
@@ -117,8 +83,7 @@ describe('sqlLabReducer', () => {
         schema,
       };
       newState = sqlLabReducer(newState, action);
-      expect(newState.unsavedQueryEditor.schema).toBe(schema);
-      expect(newState.unsavedQueryEditor.id).toBe(qe.id);
+      expect(newState.queryEditors[1].schema).toBe(schema);
     });
     it('should not fail while setting autorun', () => {
       const action = {
@@ -126,22 +91,19 @@ describe('sqlLabReducer', () => {
         queryEditor: qe,
       };
       newState = sqlLabReducer(newState, { ...action, autorun: false });
-      expect(newState.unsavedQueryEditor.autorun).toBe(false);
-      expect(newState.unsavedQueryEditor.id).toBe(qe.id);
+      expect(newState.queryEditors[1].autorun).toBe(false);
       newState = sqlLabReducer(newState, { ...action, autorun: true });
-      expect(newState.unsavedQueryEditor.autorun).toBe(true);
-      expect(newState.unsavedQueryEditor.id).toBe(qe.id);
+      expect(newState.queryEditors[1].autorun).toBe(true);
     });
     it('should not fail while setting title', () => {
       const title = 'Untitled Query 1';
       const action = {
         type: actions.QUERY_EDITOR_SET_TITLE,
         queryEditor: qe,
-        name: title,
+        title,
       };
       newState = sqlLabReducer(newState, action);
-      expect(newState.unsavedQueryEditor.name).toBe(title);
-      expect(newState.unsavedQueryEditor.id).toBe(qe.id);
+      expect(newState.queryEditors[0].name).toBe(title);
     });
     it('should not fail while setting Sql', () => {
       const sql = 'SELECT nothing from dev_null';
@@ -151,8 +113,7 @@ describe('sqlLabReducer', () => {
         sql,
       };
       newState = sqlLabReducer(newState, action);
-      expect(newState.unsavedQueryEditor.sql).toBe(sql);
-      expect(newState.unsavedQueryEditor.id).toBe(qe.id);
+      expect(newState.queryEditors[1].sql).toBe(sql);
     });
     it('should not fail while setting queryLimit', () => {
       const queryLimit = 101;
@@ -162,8 +123,7 @@ describe('sqlLabReducer', () => {
         queryLimit,
       };
       newState = sqlLabReducer(newState, action);
-      expect(newState.unsavedQueryEditor.queryLimit).toBe(queryLimit);
-      expect(newState.unsavedQueryEditor.id).toBe(qe.id);
+      expect(newState.queryEditors[1].queryLimit).toEqual(queryLimit);
     });
     it('should set selectedText', () => {
       const selectedText = 'TEST';
@@ -172,10 +132,9 @@ describe('sqlLabReducer', () => {
         queryEditor: newState.queryEditors[0],
         sql: selectedText,
       };
-      expect(newState.queryEditors[0].selectedText).toBeFalsy();
+      expect(newState.queryEditors[0].selectedText).toBeNull();
       newState = sqlLabReducer(newState, action);
-      expect(newState.unsavedQueryEditor.selectedText).toBe(selectedText);
-      expect(newState.unsavedQueryEditor.id).toBe(newState.queryEditors[0].id);
+      expect(newState.queryEditors[0].selectedText).toBe(selectedText);
     });
   });
   describe('Tables', () => {
diff --git a/superset-frontend/src/SqlLab/types.ts b/superset-frontend/src/SqlLab/types.ts
index 18c6773ae6..8bb3e73d28 100644
--- a/superset-frontend/src/SqlLab/types.ts
+++ b/superset-frontend/src/SqlLab/types.ts
@@ -32,26 +32,16 @@ export type QueryDictionary = {
 };
 
 export interface QueryEditor {
-  id: string;
   dbId?: number;
   name: string;
   schema: string;
   autorun: boolean;
   sql: string;
   remoteId: number | null;
-  tableOptions: any[];
-  schemaOptions?: SchemaOption[];
-  functionNames: string[];
   validationResult?: {
     completed: boolean;
     errors: SupersetError[];
   };
-  hideLeftBar?: boolean;
-  latestQueryId?: string | null;
-  templateParams?: string;
-  selectedText?: string;
-  queryLimit?: number;
-  description?: string;
 }
 
 export type toastState = {
@@ -69,15 +59,13 @@ export type SqlLabRootState = {
     databases: Record<string, any>;
     dbConnect: boolean;
     offline: boolean;
-    queries: Record<string, Query>;
+    queries: Query[];
     queryEditors: QueryEditor[];
     tabHistory: string[]; // default is activeTab ? [activeTab.id.toString()] : []
     tables: Record<string, any>[];
     queriesLastUpdate: number;
     user: UserWithPermissionsAndRoles;
     errorMessage: string | null;
-    unsavedQueryEditor: Partial<QueryEditor>;
-    queryCostEstimates?: Record<string, QueryCostEstimate>;
   };
   localStorageUsageInKilobytes: number;
   messageToasts: toastState[];
@@ -125,15 +113,3 @@ export interface DatasetOptionAutocomplete {
   datasetId: number;
   owners: [DatasetOwner];
 }
-
-export interface SchemaOption {
-  value: string;
-  label: string;
-  title: string;
-}
-
-export interface QueryCostEstimate {
-  completed: string;
-  cost: Record<string, any>[];
-  error: string;
-}
diff --git a/superset-frontend/src/SqlLab/utils/newQueryTabName.test.ts b/superset-frontend/src/SqlLab/utils/newQueryTabName.test.ts
index 33eec73781..6f2af5ebb7 100644
--- a/superset-frontend/src/SqlLab/utils/newQueryTabName.test.ts
+++ b/superset-frontend/src/SqlLab/utils/newQueryTabName.test.ts
@@ -17,11 +17,9 @@
  * under the License.
  */
 
-import { defaultQueryEditor } from 'src/SqlLab/fixtures';
 import { newQueryTabName } from './newQueryTabName';
 
 const emptyEditor = {
-  ...defaultQueryEditor,
   title: '',
   schema: '',
   autorun: false,
diff --git a/superset-frontend/src/components/Dropdown/index.tsx b/superset-frontend/src/components/Dropdown/index.tsx
index bd01aabb4d..e5d5f9f852 100644
--- a/superset-frontend/src/components/Dropdown/index.tsx
+++ b/superset-frontend/src/components/Dropdown/index.tsx
@@ -66,7 +66,7 @@ const MenuDotsWrapper = styled.div`
   padding-left: ${({ theme }) => theme.gridUnit}px;
 `;
 
-export interface DropdownProps extends DropDownProps {
+export interface DropdownProps {
   overlay: React.ReactElement;
 }
 
diff --git a/superset-frontend/src/components/TableSelector/index.tsx b/superset-frontend/src/components/TableSelector/index.tsx
index a41c4aa645..4c07e256cc 100644
--- a/superset-frontend/src/components/TableSelector/index.tsx
+++ b/superset-frontend/src/components/TableSelector/index.tsx
@@ -36,7 +36,6 @@ import RefreshLabel from 'src/components/RefreshLabel';
 import CertifiedBadge from 'src/components/CertifiedBadge';
 import WarningIconWithTooltip from 'src/components/WarningIconWithTooltip';
 import { useToasts } from 'src/components/MessageToasts/withToasts';
-import { SchemaOption } from 'src/SqlLab/types';
 
 const TableSelectorWrapper = styled.div`
   ${({ theme }) => `
@@ -90,7 +89,7 @@ interface TableSelectorProps {
   isDatabaseSelectEnabled?: boolean;
   onDbChange?: (db: DatabaseObject) => void;
   onSchemaChange?: (schema?: string) => void;
-  onSchemasLoad?: (schemaOptions: SchemaOption[]) => void;
+  onSchemasLoad?: () => void;
   onTablesLoad?: (options: Array<any>) => void;
   readOnly?: boolean;
   schema?: string;