You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ly...@apache.org on 2022/09/20 13:41:29 UTC

[superset] branch master updated: chore: refactor SqlEditor to functional component (#21320)

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

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


The following commit(s) were added to refs/heads/master by this push:
     new 2224ebecfe chore: refactor SqlEditor to functional component (#21320)
2224ebecfe is described below

commit 2224ebecfe7af0a4fa3736c33d697f6f41a07e46
Author: EugeneTorap <ev...@gmail.com>
AuthorDate: Tue Sep 20 16:41:14 2022 +0300

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

diff --git a/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx b/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx
index c48594d304..d6947c1ba5 100644
--- a/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx
+++ b/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx
@@ -18,16 +18,15 @@
  */
 /* eslint-disable jsx-a11y/anchor-is-valid */
 /* eslint-disable jsx-a11y/no-static-element-interactions */
-import React from 'react';
+import React, { useState, useEffect, useMemo, useRef } from 'react';
 import { CSSTransition } from 'react-transition-group';
-import { connect } from 'react-redux';
-import { bindActionCreators } from 'redux';
+import { useDispatch, useSelector } from 'react-redux';
 import PropTypes from 'prop-types';
 import Split from 'react-split';
-import { t, styled, withTheme } from '@superset-ui/core';
+import { t, styled, useTheme } from '@superset-ui/core';
 import debounce from 'lodash/debounce';
 import throttle from 'lodash/throttle';
-import StyledModal from 'src/components/Modal';
+import Modal from 'src/components/Modal';
 import Mousetrap from 'mousetrap';
 import Button from 'src/components/Button';
 import Timer from 'src/components/Timer';
@@ -48,7 +47,6 @@ import {
   queryEditorSetAndSaveSql,
   queryEditorSetTemplateParams,
   runQueryFromSqlEditor,
-  runQuery,
   saveQuery,
   addSavedQueryToTabState,
   scheduleQuery,
@@ -62,6 +60,12 @@ 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,
@@ -83,13 +87,6 @@ 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') || '{}',
@@ -132,7 +129,7 @@ const StyledToolbar = styled.div`
 const StyledSidebar = styled.div`
   flex: 0 0 ${({ width }) => width}px;
   width: ${({ width }) => width}px;
-  padding: ${({ hide }) => (hide ? 0 : 10)}px;
+  padding: ${({ theme, hide }) => (hide ? 0 : theme.gridUnit * 2.5)}px;
   border-right: 1px solid
     ${({ theme, hide }) =>
       hide ? 'transparent' : theme.colors.grayscale.light2};
@@ -140,13 +137,10 @@ 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,
@@ -154,158 +148,97 @@ const propTypes = {
   scheduleQueryWarning: PropTypes.string,
 };
 
-const defaultProps = {
-  database: null,
-  latestQuery: null,
-  hideLeftBar: false,
-  scheduleQueryWarning: null,
-};
+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,
+      };
+    },
+  );
 
-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 queryEditors = useSelector(({ sqlLab }) => sqlLab.queryEditors);
 
-    this.onBeforeUnload = this.onBeforeUnload.bind(this);
-    this.renderDropdown = this.renderDropdown.bind(this);
-  }
+  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);
 
-  UNSAFE_componentWillMount() {
-    if (this.state.autorun) {
-      this.setState({ autorun: false });
-      this.props.queryEditorSetAutorun(this.props.queryEditor, false);
-      this.startQuery();
-    }
-  }
+  const sqlEditorRef = useRef(null);
+  const northPaneRef = useRef(null);
 
-  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);
+  const startQuery = (ctasArg = false, ctas_method = CtasEnum.TABLE) => {
+    if (!database) {
+      return;
     }
 
-    window.addEventListener('resize', this.handleWindowResize);
-    window.addEventListener('beforeunload', this.onBeforeUnload);
-
-    // 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,
-      );
-    }
-  }
+    dispatch(
+      runQueryFromSqlEditor(
+        database,
+        queryEditor,
+        defaultQueryLimit,
+        ctasArg ? ctas : '',
+        ctasArg,
+        ctas_method,
+      ),
+    );
+    dispatch(setActiveSouthPaneTab('Results'));
+  };
 
