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;