diff --git a/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx b/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx
index 3e8ee84f6c500..c48594d304841 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,102 +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 { currentQueryEditor, database, latestQuery, hideLeftBar } =
- useSelector(({ sqlLab: { unsavedQueryEditor, databases, queries } }) => {
- const currentQueryEditor = {
- ...queryEditor,
- ...(queryEditor.id === unsavedQueryEditor.id && unsavedQueryEditor),
- };
+const defaultProps = {
+ database: null,
+ latestQuery: null,
+ hideLeftBar: false,
+ scheduleQueryWarning: null,
+};
- let { dbId, latestQueryId, hideLeftBar } = queryEditor;
- if (unsavedQueryEditor.id === queryEditor.id) {
- dbId = unsavedQueryEditor.dbId || dbId;
- latestQueryId = unsavedQueryEditor.latestQueryId || latestQueryId;
- hideLeftBar = unsavedQueryEditor.hideLeftBar || hideLeftBar;
- }
- return {
- currentQueryEditor,
- 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,
+ );
+
+ this.onBeforeUnload = this.onBeforeUnload.bind(this);
+ this.renderDropdown = this.renderDropdown.bind(this);
+ }
+
+ UNSAFE_componentWillMount() {
+ if (this.state.autorun) {
+ this.setState({ autorun: false });
+ this.props.queryEditorSetAutorun(this.props.queryEditor, false);
+ this.startQuery();
+ }
+ }
+
+ 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);
+ }
+
+ 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);
});
+ }
- const queryEditors = useSelector(({ sqlLab }) => sqlLab.queryEditors);
+ componentWillUnmount() {
+ window.removeEventListener('resize', this.handleWindowResize);
+ window.removeEventListener('beforeunload', this.onBeforeUnload);
+ }
- 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);
+ 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 sqlEditorRef = useRef(null);
- const northPaneRef = useRef(null);
+ onResizeEnd([northPercent, southPercent]) {
+ this.setState({ northPercent, southPercent });
- const startQuery = (ctasArg = false, ctas_method = CtasEnum.TABLE) => {
- if (!database) {
- return;
+ 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'));
- };
-
- const stopQuery = () => {
- if (latestQuery && ['running', 'pending'].indexOf(latestQuery.state) >= 0) {
- dispatch(postStopQuery(latestQuery));
+ 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();
@@ -253,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();
}
},
},
@@ -263,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();
}
},
},
@@ -273,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,
},
];
@@ -302,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 (
+