-  onBeforeUnload(event) {
-    if (
-      this.props.database?.extra_json?.cancel_query_on_windows_unload &&
-      this.props.latestQuery?.state === 'running'
-    ) {
-      event.preventDefault();
-      this.stopQuery();
+  const stopQuery = () => {
+    if (latestQuery && ['running', 'pending'].indexOf(latestQuery.state) >= 0) {
+      dispatch(postStopQuery(latestQuery));
     }
-  }
+  };
 
-  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);
+  useState(() => {
+    if (autorun) {
+      setAutorun(false);
+      dispatch(queryEditorSetAutorun(queryEditor, false));
+      startQuery();
     }
-  }
+  });
 
   // One layer of abstraction for easy spying in unit tests
-  getSqlEditorHeight() {
-    return this.sqlEditorRef.current
-      ? this.sqlEditorRef.current.clientHeight - SQL_EDITOR_PADDING * 2
+  const getSqlEditorHeight = () =>
+    sqlEditorRef.current
+      ? sqlEditorRef.current.clientHeight - SQL_EDITOR_PADDING * 2
       : 0;
-  }
 
-  // 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() {
+  const getHotkeyConfig = () => {
     // Get the user's OS
     const userOS = detectOS();
 
@@ -315,8 +248,8 @@ class SqlEditor extends React.PureComponent {
         key: 'ctrl+r',
         descr: t('Run query'),
         func: () => {
-          if (this.props.queryEditor.sql.trim() !== '') {
-            this.runQuery();
+          if (queryEditor.sql.trim() !== '') {
+            startQuery();
           }
         },
       },
@@ -325,8 +258,8 @@ class SqlEditor extends React.PureComponent {
         key: 'ctrl+enter',
         descr: t('Run query'),
         func: () => {
-          if (this.props.queryEditor.sql.trim() !== '') {
-            this.runQuery();
+          if (queryEditor.sql.trim() !== '') {
+            startQuery();
           }
         },
       },
@@ -335,18 +268,20 @@ class SqlEditor extends React.PureComponent {
         key: userOS === 'Windows' ? 'ctrl+q' : 'ctrl+t',
         descr: t('New tab'),
         func: () => {
-          const name = newQueryTabName(this.props.queryEditors || []);
-          this.props.addQueryEditor({
-            ...this.props.queryEditor,
-            name,
-          });
+          const name = newQueryTabName(queryEditors || []);
+          dispatch(
+            addQueryEditor({
+              ...queryEditor,
+              name,
+            }),
+          );
         },
       },
       {
         name: 'stopQuery',
         key: userOS === 'MacOS' ? 'ctrl+x' : 'ctrl+e',
         descr: t('Stop query'),
-        func: this.stopQuery,
+        func: stopQuery,
       },
     ];
 
@@ -362,176 +297,170 @@ class SqlEditor extends React.PureComponent {
     }
 
     return base;
-  }
+  };
 
-  setEmptyState(bool) {
-    this.setState({ showEmptyState: bool });
-  }
+  const handleWindowResize = () => {
+    setHeight(getSqlEditorHeight());
+  };
 
-  setQueryEditorAndSaveSql(sql) {
-    this.props.queryEditorSetAndSaveSql(this.props.queryEditor, sql);
-  }
+  const handleWindowResizeWithThrottle = useMemo(
+    () => throttle(handleWindowResize, WINDOW_RESIZE_THROTTLE_MS),
+    [],
+  );
 
-  getQueryCostEstimate() {
-    if (this.props.database) {
-      const qe = this.props.queryEditor;
-      this.props.estimateQueryCost(qe);
+  const onBeforeUnload = event => {
+    if (
+      database?.extra_json?.cancel_query_on_windows_unload &&
+      latestQuery?.state === 'running'
+    ) {
+      event.preventDefault();
+      stopQuery();
     }
-  }
-
-  handleToggleAutocompleteEnabled = () => {
-    this.setState(prevState => {
-      setItem(
-        LocalStorageKeys.sqllab__is_autocomplete_enabled,
-        !prevState.autocompleteEnabled,
-      );
-      return {
-        autocompleteEnabled: !prevState.autocompleteEnabled,
-      };
-    });
   };
 
-  handleWindowResize() {
-    this.setState({ height: this.getSqlEditorHeight() });
-  }
+  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);
+    }
+
+    window.addEventListener('resize', handleWindowResizeWithThrottle);
+    window.addEventListener('beforeunload', onBeforeUnload);
 
