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/10/07 17:14:14 UTC

[superset] 05/13: Revert "chore: refactor SqlEditor to functional component (#21320)"

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

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

commit 2c452903f3922d99b408c71d27cda51368c067b2
Author: Joe Li <jo...@preset.io>
AuthorDate: Mon Oct 3 10:36:11 2022 -0700

    Revert "chore: refactor SqlEditor to functional component (#21320)"
    
    This reverts commit 2224ebecfe7af0a4fa3736c33d697f6f41a07e46.
---
 .../src/SqlLab/components/SqlEditor/index.jsx      | 888 ++++++++++++---------
 superset-frontend/src/SqlLab/constants.ts          |   6 -
 2 files changed, 496 insertions(+), 398 deletions(-)

diff --git a/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx b/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx
index d6947c1ba5..c48594d304 100644
--- a/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx
+++ b/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx
@@ -18,15 +18,16 @@
  */
 /* eslint-disable jsx-a11y/anchor-is-valid */
 /* eslint-disable jsx-a11y/no-static-element-interactions */
-import React, { useState, useEffect, useMemo, useRef } from 'react';
+import React from 'react';
 import { CSSTransition } from 'react-transition-group';
-import { useDispatch, useSelector } from 'react-redux';
+import { connect } from 'react-redux';
+import { bindActionCreators } from 'redux';
 import PropTypes from 'prop-types';
 import Split from 'react-split';
-import { t, styled, useTheme } from '@superset-ui/core';
+import { t, styled, withTheme } from '@superset-ui/core';
 import debounce from 'lodash/debounce';
 import throttle from 'lodash/throttle';
-import Modal from 'src/components/Modal';
+import StyledModal from 'src/components/Modal';
 import Mousetrap from 'mousetrap';
 import Button from 'src/components/Button';
 import Timer from 'src/components/Timer';
