diff --git a/superset/assets/javascripts/SqlLab/actions.js b/superset/assets/javascripts/SqlLab/actions.js index d1fbfea46fdb2..04a9a5eae4c4b 100644 --- a/superset/assets/javascripts/SqlLab/actions.js +++ b/superset/assets/javascripts/SqlLab/actions.js @@ -196,8 +196,11 @@ export function setDatabases(databases) { } export function addQueryEditor(queryEditor) { - const newQe = Object.assign({}, queryEditor, { id: shortid.generate() }); - return { type: ADD_QUERY_EDITOR, queryEditor: newQe }; + const newQueryEditor = { + ...queryEditor, + id: shortid.generate(), + }; + return { type: ADD_QUERY_EDITOR, queryEditor: newQueryEditor }; } export function cloneQueryToNewTab(query) { diff --git a/superset/assets/javascripts/SqlLab/components/AceEditorWrapper.jsx b/superset/assets/javascripts/SqlLab/components/AceEditorWrapper.jsx index a66d80ea6b4af..24e9e25db630c 100644 --- a/superset/assets/javascripts/SqlLab/components/AceEditorWrapper.jsx +++ b/superset/assets/javascripts/SqlLab/components/AceEditorWrapper.jsx @@ -28,16 +28,19 @@ const sqlWords = sqlKeywords.map(s => ({ const propTypes = { actions: PropTypes.object.isRequired, onBlur: PropTypes.func, - onAltEnter: PropTypes.func, sql: PropTypes.string.isRequired, tables: PropTypes.array, queryEditor: PropTypes.object.isRequired, height: PropTypes.string, + hotkeys: PropTypes.arrayOf(PropTypes.shape({ + key: PropTypes.string.isRequired, + descr: PropTypes.string.isRequired, + func: PropTypes.func.isRequired, + })), }; const defaultProps = { onBlur: () => {}, - onAltEnter: () => {}, tables: [], }; @@ -67,7 +70,6 @@ class AceEditorWrapper extends React.PureComponent { } onAltEnter() { this.props.onBlur(this.state.sql); - this.props.onAltEnter(); } onEditorLoad(editor) { editor.commands.addCommand({ @@ -77,6 +79,13 @@ class AceEditorWrapper extends React.PureComponent { this.onAltEnter(); }, }); + this.props.hotkeys.forEach((keyConfig) => { + editor.commands.addCommand({ + name: keyConfig.name, + bindKey: { win: keyConfig.key, mac: keyConfig.key }, + exec: keyConfig.func, + }); + }); editor.$blockScrolling = Infinity; // eslint-disable-line no-param-reassign editor.selection.on('changeSelection', () => { const selectedText = editor.getSelectedText(); diff --git a/superset/assets/javascripts/SqlLab/components/SqlEditor.jsx b/superset/assets/javascripts/SqlLab/components/SqlEditor.jsx index 5a2cd04232e27..57be300b43e6f 100644 --- a/superset/assets/javascripts/SqlLab/components/SqlEditor.jsx +++ b/superset/assets/javascripts/SqlLab/components/SqlEditor.jsx @@ -21,6 +21,7 @@ import SouthPane from './SouthPane'; import SaveQuery from './SaveQuery'; import ShareQuery from './ShareQuery'; import Timer from '../../components/Timer'; +import Hotkeys from '../../components/Hotkeys'; import SqlEditorLeftBar from './SqlEditorLeftBar'; import AceEditorWrapper from './AceEditorWrapper'; import { STATE_BSSTYLE_MAP } from '../constants'; @@ -46,7 +47,6 @@ const defaultProps = { hideLeftBar: false, }; - class SqlEditor extends React.PureComponent { constructor(props) { super(props); @@ -57,6 +57,8 @@ class SqlEditor extends React.PureComponent { this.onResize = this.onResize.bind(this); this.throttledResize = throttle(this.onResize, 250); + this.runQuery = this.runQuery.bind(this); + this.stopQuery = this.stopQuery.bind(this); } componentWillMount() { if (this.state.autorun) { @@ -86,18 +88,39 @@ class SqlEditor extends React.PureComponent { this.props.actions.persistEditorHeight(this.props.queryEditor, this.refs.ace.clientHeight); } } + getHotkeyConfig() { + return [ + { + name: 'runQuery', + key: 'ctrl+r', + descr: 'Run query', + func: this.runQuery, + }, + { + name: 'newTab', + key: 'ctrl+t', + descr: 'New tab', + func: () => { + this.props.actions.addQueryEditor({ + ...this.props.queryEditor, + title: t('Untitled Query'), + sql: '', + }); + }, + }, + { + name: 'stopQuery', + key: 'ctrl+x', + descr: 'Stop query', + func: this.stopQuery, + }, + ]; + } setQueryEditorSql(sql) { this.props.actions.queryEditorSetSql(this.props.queryEditor, sql); } - runQuery(runAsync = false) { - if (!this.props.queryEditor.sql) { - return; - } - let effectiveRunAsync = runAsync; - if (!this.props.database.allow_run_sync) { - effectiveRunAsync = true; - } - this.startQuery(effectiveRunAsync); + runQuery() { + this.startQuery(!this.props.database.allow_run_sync); } startQuery(runAsync = false, ctas = false) { const qe = this.props.queryEditor; @@ -116,7 +139,9 @@ class SqlEditor extends React.PureComponent { this.props.actions.setActiveSouthPaneTab('Results'); } stopQuery() { - this.props.actions.postStopQuery(this.props.latestQuery); + if (this.props.latestQuery && this.props.latestQuery.state === 'running') { + this.props.actions.postStopQuery(this.props.latestQuery); + } } createTableAs() { this.startQuery(true, true); @@ -128,7 +153,7 @@ class SqlEditor extends React.PureComponent { const horizontalScrollbarHeight = 25; return parseInt(this.props.getHeight(), 10) - horizontalScrollbarHeight; } - renderEditorBottomBar() { + renderEditorBottomBar(hotkeys) { let ctasControls; if (this.props.database && this.props.database.allow_ctas) { const ctasToolTip = t('Create table as with query results'); @@ -181,9 +206,9 @@ class SqlEditor extends React.PureComponent { allowAsync={this.props.database ? this.props.database.allow_run_async : false} dbId={qe.dbId} queryState={this.props.latestQuery && this.props.latestQuery.state} - runQuery={this.runQuery.bind(this)} + runQuery={this.runQuery} selectedText={qe.selectedText} - stopQuery={this.stopQuery.bind(this)} + stopQuery={this.stopQuery} /> @@ -200,6 +225,12 @@ class SqlEditor extends React.PureComponent { {ctasControls} + + +
@@ -226,6 +257,7 @@ class SqlEditor extends React.PureComponent { render() { const height = this.sqlEditorHeight(); const defaultNorthHeight = this.props.queryEditor.height || 200; + const hotkeys = this.getHotkeyConfig(); return (
- {this.renderEditorBottomBar()} + {this.renderEditorBottomBar(hotkeys)}
diff --git a/superset/assets/javascripts/components/Hotkeys.jsx b/superset/assets/javascripts/components/Hotkeys.jsx new file mode 100644 index 0000000000000..f6ea1144183f6 --- /dev/null +++ b/superset/assets/javascripts/components/Hotkeys.jsx @@ -0,0 +1,53 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { OverlayTrigger, Popover } from 'react-bootstrap'; +import { Table } from 'reactable'; + +import Mousetrap from 'mousetrap'; + +const propTypes = { + hotkeys: PropTypes.arrayOf(PropTypes.shape({ + key: PropTypes.string.isRequired, + descr: PropTypes.string.isRequired, + func: PropTypes.func.isRequired, + })).isRequired, + header: PropTypes.string, +}; + +const defaultProps = { + hotkeys: [], +}; + +export default class Hotkeys extends React.PureComponent { + componentDidMount() { + this.props.hotkeys.forEach((keyConfig) => { + Mousetrap.bind([keyConfig.key], keyConfig.func); + }); + } + renderPopover() { + return ( + + ({ + Key: keyConfig.key, + Action: keyConfig.descr, + }))} + /> + ); + } + render() { + return ( + + + + ); + } +} + +Hotkeys.propTypes = propTypes; +Hotkeys.defaultProps = defaultProps;