-  elementStyle(dimension, elementSize, gutterSize) {
-    return {
-      [dimension]: `calc(${elementSize}% - ${
-        gutterSize + SQL_EDITOR_GUTTER_MARGIN
-      }px)`,
+    // setup hotkeys
+    const hotkeys = getHotkeyConfig();
+    hotkeys.forEach(keyConfig => {
+      Mousetrap.bind([keyConfig.key], keyConfig.func);
+    });
+
+    return () => {
+      window.removeEventListener('resize', handleWindowResizeWithThrottle);
+      window.removeEventListener('beforeunload', onBeforeUnload);
     };
-  }
+  }, []);
 
-  requestValidation(sql) {
-    const { database, queryEditor, validateQuery } = this.props;
-    if (database) {
-      validateQuery(queryEditor, sql);
+  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));
     }
-  }
+  };
+
+  const setQueryEditorAndSaveSql = sql => {
+    dispatch(queryEditorSetAndSaveSql(queryEditor, sql));
+  };
+
+  const setQueryEditorAndSaveSqlWithDebounce = useMemo(
+    () => debounce(setQueryEditorAndSaveSql, SET_QUERY_EDITOR_SQL_DEBOUNCE_MS),
+    [],
+  );
 
-  canValidateQuery() {
+  const 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 (this.props.database) {
-      return validatorMap.hasOwnProperty(this.props.database.backend);
+    if (database) {
+      return validatorMap.hasOwnProperty(database.backend);
     }
     return false;
-  }
+  };
 
-  runQuery() {
-    if (this.props.database) {
-      this.startQuery();
+  const requestValidation = sql => {
+    if (database) {
+      dispatch(validateQuery(queryEditor, sql));
     }
-  }
+  };
 
-  convertToNumWithSpaces(num) {
-    return num.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1 ');
-  }
+  const requestValidationWithDebounce = useMemo(
+    () => debounce(requestValidation, VALIDATION_DEBOUNCE_MS),
+    [],
+  );
 
-  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');
-  }
+  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);
+    }
+  };
 
-  stopQuery() {
-    if (
-      this.props.latestQuery &&
-      ['running', 'pending'].indexOf(this.props.latestQuery.state) >= 0
-    ) {
-      this.props.postStopQuery(this.props.latestQuery);
+  // 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));
     }
-  }
+  };
 
-  createTableAs() {
-    this.startQuery(true, CtasEnum.TABLE);
-    this.setState({ showCreateAsModal: false, ctas: '' });
-  }
+  const handleToggleAutocompleteEnabled = () => {
+    setItem(
+      LocalStorageKeys.sqllab__is_autocomplete_enabled,
+      !autocompleteEnabled,
+    );
+    setAutocompleteEnabled(!autocompleteEnabled);
+  };
 
-  createViewAs() {
-    this.startQuery(true, CtasEnum.VIEW);
-    this.setState({ showCreateAsModal: false, ctas: '' });
-  }
+  const elementStyle = (dimension, elementSize, gutterSize) => ({
+    [dimension]: `calc(${elementSize}% - ${
+      gutterSize + SQL_EDITOR_GUTTER_MARGIN
+    }px)`,
+  });
 
-  ctasChanged(event) {
-    this.setState({ ctas: event.target.value });
-  }
+  const createTableAs = () => {
+    startQuery(true, CtasEnum.TABLE);
+    setShowCreateAsModal(false);
+    setCtas('');
+  };
 
-  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 createViewAs = () => {
+    startQuery(true, CtasEnum.VIEW);
+    setShowCreateAsModal(false);
+    setCtas('');
+  };
+
+  const ctasChanged = event => {
+    setCtas(event.target.value);
+  };
 
