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;