@@ -47,6 +48,7 @@ import {
   queryEditorSetAndSaveSql,
   queryEditorSetTemplateParams,
   runQueryFromSqlEditor,
+  runQuery,
   saveQuery,
   addSavedQueryToTabState,
   scheduleQuery,
@@ -60,12 +62,6 @@ import {
   SQL_EDITOR_GUTTER_MARGIN,
   SQL_TOOLBAR_HEIGHT,
   SQL_EDITOR_LEFTBAR_WIDTH,
-  SQL_EDITOR_PADDING,
-  INITIAL_NORTH_PERCENT,
-  INITIAL_SOUTH_PERCENT,
-  SET_QUERY_EDITOR_SQL_DEBOUNCE_MS,
-  VALIDATION_DEBOUNCE_MS,
-  WINDOW_RESIZE_THROTTLE_MS,
 } from 'src/SqlLab/constants';
 import {
   getItem,
@@ -87,6 +83,13 @@ import RunQueryActionButton from '../RunQueryActionButton';
 import { newQueryTabName } from '../../utils/newQueryTabName';
 import QueryLimitSelect from '../QueryLimitSelect';
 
+const SQL_EDITOR_PADDING = 10;
+const INITIAL_NORTH_PERCENT = 30;
+const INITIAL_SOUTH_PERCENT = 70;
+const SET_QUERY_EDITOR_SQL_DEBOUNCE_MS = 2000;
+const VALIDATION_DEBOUNCE_MS = 600;
+const WINDOW_RESIZE_THROTTLE_MS = 100;
+
 const appContainer = document.getElementById('app');
 const bootstrapData = JSON.parse(
   appContainer.getAttribute('data-bootstrap') || '{}',
@@ -129,7 +132,7 @@ const StyledToolbar = styled.div`
 const StyledSidebar = styled.div`
   flex: 0 0 ${({ width }) => width}px;
   width: ${({ width }) => width}px;
-  padding: ${({ theme, hide }) => (hide ? 0 : theme.gridUnit * 2.5)}px;
+  padding: ${({ hide }) => (hide ? 0 : 10)}px;
   border-right: 1px solid
     ${({ theme, hide }) =>
       hide ? 'transparent' : theme.colors.grayscale.light2};
@@ -137,10 +140,13 @@ const StyledSidebar = styled.div`
 
 const propTypes = {
   actions: PropTypes.object.isRequired,
+  database: PropTypes.object,
+  latestQuery: PropTypes.object,
   tables: PropTypes.array.isRequired,
   editorQueries: PropTypes.array.isRequired,
   dataPreviewQueries: PropTypes.array.isRequired,
   queryEditor: PropTypes.object.isRequired,
+  hideLeftBar: PropTypes.bool,
   defaultQueryLimit: PropTypes.number.isRequired,
   maxRow: PropTypes.number.isRequired,
   displayLimit: PropTypes.number.isRequired,
@@ -148,97 +154,158 @@ const propTypes = {
   scheduleQueryWarning: PropTypes.string,
 };
 
-const SqlEditor = ({
-  actions,
-  tables,
-  editorQueries,
-  dataPreviewQueries,
-  queryEditor,
-  defaultQueryLimit,
-  maxRow,
-  displayLimit,
-  saveQueryWarning,
-  scheduleQueryWarning = null,
-}) => {
-  const theme = useTheme();
-  const dispatch = useDispatch();
-
-  const { database, latestQuery, hideLeftBar } = useSelector(
-    ({ sqlLab: { unsavedQueryEditor, databases, queries } }) => {
-      let { dbId, latestQueryId, hideLeftBar } = queryEditor;
-      if (unsavedQueryEditor.id === queryEditor.id) {
-        dbId = unsavedQueryEditor.dbId || dbId;
-        latestQueryId = unsavedQueryEditor.latestQueryId || latestQueryId;
-        hideLeftBar = unsavedQueryEditor.hideLeftBar || hideLeftBar;
-      }
-      return {
-        database: databases[dbId],
-        latestQuery: queries[latestQueryId],
-        hideLeftBar,
-      };
-    },
-  );
+const defaultProps = {
+  database: null,
+  latestQuery: null,
+  hideLeftBar: false,
+  scheduleQueryWarning: null,
+};
 
-  const queryEditors = useSelector(({ sqlLab }) => sqlLab.queryEditors);
+class SqlEditor extends React.PureComponent {
+  constructor(props) {
+    super(props);
+    this.state = {
+      autorun: props.queryEditor.autorun,
+      ctas: '',
+      northPercent: props.queryEditor.northPercent || INITIAL_NORTH_PERCENT,
+      southPercent: props.queryEditor.southPercent || INITIAL_SOUTH_PERCENT,
+      autocompleteEnabled: getItem(
+        LocalStorageKeys.sqllab__is_autocomplete_enabled,
+        true,
+      ),
+      showCreateAsModal: false,
+      createAs: '',
+      showEmptyState: false,
+    };
+    this.sqlEditorRef = React.createRef();
+    this.northPaneRef = React.createRef();
+
+    this.elementStyle = this.elementStyle.bind(this);
+    this.onResizeStart = this.onResizeStart.bind(this);
+    this.onResizeEnd = this.onResizeEnd.bind(this);
+    this.canValidateQuery = this.canValidateQuery.bind(this);
+    this.runQuery = this.runQuery.bind(this);
+    this.setEmptyState = this.setEmptyState.bind(this);
+    this.stopQuery = this.stopQuery.bind(this);
+    this.saveQuery = this.saveQuery.bind(this);
+    this.onSqlChanged = this.onSqlChanged.bind(this);
+    this.setQueryEditorAndSaveSql = this.setQueryEditorAndSaveSql.bind(this);
+    this.setQueryEditorAndSaveSqlWithDebounce = debounce(
+      this.setQueryEditorAndSaveSql.bind(this),
+      SET_QUERY_EDITOR_SQL_DEBOUNCE_MS,
+    );
+    this.queryPane = this.queryPane.bind(this);
+    this.getHotkeyConfig = this.getHotkeyConfig.bind(this);
+    this.getAceEditorAndSouthPaneHeights =
+      this.getAceEditorAndSouthPaneHeights.bind(this);
+    this.getSqlEditorHeight = this.getSqlEditorHeight.bind(this);
+    this.requestValidation = debounce(
+      this.requestValidation.bind(this),
+      VALIDATION_DEBOUNCE_MS,
+    );
+    this.getQueryCostEstimate = this.getQueryCostEstimate.bind(this);
+    this.handleWindowResize = throttle(
+      this.handleWindowResize.bind(this),
+      WINDOW_RESIZE_THROTTLE_MS,
+    );
 
-  const [height, setHeight] = useState(0);
-  const [autorun, setAutorun] = useState(queryEditor.autorun);
-  const [ctas, setCtas] = useState('');
-  const [northPercent, setNorthPercent] = useState(
-    queryEditor.northPercent || INITIAL_NORTH_PERCENT,
-  );
-  const [southPercent, setSouthPercent] = useState(
-    queryEditor.southPercent || INITIAL_SOUTH_PERCENT,
-  );
-  const [autocompleteEnabled, setAutocompleteEnabled] = useState(
-    getItem(LocalStorageKeys.sqllab__is_autocomplete_enabled, true),
-  );
-  const [showCreateAsModal, setShowCreateAsModal] = useState(false);
-  const [createAs, setCreateAs] = useState('');
-  const [showEmptyState, setShowEmptyState] = useState(false);
+    this.onBeforeUnload = this.onBeforeUnload.bind(this);
+    this.renderDropdown = this.renderDropdown.bind(this);
+  }
 
-  const sqlEditorRef = useRef(null);
-  const northPaneRef = useRef(null);
+  UNSAFE_componentWillMount() {
+    if (this.state.autorun) {
+      this.setState({ autorun: false });
+      this.props.queryEditorSetAutorun(this.props.queryEditor, false);
+      this.startQuery();
+    }
+  }
 
-  const startQuery = (ctasArg = false, ctas_method = CtasEnum.TABLE) => {
-    if (!database) {
-      return;
+  componentDidMount() {
+    // We need to measure the height of the sql editor post render to figure the height of
+    // the south pane so it gets rendered properly
+    // eslint-disable-next-line react/no-did-mount-set-state
+    const db = this.props.database;
+    this.setState({ height: this.getSqlEditorHeight() });
+    if (!db || isEmpty(db)) {
+      this.setEmptyState(true);
     }
 
-    dispatch(
-      runQueryFromSqlEditor(
-        database,
-        queryEditor,
-        defaultQueryLimit,
-        ctasArg ? ctas : '',
-        ctasArg,
-        ctas_method,
-      ),
-    );
-    dispatch(setActiveSouthPaneTab('Results'));
-  };
+    window.addEventListener('resize', this.handleWindowResize);
+    window.addEventListener('beforeunload', this.onBeforeUnload);
 
-  const stopQuery = () => {
-    if (latestQuery && ['running', 'pending'].indexOf(latestQuery.state) >= 0) {
-      dispatch(postStopQuery(latestQuery));
+    // setup hotkeys
+    const hotkeys = this.getHotkeyConfig();
+    hotkeys.forEach(keyConfig => {
+      Mousetrap.bind([keyConfig.key], keyConfig.func);
+    });
+  }
+
+  componentWillUnmount() {
+    window.removeEventListener('resize', this.handleWindowResize);
+    window.removeEventListener('beforeunload', this.onBeforeUnload);
+  }
+
+  onResizeStart() {
+    // Set the heights on the ace editor and the ace content area after drag starts
+    // to smooth out the visual transition to the new heights when drag ends
+    document.getElementsByClassName('ace_content')[0].style.height = '100%';
+  }
+
+  onResizeEnd([northPercent, southPercent]) {
+    this.setState({ northPercent, southPercent });
+
+    if (this.northPaneRef.current && this.northPaneRef.current.clientHeight) {
+      this.props.persistEditorHeight(
+        this.props.queryEditor,
+        northPercent,
+        southPercent,
+      );
     }
-  };
+  }
+
+  onBeforeUnload(event) {
+    if (
+      this.props.database?.extra_json?.cancel_query_on_windows_unload &&
+      this.props.latestQuery?.state === 'running'
+    ) {
+      event.preventDefault();
+      this.stopQuery();
+    }
+  }
 
-  useState(() => {
-    if (autorun) {
-      setAutorun(false);
-      dispatch(queryEditorSetAutorun(queryEditor, false));
-      startQuery();
+  onSqlChanged(sql) {
+    this.props.queryEditorSetSql(this.props.queryEditor, sql);
+    this.setQueryEditorAndSaveSqlWithDebounce(sql);
+    // Request server-side validation of the query text
+    if (this.canValidateQuery()) {
+      // NB. requestValidation is debounced
+      this.requestValidation(sql);
     }
-  });
+  }
 
   // One layer of abstraction for easy spying in unit tests
-  const getSqlEditorHeight = () =>
-    sqlEditorRef.current
-      ? sqlEditorRef.current.clientHeight - SQL_EDITOR_PADDING * 2
+  getSqlEditorHeight() {
+    return this.sqlEditorRef.current
+      ? this.sqlEditorRef.current.clientHeight - SQL_EDITOR_PADDING * 2
       : 0;
+  }
 
-  const getHotkeyConfig = () => {
+  // Return the heights for the ace editor and the south pane as an object
+  // given the height of the sql editor, north pane percent and south pane percent.
+  getAceEditorAndSouthPaneHeights(height, northPercent, southPercent) {
+    return {
+      aceEditorHeight:
+        (height * northPercent) / 100 -
+        (SQL_EDITOR_GUTTER_HEIGHT / 2 + SQL_EDITOR_GUTTER_MARGIN) -
+        SQL_TOOLBAR_HEIGHT,
+      southPaneHeight:
+        (height * southPercent) / 100 -
+        (SQL_EDITOR_GUTTER_HEIGHT / 2 + SQL_EDITOR_GUTTER_MARGIN),
+    };
+  }
+
+  getHotkeyConfig() {
     // Get the user's OS
     const userOS = detectOS();
 
@@ -248,8 +315,8 @@ const SqlEditor = ({
         key: 'ctrl+r',
         descr: t('Run query'),
         func: () => {
-          if (queryEditor.sql.trim() !== '') {
-            startQuery();
+          if (this.props.queryEditor.sql.trim() !== '') {
+            this.runQuery();
           }
         },
       },
@@ -258,8 +325,8 @@ const SqlEditor = ({
         key: 'ctrl+enter',
         descr: t('Run query'),
         func: () => {
-          if (queryEditor.sql.trim() !== '') {
-            startQuery();
+          if (this.props.queryEditor.sql.trim() !== '') {
+            this.runQuery();
           }
         },
       },
@@ -268,20 +335,18 @@ const SqlEditor = ({
         key: userOS === 'Windows' ? 'ctrl+q' : 'ctrl+t',
         descr: t('New tab'),
         func: () => {
-          const name = newQueryTabName(queryEditors || []);
-          dispatch(
-            addQueryEditor({
-              ...queryEditor,
-              name,
-            }),
-          );
+          const name = newQueryTabName(this.props.queryEditors || []);
+          this.props.addQueryEditor({
+            ...this.props.queryEditor,
+            name,
+          });
         },
       },
       {
         name: 'stopQuery',
         key: userOS === 'MacOS' ? 'ctrl+x' : 'ctrl+e',
         descr: t('Stop query'),
-        func: stopQuery,
+        func: this.stopQuery,
       },
     ];
 
@@ -297,170 +362,176 @@ const SqlEditor = ({
     }
 
     return base;
-  };
-
-  const handleWindowResize = () => {
-    setHeight(getSqlEditorHeight());
-  };
+  }
 
-  const handleWindowResizeWithThrottle = useMemo(
-    () => throttle(handleWindowResize, WINDOW_RESIZE_THROTTLE_MS),
-    [],
-  );
+  setEmptyState(bool) {
+    this.setState({ showEmptyState: bool });
+  }
 
-  const onBeforeUnload = event => {
-    if (
-      database?.extra_json?.cancel_query_on_windows_unload &&
-      latestQuery?.state === 'running'
-    ) {
-      event.preventDefault();
-      stopQuery();
-    }
-  };
+  setQueryEditorAndSaveSql(sql) {
+    this.props.queryEditorSetAndSaveSql(this.props.queryEditor, sql);
+  }
 
-  useEffect(() => {
-    // We need to measure the height of the sql editor post render to figure the height of
-    // the south pane so it gets rendered properly
-    setHeight(getSqlEditorHeight());
-    if (!database || isEmpty(database)) {
-      setShowEmptyState(true);
+  getQueryCostEstimate() {
+    if (this.props.database) {
+      const qe = this.props.queryEditor;
+      this.props.estimateQueryCost(qe);
     }
+  }
 
-    window.addEventListener('resize', handleWindowResizeWithThrottle);
-    window.addEventListener('beforeunload', onBeforeUnload);
-
-    // setup hotkeys
-    const hotkeys = getHotkeyConfig();
-    hotkeys.forEach(keyConfig => {
-      Mousetrap.bind([keyConfig.key], keyConfig.func);
+  handleToggleAutocompleteEnabled = () => {
+    this.setState(prevState => {
+      setItem(
+        LocalStorageKeys.sqllab__is_autocomplete_enabled,
+        !prevState.autocompleteEnabled,
+      );
+      return {
+        autocompleteEnabled: !prevState.autocompleteEnabled,
+      };
     });
-
-    return () => {
-      window.removeEventListener('resize', handleWindowResizeWithThrottle);
-      window.removeEventListener('beforeunload', onBeforeUnload);
-    };
-  }, []);
-
-  const onResizeStart = () => {
-    // Set the heights on the ace editor and the ace content area after drag starts
-    // to smooth out the visual transition to the new heights when drag ends
-    document.getElementsByClassName('ace_content')[0].style.height = '100%';
   };
 
-  const onResizeEnd = ([northPercent, southPercent]) => {
-    setNorthPercent(northPercent);
-    setSouthPercent(southPercent);
-
-    if (northPaneRef.current?.clientHeight) {
-      dispatch(persistEditorHeight(queryEditor, northPercent, southPercent));
-    }
-  };
+  handleWindowResize() {
+    this.setState({ height: this.getSqlEditorHeight() });
+  }
 
-  const setQueryEditorAndSaveSql = sql => {
-    dispatch(queryEditorSetAndSaveSql(queryEditor, sql));
-  };
+  elementStyle(dimension, elementSize, gutterSize) {
+    return {
+      [dimension]: `calc(${elementSize}% - ${
+        gutterSize + SQL_EDITOR_GUTTER_MARGIN
+      }px)`,
+    };
+  }
 
-  const setQueryEditorAndSaveSqlWithDebounce = useMemo(
-    () => debounce(setQueryEditorAndSaveSql, SET_QUERY_EDITOR_SQL_DEBOUNCE_MS),
-    [],
-  );
+  requestValidation(sql) {
+    const { database, queryEditor, validateQuery } = this.props;
+    if (database) {
+      validateQuery(queryEditor, sql);
+    }
+  }
 
-  const canValidateQuery = () => {
+  canValidateQuery() {
     // Check whether or not we can validate the current query based on whether
     // or not the backend has a validator configured for it.
-    if (database) {
-      return validatorMap.hasOwnProperty(database.backend);
+    if (this.props.database) {
+      return validatorMap.hasOwnProperty(this.props.database.backend);
     }
     return false;
-  };
+  }
 
-  const requestValidation = sql => {
-    if (database) {
-      dispatch(validateQuery(queryEditor, sql));
+  runQuery() {
+    if (this.props.database) {
+      this.startQuery();
     }
-  };
+  }
 
-  const requestValidationWithDebounce = useMemo(
-    () => debounce(requestValidation, VALIDATION_DEBOUNCE_MS),
-    [],
-  );
+  convertToNumWithSpaces(num) {
+    return num.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1 ');
+  }
 
-  const onSqlChanged = sql => {
-    dispatch(queryEditorSetSql(queryEditor, sql));
-    setQueryEditorAndSaveSqlWithDebounce(sql);
-    // Request server-side validation of the query text
-    if (canValidateQuery()) {
-      // NB. requestValidation is debounced
-      requestValidationWithDebounce(sql);
-    }
-  };
+  startQuery(ctas = false, ctas_method = CtasEnum.TABLE) {
+    const {
+      database,
+      runQueryFromSqlEditor,
+      setActiveSouthPaneTab,
+      queryEditor,
+      defaultQueryLimit,
+    } = this.props;
+    runQueryFromSqlEditor(
+      database,
+      queryEditor,
+      defaultQueryLimit,
+      ctas ? this.state.ctas : '',
+      ctas,
+      ctas_method,
+    );
+    setActiveSouthPaneTab('Results');
+  }
 
-  // Return the heights for the ace editor and the south pane as an object
-  // given the height of the sql editor, north pane percent and south pane percent.
-  const getAceEditorAndSouthPaneHeights = (
-    height,
-    northPercent,
-    southPercent,
-  ) => ({
-    aceEditorHeight:
-      (height * northPercent) / (theme.gridUnit * 25) -
-      (SQL_EDITOR_GUTTER_HEIGHT / 2 + SQL_EDITOR_GUTTER_MARGIN) -
-      SQL_TOOLBAR_HEIGHT,
-    southPaneHeight:
-      (height * southPercent) / (theme.gridUnit * 25) -
-      (SQL_EDITOR_GUTTER_HEIGHT / 2 + SQL_EDITOR_GUTTER_MARGIN),
-  });
-
-  const getQueryCostEstimate = () => {
-    if (database) {
-      dispatch(estimateQueryCost(queryEditor));
+  stopQuery() {
+    if (
+      this.props.latestQuery &&
+      ['running', 'pending'].indexOf(this.props.latestQuery.state) >= 0
+    ) {
+      this.props.postStopQuery(this.props.latestQuery);
     }
-  };
-
-  const handleToggleAutocompleteEnabled = () => {
-    setItem(
-      LocalStorageKeys.sqllab__is_autocomplete_enabled,
-      !autocompleteEnabled,
-    );
-    setAutocompleteEnabled(!autocompleteEnabled);
-  };
+  }
 
-  const elementStyle = (dimension, elementSize, gutterSize) => ({
-    [dimension]: `calc(${elementSize}% - ${
-      gutterSize + SQL_EDITOR_GUTTER_MARGIN
-    }px)`,
-  });
+  createTableAs() {
+    this.startQuery(true, CtasEnum.TABLE);
+    this.setState({ showCreateAsModal: false, ctas: '' });
+  }
 
-  const createTableAs = () => {
-    startQuery(true, CtasEnum.TABLE);
-    setShowCreateAsModal(false);
-    setCtas('');
-  };
+  createViewAs() {
+    this.startQuery(true, CtasEnum.VIEW);
+    this.setState({ showCreateAsModal: false, ctas: '' });
+  }
 
-  const createViewAs = () => {
-    startQuery(true, CtasEnum.VIEW);
-    setShowCreateAsModal(false);
-    setCtas('');
-  };
+  ctasChanged(event) {
+    this.setState({ ctas: event.target.value });
+  }
 
-  const ctasChanged = event => {
-    setCtas(event.target.value);
-  };
+  queryPane() {
+    const hotkeys = this.getHotkeyConfig();
+    const { aceEditorHeight, southPaneHeight } =
+      this.getAceEditorAndSouthPaneHeights(
+        this.state.height,
+        this.state.northPercent,
+        this.state.southPercent,
+      );
+    return (
+      <Split
+        expandToMin
+        className="queryPane"
+        sizes={[this.state.northPercent, this.state.southPercent]}
+        elementStyle={this.elementStyle}
+        minSize={200}
+        direction="vertical"
+        gutterSize={SQL_EDITOR_GUTTER_HEIGHT}
+        onDragStart={this.onResizeStart}
+        onDragEnd={this.onResizeEnd}
+      >
+        <div ref={this.northPaneRef} className="north-pane">
+          <AceEditorWrapper
+            actions={this.props.actions}
+            autocomplete={this.state.autocompleteEnabled}
+            onBlur={this.setQueryEditorSql}
+            onChange={this.onSqlChanged}
+            queryEditor={this.props.queryEditor}
+            database={this.props.database}
+            extendedTables={this.props.tables}
+            height={`${aceEditorHeight}px`}
+            hotkeys={hotkeys}
+          />
+          {this.renderEditorBottomBar(hotkeys)}
+        </div>
+        <ConnectedSouthPane
+          editorQueries={this.props.editorQueries}
+          latestQueryId={this.props.latestQuery && this.props.latestQuery.id}
+          dataPreviewQueries={this.props.dataPreviewQueries}
+          actions={this.props.actions}
+          height={southPaneHeight}
+          displayLimit={this.props.displayLimit}
+          defaultQueryLimit={this.props.defaultQueryLimit}
+        />
+      </Split>
+    );
+  }
 
-  const renderDropdown = () => {
-    const qe = queryEditor;
-    const successful = latestQuery?.state === 'success';
+  renderDropdown() {
+    const qe = this.props.queryEditor;
+    const successful = this.props.latestQuery?.state === 'success';
     const scheduleToolTip = successful
       ? t('Schedule the query periodically')
       : t('You must run the query successfully first');
     return (
-      <Menu css={{ width: theme.gridUnit * 44 }}>
-        <Menu.Item css={{ display: 'flex', justifyContent: 'space-between' }}>
+      <Menu onClick={this.handleMenuClick} style={{ width: 176 }}>
+        <Menu.Item style={{ display: 'flex', justifyContent: 'space-between' }}>
           {' '}
           <span>{t('Autocomplete')}</span>{' '}
           <AntdSwitch
-            checked={autocompleteEnabled}
-            onChange={handleToggleAutocompleteEnabled}
+            checked={this.state.autocompleteEnabled}
+            onChange={this.handleToggleAutocompleteEnabled}
             name="autocomplete-switch"
           />{' '}
         </Menu.Item>
@@ -469,7 +540,7 @@ const SqlEditor = ({
             <TemplateParamsEditor
               language="json"
               onChange={params => {
-                dispatch(queryEditorSetTemplateParams(qe, params));
+                this.props.actions.queryEditorSetTemplateParams(qe, params);
               }}
               queryEditor={qe}
             />
@@ -480,10 +551,10 @@ const SqlEditor = ({
             <ScheduleQueryButton
               defaultLabel={qe.name}
               sql={qe.sql}
-              onSchedule={query => dispatch(scheduleQuery(query))}
+              onSchedule={this.props.actions.scheduleQuery}
               schema={qe.schema}
               dbId={qe.dbId}
-              scheduleQueryWarning={scheduleQueryWarning}
+              scheduleQueryWarning={this.props.scheduleQueryWarning}
               tooltip={scheduleToolTip}
               disabled={!successful}
             />
@@ -491,24 +562,31 @@ const SqlEditor = ({
         )}
       </Menu>
     );
-  };
+  }
 
-  const onSaveQuery = async query => {
-    const savedQuery = await dispatch(saveQuery(query));
-    dispatch(addSavedQueryToTabState(queryEditor, savedQuery));
-  };
+  async saveQuery(query) {
+    const { queryEditor: qe, actions } = this.props;
+    const savedQuery = await actions.saveQuery(query);
+    actions.addSavedQueryToTabState(qe, savedQuery);
+  }
+
+  renderEditorBottomBar() {
+    const { queryEditor: qe } = this.props;
 
-  const renderEditorBottomBar = () => {
-    const { allow_ctas: allowCTAS, allow_cvas: allowCVAS } = database || {};
+    const { allow_ctas: allowCTAS, allow_cvas: allowCVAS } =
+      this.props.database || {};
 
     const showMenu = allowCTAS || allowCVAS;
+    const { theme } = this.props;
     const runMenuBtn = (
       <Menu>
         {allowCTAS && (
           <Menu.Item
             onClick={() => {
-              setShowCreateAsModal(true);
-              setCreateAs(CtasEnum.TABLE);
+              this.setState({
+                showCreateAsModal: true,
+                createAs: CtasEnum.TABLE,
+              });
             }}
             key="1"
           >
@@ -518,8 +596,10 @@ const SqlEditor = ({
         {allowCVAS && (
           <Menu.Item
             onClick={() => {
-              setShowCreateAsModal(true);
-              setCreateAs(CtasEnum.VIEW);
+              this.setState({
+                showCreateAsModal: true,
+                createAs: CtasEnum.VIEW,
+              });
             }}
             key="2"
           >
@@ -534,190 +614,214 @@ const SqlEditor = ({
         <div className="leftItems">
           <span>
             <RunQueryActionButton
-              allowAsync={database ? database.allow_run_async : false}
-              queryEditor={queryEditor}
-              queryState={latestQuery?.state}
-              runQuery={startQuery}
-              stopQuery={stopQuery}
+              allowAsync={
+                this.props.database
+                  ? this.props.database.allow_run_async
+                  : false
+              }
+              queryEditor={qe}
+              queryState={this.props.latestQuery?.state}
+              runQuery={this.runQuery}
+              stopQuery={this.stopQuery}
               overlayCreateAsMenu={showMenu ? runMenuBtn : null}
             />
           </span>
           {isFeatureEnabled(FeatureFlag.ESTIMATE_QUERY_COST) &&
-            database?.allows_cost_estimate && (
+            this.props.database &&
+            this.props.database.allows_cost_estimate && (
               <span>
                 <EstimateQueryCostButton
-                  getEstimate={getQueryCostEstimate}
-                  queryEditor={queryEditor}
+                  getEstimate={this.getQueryCostEstimate}
+                  queryEditor={qe}
                   tooltip={t('Estimate the cost before running a query')}
                 />
               </span>
             )}
           <span>
             <QueryLimitSelect
-              queryEditor={queryEditor}
-              maxRow={maxRow}
-              defaultQueryLimit={defaultQueryLimit}
+              queryEditor={this.props.queryEditor}
+              maxRow={this.props.maxRow}
+              defaultQueryLimit={this.props.defaultQueryLimit}
             />
           </span>
-          {latestQuery && (
+          {this.props.latestQuery && (
             <Timer
-              startTime={latestQuery.startDttm}
-              endTime={latestQuery.endDttm}
-              state={STATE_TYPE_MAP[latestQuery.state]}
-              isRunning={latestQuery.state === 'running'}
+              startTime={this.props.latestQuery.startDttm}
+              endTime={this.props.latestQuery.endDttm}
+              state={STATE_TYPE_MAP[this.props.latestQuery.state]}
+              isRunning={this.props.latestQuery.state === 'running'}
             />
           )}
         </div>
         <div className="rightItems">
           <span>
             <SaveQuery
-              queryEditor={queryEditor}
-              columns={latestQuery?.results?.columns || []}
-              onSave={onSaveQuery}
-              onUpdate={query => dispatch(updateSavedQuery(query))}
-              saveQueryWarning={saveQueryWarning}
-              database={database}
+              queryEditor={qe}
+              columns={this.props.latestQuery?.results?.columns || []}
+              onSave={this.saveQuery}
+              onUpdate={this.props.actions.updateSavedQuery}
+              saveQueryWarning={this.props.saveQueryWarning}
+              database={this.props.database}
             />
           </span>
           <span>
-            <ShareSqlLabQuery queryEditor={queryEditor} />
+            <ShareSqlLabQuery queryEditor={qe} />
           </span>
-          <AntdDropdown overlay={renderDropdown()} trigger="click">
+          <AntdDropdown overlay={this.renderDropdown()} trigger="click">
             <Icons.MoreHoriz iconColor={theme.colors.grayscale.base} />
           </AntdDropdown>
         </div>
       </StyledToolbar>
     );
-  };
+  }
 
-  const queryPane = () => {
-    const hotkeys = getHotkeyConfig();
-    const { aceEditorHeight, southPaneHeight } =
-      getAceEditorAndSouthPaneHeights(height, northPercent, southPercent);
+  render() {
+    const createViewModalTitle =
+      this.state.createAs === CtasEnum.VIEW
+        ? 'CREATE VIEW AS'
+        : 'CREATE TABLE AS';
+
+    const createModalPlaceHolder =
+      this.state.createAs === CtasEnum.VIEW
+        ? 'Specify name to CREATE VIEW AS schema in: public'
+        : 'Specify name to CREATE TABLE AS schema in: public';
+    const leftBarStateClass = this.props.hideLeftBar
+      ? 'schemaPane-exit-done'
+      : 'schemaPane-enter-done';
     return (
-      <Split
-        expandToMin
-        className="queryPane"
-        sizes={[northPercent, southPercent]}
-        elementStyle={elementStyle}
-        minSize={200}
-        direction="vertical"
-        gutterSize={SQL_EDITOR_GUTTER_HEIGHT}
-        onDragStart={onResizeStart}
-        onDragEnd={onResizeEnd}
-      >
-        <div ref={northPaneRef} className="north-pane">
-          <AceEditorWrapper
-            actions={actions}
-            autocomplete={autocompleteEnabled}
-            onBlur={setQueryEditorAndSaveSql}
-            onChange={onSqlChanged}
-            queryEditor={queryEditor}
-            database={database}
-            extendedTables={tables}
-            height={`${aceEditorHeight}px`}
-            hotkeys={hotkeys}
-          />
-          {renderEditorBottomBar(hotkeys)}
-        </div>
-        <ConnectedSouthPane
-          editorQueries={editorQueries}
-          latestQueryId={latestQuery?.id}
-          dataPreviewQueries={dataPreviewQueries}
-          actions={actions}
-          height={southPaneHeight}
-          displayLimit={displayLimit}
-          defaultQueryLimit={defaultQueryLimit}
-        />
-      </Split>
-    );
-  };
-
-  const createViewModalTitle =
-    createAs === CtasEnum.VIEW ? 'CREATE VIEW AS' : 'CREATE TABLE AS';
-
-  const createModalPlaceHolder =
-    createAs === CtasEnum.VIEW
-      ? t('Specify name to CREATE VIEW AS schema in: public')
-      : t('Specify name to CREATE TABLE AS schema in: public');
-
-  const leftBarStateClass = hideLeftBar
-    ? 'schemaPane-exit-done'
-    : 'schemaPane-enter-done';
-  return (
-    <div ref={sqlEditorRef} className="SqlEditor">
-      <CSSTransition classNames="schemaPane" in={!hideLeftBar} timeout={300}>
-        <ResizableSidebar
-          id={`sqllab:${queryEditor.id}`}
-          minWidth={SQL_EDITOR_LEFTBAR_WIDTH}
-          initialWidth={SQL_EDITOR_LEFTBAR_WIDTH}
-          enable={!hideLeftBar}
+      <div ref={this.sqlEditorRef} className="SqlEditor">
+        <CSSTransition
+          classNames="schemaPane"
+          in={!this.props.hideLeftBar}
+          timeout={300}
         >
-          {adjustedWidth => (
-            <StyledSidebar
-              className={`schemaPane ${leftBarStateClass}`}
-              width={adjustedWidth}
-              hide={hideLeftBar}
-            >
-              <SqlEditorLeftBar
-                database={database}
-                queryEditor={queryEditor}
-                tables={tables}
-                actions={actions}
-                setEmptyState={bool => setShowEmptyState(bool)}
-              />
-            </StyledSidebar>
-          )}
-        </ResizableSidebar>
-      </CSSTransition>
-      {showEmptyState ? (
-        <EmptyStateBig
-          image="vector.svg"
-          title={t('Select a database to write a query')}
-          description={t(
-            'Choose one of the available databases from the panel on the left.',
-          )}
-        />
-      ) : (
-        queryPane()
-      )}
-      <Modal
-        visible={showCreateAsModal}
-        title={t(createViewModalTitle)}
-        onHide={() => setShowCreateAsModal(false)}
-        footer={
-          <>
-            <Button onClick={() => setShowCreateAsModal(false)}>
-              {t('Cancel')}
-            </Button>
-            {createAs === CtasEnum.TABLE && (
-              <Button
-                buttonStyle="primary"
-                disabled={ctas.length === 0}
-                onClick={createTableAs}
+          <ResizableSidebar
+            id={`sqllab:${this.props.queryEditor.id}`}
+            minWidth={SQL_EDITOR_LEFTBAR_WIDTH}
+            initialWidth={SQL_EDITOR_LEFTBAR_WIDTH}
+            enable={!this.props.hideLeftBar}
+          >
+            {adjustedWidth => (
+              <StyledSidebar
+                className={`schemaPane ${leftBarStateClass}`}
+                width={adjustedWidth}
+                hide={this.props.hideLeftBar}
               >
-                {t('Create')}
-              </Button>
+                <SqlEditorLeftBar
+                  database={this.props.database}
+                  queryEditor={this.props.queryEditor}
+                  tables={this.props.tables}
+                  actions={this.props.actions}
+                  setEmptyState={this.setEmptyState}
+                />
+              </StyledSidebar>
             )}
-            {createAs === CtasEnum.VIEW && (
+          </ResizableSidebar>
+        </CSSTransition>
+        {this.state.showEmptyState ? (
+          <EmptyStateBig
+            image="vector.svg"
+            title={t('Select a database to write a query')}
+            description={t(
+              'Choose one of the available databases from the panel on the left.',
+            )}
+          />
+        ) : (
+          this.queryPane()
+        )}
+        <StyledModal
+          visible={this.state.showCreateAsModal}
+          title={t(createViewModalTitle)}
+          onHide={() => {
+            this.setState({ showCreateAsModal: false });
+          }}
+          footer={
+            <>
               <Button
-                buttonStyle="primary"
-                disabled={ctas.length === 0}
-                onClick={createViewAs}
+                onClick={() => this.setState({ showCreateAsModal: false })}
               >
-                {t('Create')}
+                Cancel
               </Button>
-            )}
-          </>
-        }
-      >
-        <span>{t('Name')}</span>
-        <Input placeholder={createModalPlaceHolder} onChange={ctasChanged} />
-      </Modal>
-    </div>
-  );
-};
-
+              {this.state.createAs === CtasEnum.TABLE && (
+                <Button
+                  buttonStyle="primary"
+                  disabled={this.state.ctas.length === 0}
+                  onClick={this.createTableAs.bind(this)}
+                >
+                  Create
+                </Button>
+              )}
+              {this.state.createAs === CtasEnum.VIEW && (
+                <Button
+                  buttonStyle="primary"
+                  disabled={this.state.ctas.length === 0}
+                  onClick={this.createViewAs.bind(this)}
+                >
+                  Create
+                </Button>
+              )}
+            </>
+          }
+        >
+          <span>Name</span>
+          <Input
+            placeholder={createModalPlaceHolder}
+            onChange={this.ctasChanged.bind(this)}
+          />
+        </StyledModal>
+      </div>
+    );
+  }
+}
+SqlEditor.defaultProps = defaultProps;
 SqlEditor.propTypes = propTypes;
 
-export default SqlEditor;
+function mapStateToProps({ sqlLab }, { queryEditor }) {
+  let { latestQueryId, dbId, hideLeftBar } = queryEditor;
+  if (sqlLab.unsavedQueryEditor.id === queryEditor.id) {
+    const {
+      latestQueryId: unsavedQID,
+      dbId: unsavedDBID,
+      hideLeftBar: unsavedHideLeftBar,
+    } = sqlLab.unsavedQueryEditor;
+    latestQueryId = unsavedQID || latestQueryId;
+    dbId = unsavedDBID || dbId;
+    hideLeftBar = unsavedHideLeftBar || hideLeftBar;
+  }
+  const database = sqlLab.databases[dbId];
+  const latestQuery = sqlLab.queries[latestQueryId];
+
+  return {
+    hideLeftBar,
+    queryEditors: sqlLab.queryEditors,
+    latestQuery,
+    database,
+  };
+}
+
+function mapDispatchToProps(dispatch) {
+  return bindActionCreators(
+    {
+      addQueryEditor,
+      estimateQueryCost,
+      persistEditorHeight,
+      postStopQuery,
+      queryEditorSetAutorun,
+      queryEditorSetSql,
+      queryEditorSetAndSaveSql,
+      queryEditorSetTemplateParams,
+      runQueryFromSqlEditor,
+      runQuery,
+      saveQuery,
+      addSavedQueryToTabState,
+      scheduleQuery,
+      setActiveSouthPaneTab,
+      updateSavedQuery,
+      validateQuery,
+    },
+    dispatch,
+  );
+}
+
+const themedSqlEditor = withTheme(SqlEditor);
+export default connect(mapStateToProps, mapDispatchToProps)(themedSqlEditor);
diff --git a/superset-frontend/src/SqlLab/constants.ts b/superset-frontend/src/SqlLab/constants.ts
index 29b0f6cf6b..11d990032d 100644
--- a/superset-frontend/src/SqlLab/constants.ts
+++ b/superset-frontend/src/SqlLab/constants.ts
@@ -49,12 +49,6 @@ export const SQL_EDITOR_GUTTER_HEIGHT = 5;
 export const SQL_EDITOR_GUTTER_MARGIN = 3;
 export const SQL_TOOLBAR_HEIGHT = 51;
 export const SQL_EDITOR_LEFTBAR_WIDTH = 400;
-export const SQL_EDITOR_PADDING = 10;
-export const INITIAL_NORTH_PERCENT = 30;
-export const INITIAL_SOUTH_PERCENT = 70;
-export const SET_QUERY_EDITOR_SQL_DEBOUNCE_MS = 2000;
-export const VALIDATION_DEBOUNCE_MS = 600;
-export const WINDOW_RESIZE_THROTTLE_MS = 100;
 
 // kilobyte storage
 export const KB_STORAGE = 1024;