-  renderDropdown() {
-    const qe = this.props.queryEditor;
-    const successful = this.props.latestQuery?.state === 'success';
+  const renderDropdown = () => {
+    const qe = queryEditor;
+    const successful = latestQuery?.state === 'success';
     const scheduleToolTip = successful
       ? t('Schedule the query periodically')
       : t('You must run the query successfully first');
     return (
-      <Menu onClick={this.handleMenuClick} style={{ width: 176 }}>
-        <Menu.Item style={{ display: 'flex', justifyContent: 'space-between' }}>
+      <Menu css={{ width: theme.gridUnit * 44 }}>
+        <Menu.Item css={{ display: 'flex', justifyContent: 'space-between' }}>
           {' '}
           <span>{t('Autocomplete')}</span>{' '}
           <AntdSwitch
-            checked={this.state.autocompleteEnabled}
-            onChange={this.handleToggleAutocompleteEnabled}
+            checked={autocompleteEnabled}
+            onChange={handleToggleAutocompleteEnabled}
             name="autocomplete-switch"
           />{' '}
         </Menu.Item>
@@ -540,7 +469,7 @@ class SqlEditor extends React.PureComponent {
             <TemplateParamsEditor
               language="json"
               onChange={params => {
-                this.props.actions.queryEditorSetTemplateParams(qe, params);
+                dispatch(queryEditorSetTemplateParams(qe, params));
               }}
               queryEditor={qe}
             />
@@ -551,10 +480,10 @@ class SqlEditor extends React.PureComponent {
             <ScheduleQueryButton
               defaultLabel={qe.name}
               sql={qe.sql}
-              onSchedule={this.props.actions.scheduleQuery}
+              onSchedule={query => dispatch(scheduleQuery(query))}
               schema={qe.schema}
               dbId={qe.dbId}
-              scheduleQueryWarning={this.props.scheduleQueryWarning}
+              scheduleQueryWarning={scheduleQueryWarning}
               tooltip={scheduleToolTip}
               disabled={!successful}
             />
@@ -562,31 +491,24 @@ class SqlEditor extends React.PureComponent {
         )}
       </Menu>
     );
-  }
-
-  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 onSaveQuery = async query => {
+    const savedQuery = await dispatch(saveQuery(query));
+    dispatch(addSavedQueryToTabState(queryEditor, savedQuery));
+  };
 
-    const { allow_ctas: allowCTAS, allow_cvas: allowCVAS } =
-      this.props.database || {};
+  const renderEditorBottomBar = () => {
+    const { allow_ctas: allowCTAS, allow_cvas: allowCVAS } = database || {};
 
     const showMenu = allowCTAS || allowCVAS;
-    const { theme } = this.props;
     const runMenuBtn = (
       <Menu>
         {allowCTAS && (
           <Menu.Item
             onClick={() => {
-              this.setState({
-                showCreateAsModal: true,
-                createAs: CtasEnum.TABLE,
-              });
+              setShowCreateAsModal(true);
+              setCreateAs(CtasEnum.TABLE);
             }}
             key="1"
           >
@@ -596,10 +518,8 @@ class SqlEditor extends React.PureComponent {
         {allowCVAS && (
           <Menu.Item
             onClick={() => {
-              this.setState({
-                showCreateAsModal: true,
-                createAs: CtasEnum.VIEW,
-              });
+              setShowCreateAsModal(true);
+              setCreateAs(CtasEnum.VIEW);
             }}
             key="2"
           >
@@ -614,214 +534,190 @@ class SqlEditor extends React.PureComponent {
         <div className="leftItems">
           <span>
             <RunQueryActionButton
-              allowAsync={
-                this.props.database
-                  ? this.props.database.allow_run_async
-                  : false
-              }
-              queryEditor={qe}
-              queryState={this.props.latestQuery?.state}
-              runQuery={this.runQuery}
-              stopQuery={this.stopQuery}
+              allowAsync={database ? database.allow_run_async : false}
+              queryEditor={queryEditor}
+              queryState={latestQuery?.state}
+              runQuery={startQuery}
+              stopQuery={stopQuery}
               overlayCreateAsMenu={showMenu ? runMenuBtn : null}
             />
           </span>
           {isFeatureEnabled(FeatureFlag.ESTIMATE_QUERY_COST) &&
-            this.props.database &&
-            this.props.database.allows_cost_estimate && (
+            database?.allows_cost_estimate && (
               <span>
                 <EstimateQueryCostButton
-                  getEstimate={this.getQueryCostEstimate}
-                  queryEditor={qe}
+                  getEstimate={getQueryCostEstimate}
+                  queryEditor={queryEditor}
                   tooltip={t('Estimate the cost before running a query')}
                 />
               </span>
             )}
           <span>
             <QueryLimitSelect
-              queryEditor={this.props.queryEditor}
-              maxRow={this.props.maxRow}
-              defaultQueryLimit={this.props.defaultQueryLimit}
+              queryEditor={queryEditor}
+              maxRow={maxRow}
+              defaultQueryLimit={defaultQueryLimit}
             />
           </span>
-          {this.props.latestQuery && (
+          {latestQuery && (
             <Timer
-              startTime={this.props.latestQuery.startDttm}
-              endTime={this.props.latestQuery.endDttm}
-              state={STATE_TYPE_MAP[this.props.latestQuery.state]}
-              isRunning={this.props.latestQuery.state === 'running'}
+              startTime={latestQuery.startDttm}
+              endTime={latestQuery.endDttm}
+              state={STATE_TYPE_MAP[latestQuery.state]}
+              isRunning={latestQuery.state === 'running'}
             />
           )}
         </div>
         <div className="rightItems">
           <span>
             <SaveQuery
-              queryEditor={qe}
-              columns={this.props.latestQuery?.results?.columns || []}
-              onSave={this.saveQuery}
-              onUpdate={this.props.actions.updateSavedQuery}
-              saveQueryWarning={this.props.saveQueryWarning}
-              database={this.props.database}
+              queryEditor={queryEditor}
+              columns={latestQuery?.results?.columns || []}
+              onSave={onSaveQuery}
+              onUpdate={query => dispatch(updateSavedQuery(query))}
+              saveQueryWarning={saveQueryWarning}
+              database={database}
             />
           </span>
           <span>
-            <ShareSqlLabQuery queryEditor={qe} />
+            <ShareSqlLabQuery queryEditor={queryEditor} />
           </span>
-          <AntdDropdown overlay={this.renderDropdown()} trigger="click">
+          <AntdDropdown overlay={renderDropdown()} trigger="click">
             <Icons.MoreHoriz iconColor={theme.colors.grayscale.base} />
           </AntdDropdown>
         </div>
       </StyledToolbar>
     );
-  }
+  };
 
-  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';
+  const queryPane = () => {
+    const hotkeys = getHotkeyConfig();
+    const { aceEditorHeight, southPaneHeight } =
+      getAceEditorAndSouthPaneHeights(height, northPercent, southPercent);
     return (
-      <div ref={this.sqlEditorRef} className="SqlEditor">
-        <CSSTransition
-          classNames="schemaPane"
-          in={!this.props.hideLeftBar}
-          timeout={300}
+      <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}
         >
-          <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}
+          {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}
               >
-                <SqlEditorLeftBar
-                  database={this.props.database}
-                  queryEditor={this.props.queryEditor}
-                  tables={this.props.tables}
-                  actions={this.props.actions}
-                  setEmptyState={this.setEmptyState}
-                />
-              </StyledSidebar>
-            )}
-          </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.',
+                {t('Create')}
+              </Button>
             )}
-          />
-        ) : (
-          this.queryPane()
-        )}
-        <StyledModal
-          visible={this.state.showCreateAsModal}
-          title={t(createViewModalTitle)}
-          onHide={() => {
-            this.setState({ showCreateAsModal: false });
-          }}
-          footer={
-            <>
+            {createAs === CtasEnum.VIEW && (
               <Button
-                onClick={() => this.setState({ showCreateAsModal: false })}
+                buttonStyle="primary"
+                disabled={ctas.length === 0}
+                onClick={createViewAs}
               >
-                Cancel
+                {t('Create')}
               </Button>
-              {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;
-
-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,
+            )}
+          </>
+        }
+      >
+        <span>{t('Name')}</span>
+        <Input placeholder={createModalPlaceHolder} onChange={ctasChanged} />
+      </Modal>
+    </div>
   );
-}
+};
+
+SqlEditor.propTypes = propTypes;
 
-const themedSqlEditor = withTheme(SqlEditor);
-export default connect(mapStateToProps, mapDispatchToProps)(themedSqlEditor);
+export default SqlEditor;
diff --git a/superset-frontend/src/SqlLab/constants.ts b/superset-frontend/src/SqlLab/constants.ts
index 11d990032d..29b0f6cf6b 100644
--- a/superset-frontend/src/SqlLab/constants.ts
+++ b/superset-frontend/src/SqlLab/constants.ts
@@ -49,6 +49,12 @@ 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;