diff --git a/superset/assets/javascripts/SqlLab/components/AceEditorWrapper.jsx b/superset/assets/javascripts/SqlLab/components/AceEditorWrapper.jsx index 15adf0b80d..925e8fd805 100644 --- a/superset/assets/javascripts/SqlLab/components/AceEditorWrapper.jsx +++ b/superset/assets/javascripts/SqlLab/components/AceEditorWrapper.jsx @@ -32,6 +32,7 @@ const propTypes = { sql: PropTypes.string.isRequired, tables: PropTypes.array, queryEditor: PropTypes.object.isRequired, + height: PropTypes.string, }; const defaultProps = { @@ -119,14 +120,15 @@ class AceEditorWrapper extends React.PureComponent { this.setState({ sql: text }); } render() { + const { height } = this.props; return ( col.name)} - height={this.state.height} + height={tableHeight} filterText={this.state.searchText} /> diff --git a/superset/assets/javascripts/SqlLab/components/SouthPane.jsx b/superset/assets/javascripts/SqlLab/components/SouthPane.jsx index 6773ac7827..50502d1657 100644 --- a/superset/assets/javascripts/SqlLab/components/SouthPane.jsx +++ b/superset/assets/javascripts/SqlLab/components/SouthPane.jsx @@ -4,6 +4,7 @@ import shortid from 'shortid'; import { Alert, Tab, Tabs } from 'react-bootstrap'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; +import { AutoSizer } from 'react-virtualized'; import * as Actions from '../actions'; import QueryHistory from './QueryHistory'; @@ -55,6 +56,7 @@ class SouthPane extends React.PureComponent { return window.innerHeight - sum - 95; } + switchTab(id) { this.props.actions.setActiveSouthPaneTab(id); } @@ -67,13 +69,28 @@ class SouthPane extends React.PureComponent { let results; if (latestQuery) { results = ( - + + {({ height }) => { + /* + checking of the height probably won't be necessary + after release of react-virtualized v10 + */ + if (height !== 0) { + return ( + + ); + } + return
; + }} + ); } else { results = Run a query to display results here; @@ -85,20 +102,36 @@ class SouthPane extends React.PureComponent { eventKey={query.id} key={query.id} > - + + {({ height }) => { + /* + checking of the height probably won't be necessary + after release of react-virtualized v10 + */ + if (height !== 0) { + return ( + + ); + } + return
; + }} + )); return (
-
+
diff --git a/superset/assets/javascripts/SqlLab/components/SplitPane.jsx b/superset/assets/javascripts/SqlLab/components/SplitPane.jsx new file mode 100644 index 0000000000..d751e973a8 --- /dev/null +++ b/superset/assets/javascripts/SqlLab/components/SplitPane.jsx @@ -0,0 +1,146 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import delay from 'lodash.delay'; +import { getTopOffset } from '../../../utils/common'; + + +const propTypes = { + north: PropTypes.object.isRequired, + south: PropTypes.object.isRequired, + minHeight: PropTypes.number, + onSizeChange: PropTypes.func, +}; + +class SplitPane extends React.PureComponent { + constructor(props) { + super(props); + this.state = { + dragging: false, + }; + + this.handleDraggingStart = this.handleDraggingStart.bind(this); + this.handleMouseUp = this.handleMouseUp.bind(this); + this.handleMouseMove = this.handleMouseMove.bind(this); + this.handleResize = this.handleResize.bind(this); + } + + componentDidMount() { + window.addEventListener('mouseup', this.handleMouseUp); + window.addEventListener('mousemove', this.handleMouseMove); + window.addEventListener('resize', this.handleResize); + + this.initSize(); + } + + componentWillUnmount() { + window.removeEventListener('mouseup', this.handleMouseUp); + window.removeEventListener('mousemove', this.handleMouseMove); + window.removeEventListener('resize', this.handleResize); + } + + setSize(northInPercent, southInPercent) { + const totalHeight = this.refs.splitter.clientHeight - this.refs.dragBar.clientHeight; + + const heightNorthInPixels = northInPercent * totalHeight / 100; + const heightSouthInPixels = southInPercent * totalHeight / 100; + + if (this.props.onSizeChange) { + this.props.onSizeChange({ + north: heightNorthInPixels, + south: heightSouthInPixels, + }); + } + } + + initSize() { + const totalHeight = this.refs.splitter.clientHeight; + const dragBarHeight = this.refs.dragBar.clientHeight; + + const heightInPixels = (totalHeight - dragBarHeight) / 2; + const heightInPercent = heightInPixels * 100 / totalHeight; + + this.setState({ + ...this.state, + heightNorth: heightInPercent, + heightSouth: heightInPercent, + }); + this.setSize(heightInPercent, heightInPercent); + } + + handleMouseMove(e) { + if (!this.state.dragging) { + return; + } + + const minHeight = this.props.minHeight || 0; + + const offset = getTopOffset(this.refs.splitter); + const totalHeight = this.refs.splitter.clientHeight; + const dragBarHeight = this.refs.dragBar.clientHeight; + + const heightNorthInPixels = e.pageY - offset; + const heightSouthInPixels = totalHeight - heightNorthInPixels - dragBarHeight; + + const heightNorthInPercent = 100 * heightNorthInPixels / totalHeight; + const heightSouthInPercent = 100 * heightSouthInPixels / totalHeight; + + if (heightNorthInPercent >= minHeight + && heightSouthInPercent >= minHeight) { + this.setState({ + ...this.state, + heightNorth: heightNorthInPercent, + heightSouth: heightSouthInPercent, + }); + + this.setSize(heightNorthInPercent, heightSouthInPercent); + } + } + + handleDraggingStart() { + this.setState({ ...this.state, dragging: true }); + } + + handleResize() { + const { heightNorth, heightSouth } = this.state; + /* + The `delay` is needed since some events like 'onresize' happen before rendering. + That means that we can't calculate the sizes right. + */ + delay(() => { + this.setSize(heightNorth, heightSouth); + }, 100); + } + + handleMouseUp() { + if (this.state.dragging) { + this.setState({ ...this.state, dragging: false }); + } + } + + render() { + return ( +
+
+ {this.props.north} +
+
+
+
+
+ {this.props.south} +
+
+ ); + } +} +SplitPane.propTypes = propTypes; + +export default SplitPane; diff --git a/superset/assets/javascripts/SqlLab/components/SqlEditor.jsx b/superset/assets/javascripts/SqlLab/components/SqlEditor.jsx index f96d7882f7..79e70a17e6 100644 --- a/superset/assets/javascripts/SqlLab/components/SqlEditor.jsx +++ b/superset/assets/javascripts/SqlLab/components/SqlEditor.jsx @@ -15,14 +15,16 @@ import { import Button from '../../components/Button'; +import AceEditorWrapper from './AceEditorWrapper'; import SouthPane from './SouthPane'; +import SplitPane from './SplitPane'; import SaveQuery from './SaveQuery'; import Timer from '../../components/Timer'; import SqlEditorLeftBar from './SqlEditorLeftBar'; -import AceEditorWrapper from './AceEditorWrapper'; import { STATE_BSSTYLE_MAP } from '../constants'; import RunQueryActionButton from './RunQueryActionButton'; + const propTypes = { actions: PropTypes.object.isRequired, height: PropTypes.string.isRequired, @@ -49,6 +51,7 @@ class SqlEditor extends React.PureComponent { autorun: props.queryEditor.autorun, ctas: '', }; + this.onSizeChange = this.onSizeChange.bind(this); } componentDidMount() { this.onMount(); @@ -60,6 +63,13 @@ class SqlEditor extends React.PureComponent { this.startQuery(); } } + onSizeChange(newSizes) { + const bottomBarHeight = this.refs.editorBottomBar.clientHeight; + this.setState({ + ...this.state, + editorHeight: newSizes.north - bottomBarHeight, + }); + } setQueryEditorSql(sql) { this.props.actions.queryEditorSetSql(this.props.queryEditor, sql); } @@ -147,7 +157,7 @@ class SqlEditor extends React.PureComponent { ); } const editorBottomBar = ( -
+
- - {editorBottomBar} -
- + + {editorBottomBar} +
+ } + south={ + + } /> diff --git a/superset/assets/javascripts/SqlLab/main.less b/superset/assets/javascripts/SqlLab/main.less index d5dab4c03c..be147a483c 100644 --- a/superset/assets/javascripts/SqlLab/main.less +++ b/superset/assets/javascripts/SqlLab/main.less @@ -31,6 +31,40 @@ body { padding-top: 5px; padding-bottom: 5px; } +.DragBar { + padding: 10px 0; + text-align: center; + width: 100%; +} +.DragBarVisible { + width: 100%; + height: 3px; + background-color: #ccc; + cursor: row-resize; +} +.Splitter { + height: 100%; +} +.SqlEditor .SouthPane{ + height:100%; +} +.SqlEditor .SouthPane .Tabs{ + height:100%; + display: flex; + flex-direction: column; +} +.SqlEditor .SouthPane .Tabs .tab-content{ + height:100%; + display: flex; + flex: 1 1; +} +.SqlEditor .SouthPane .Tabs .tab-pane{ + width:100%; +} +.SqlEditor .QueryHistoryWrapper{ + height: 100%; + overflow: scroll; +} .scrollbar-container { position: relative; @@ -237,8 +271,8 @@ div.tablePopover:hover { padding-top: 3px; } .ace_editor { - border: 1px solid #ccc; - margin: 0px 0px 10px 0px; + border: 1px solid #ccc; + margin: 0px 0px 10px 0px; } .Select-menu-outer { @@ -253,6 +287,10 @@ div.tablePopover:hover { padding-top: 10px; } +.NorthPane { + width: 100%; +} + .TableElement { margin-right: 10px; } diff --git a/superset/assets/package.json b/superset/assets/package.json index 2dde6b337a..d22381bec8 100644 --- a/superset/assets/package.json +++ b/superset/assets/package.json @@ -55,13 +55,14 @@ "datatables.net-bs": "^1.10.12", "immutable": "^3.8.1", "jquery": "^3.2.1", + "lodash.delay": "^4.1.1", "lodash.throttle": "^4.1.1", "moment": "^2.14.1", "mustache": "^2.2.1", "nvd3": "1.8.5", "prop-types": "^15.5.8", "react": "^15.5.1", - "react-ace": "^5.0.1", + "react-ace": "^5.1.2", "react-addons-css-transition-group": "^15.6.0", "react-addons-shallow-compare": "^15.4.2", "react-alert": "^1.0.14", diff --git a/superset/assets/spec/javascripts/sqllab/SplitPane_spec.jsx b/superset/assets/spec/javascripts/sqllab/SplitPane_spec.jsx new file mode 100644 index 0000000000..d9de0ab7c9 --- /dev/null +++ b/superset/assets/spec/javascripts/sqllab/SplitPane_spec.jsx @@ -0,0 +1,66 @@ +import React from 'react'; +import { shallow, mount } from 'enzyme'; +import { describe, it } from 'mocha'; +import { expect } from 'chai'; +import sinon from 'sinon'; + +import SplitPane from '../../../javascripts/SqlLab/components/SplitPane'; + +function simulateWindowEvent(eventName, customProps) { + const evt = document.createEvent('Event'); + evt.initEvent(eventName, true, true); + global.window.dispatchEvent(Object.assign(evt, customProps)); +} + +const TestComponent = () => (
); + +describe('ResizableAceEditor', () => { + const mockedProps = { + north: () =>
, + south: () =>
, + }; + let clock; + + beforeEach(() => { + clock = sinon.useFakeTimers(); + }); + afterEach(() => { + clock.restore(); + }); + + + it('is valid', () => { + expect( + React.isValidElement(), + ).to.equal(true); + }); + it('renders what you provide in north', () => { + const wrapper = shallow(} />); + expect(wrapper.find(TestComponent)).to.have.length(1); + }); + it('renders what you provide in south', () => { + const wrapper = shallow(} />); + expect(wrapper.find(TestComponent)).to.have.length(1); + }); + it('render a DragBar', () => { + const wrapper = shallow(); + expect(wrapper.find('.DragBar')).to.have.length(1); + }); + it('has dragging set to false by default', () => { + const wrapper = shallow(); + expect(wrapper.state().dragging).to.be.equal(false); + }); + it('has dragging set to true when dragged', () => { + const wrapper = shallow(); + const dragbar = wrapper.find('.DragBar'); + dragbar.simulate('mouseDown'); + expect(wrapper.state().dragging).to.be.equal(true); + }); + it('has dragging set to false when dropped', () => { + const wrapper = mount(); + const dragbar = wrapper.find('.DragBar'); + dragbar.simulate('mouseDown'); + simulateWindowEvent('mouseup'); + expect(wrapper.state().dragging).to.be.equal(false); + }); +}); diff --git a/superset/assets/spec/javascripts/sqllab/SqlEditor_spec.jsx b/superset/assets/spec/javascripts/sqllab/SqlEditor_spec.jsx index ab00ee6272..2bb335c31f 100644 --- a/superset/assets/spec/javascripts/sqllab/SqlEditor_spec.jsx +++ b/superset/assets/spec/javascripts/sqllab/SqlEditor_spec.jsx @@ -6,6 +6,7 @@ import { expect } from 'chai'; import { initialState, queries, table } from './fixtures'; import SqlEditor from '../../../javascripts/SqlLab/components/SqlEditor'; import SqlEditorLeftBar from '../../../javascripts/SqlLab/components/SqlEditorLeftBar'; +import SplitPane from '../../../javascripts/SqlLab/components/SplitPane'; describe('SqlEditor', () => { const mockedProps = { @@ -28,4 +29,8 @@ describe('SqlEditor', () => { const wrapper = shallow(); expect(wrapper.find(SqlEditorLeftBar)).to.have.length(1); }); + it('render a SplitPane', () => { + const wrapper = shallow(); + expect(wrapper.find(SplitPane)).to.have.length(1); + }); }); diff --git a/superset/assets/utils/common.js b/superset/assets/utils/common.js index 2e143a12c8..05b33fb22f 100644 --- a/superset/assets/utils/common.js +++ b/superset/assets/utils/common.js @@ -86,3 +86,8 @@ export function getShortUrl(longUrl, callback) { }, }); } + +export function getTopOffset(element) { + const box = element.getBoundingClientRect(); + return box.top + window.pageYOffset - document.documentElement.clientTop; +}