diff --git a/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx b/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx
index c48594d304841..d6947c1ba5f15 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 (
-