From ec6657ab2d07d6982a454e20745445ab673a6a0f Mon Sep 17 00:00:00 2001 From: Christine Chambers Date: Thu, 14 Feb 2019 18:03:43 -0800 Subject: [PATCH] Relayout SQL Editor (#6872) * Relayout SQL Editor - Refactor SQL editor to remove usage of bootstrap col, row and collapse to simplify the layout - Replace the react-split-pane libraray with react-split to allow custom styling of the gutter area without sacrifice correctness of the ace editor height calculation - Rewrite the left pane animation via plain css transition and animate it to slide in and out - General code and css clean up * Smooth out the visual transition during dragging (cherry picked from commit 19f82b729c7a939f12b1c5da6022c0fd76fa3ec9) * Adjust how the height of the south pane is computed, fixing cypress tests --- superset/assets/package-lock.json | 25 +++ superset/assets/package.json | 3 +- .../javascripts/sqllab/SouthPane_spec.jsx | 9 +- .../javascripts/sqllab/SqlEditor_spec.jsx | 5 + superset/assets/src/SqlLab/components/App.jsx | 2 +- .../src/SqlLab/components/SouthPane.jsx | 25 ++- .../src/SqlLab/components/SqlEditor.jsx | 186 +++++++++--------- .../SqlLab/components/SqlEditorLeftBar.jsx | 3 +- .../SqlLab/components/TabbedSqlEditors.jsx | 2 - superset/assets/src/SqlLab/main.less | 72 +++++-- .../assets/src/components/AsyncSelect.jsx | 20 +- .../assets/src/components/TableSelector.css | 18 +- .../assets/src/components/TableSelector.jsx | 87 ++++---- 13 files changed, 273 insertions(+), 184 deletions(-) diff --git a/superset/assets/package-lock.json b/superset/assets/package-lock.json index f6ba04f722..ba19bacdce 100644 --- a/superset/assets/package-lock.json +++ b/superset/assets/package-lock.json @@ -17339,6 +17339,15 @@ "prop-types": "^15.5.7" } }, + "react-split": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/react-split/-/react-split-2.0.4.tgz", + "integrity": "sha512-NBKm9MaqzG/00laMUaS8GS9RnItVSekNNwItgGLMbFTeUa9w4bIY8Co/LszNBnpza9n2am0MXIw3SmyiMnhs+w==", + "requires": { + "prop-types": "^15.5.7", + "split.js": "^1.5.9" + } + }, "react-split-pane": { "version": "0.1.85", "resolved": "https://registry.npmjs.org/react-split-pane/-/react-split-pane-0.1.85.tgz", @@ -17380,6 +17389,17 @@ "refractor": "^2.4.1" } }, + "react-transition-group": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.5.3.tgz", + "integrity": "sha512-2DGFck6h99kLNr8pOFk+z4Soq3iISydwOFeeEVPjTN6+Y01CmvbWmnN02VuTWyFdnRtIDPe+wy2q6Ui8snBPZg==", + "requires": { + "dom-helpers": "^3.3.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2", + "react-lifecycles-compat": "^3.0.4" + } + }, "react-virtualized": { "version": "9.19.1", "resolved": "https://registry.npmjs.org/react-virtualized/-/react-virtualized-9.19.1.tgz", @@ -19391,6 +19411,11 @@ } } }, + "split.js": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/split.js/-/split.js-1.5.10.tgz", + "integrity": "sha512-/J52X5c4ZypVwu4WAhD8E1T9uXQtNokvG6mIBHauzyA1aKH6bmETVSv3RPjBXEz6Gcc4mIThgmjGQL39LD16jQ==" + }, "sprintf-js": { "version": "1.0.3", "resolved": "http://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", diff --git a/superset/assets/package.json b/superset/assets/package.json index c791fb6c57..02a9fdc59a 100644 --- a/superset/assets/package.json +++ b/superset/assets/package.json @@ -123,9 +123,10 @@ "react-select": "1.2.1", "react-select-fast-filter-options": "^0.2.1", "react-sortable-hoc": "^0.8.3", - "react-split-pane": "^0.1.66", + "react-split": "^2.0.4", "react-sticky": "^6.0.2", "react-syntax-highlighter": "^7.0.4", + "react-transition-group": "^2.5.3", "react-virtualized": "9.19.1", "react-virtualized-select": "^3.1.3", "reactable-arc": "0.14.42", diff --git a/superset/assets/spec/javascripts/sqllab/SouthPane_spec.jsx b/superset/assets/spec/javascripts/sqllab/SouthPane_spec.jsx index 7b920f6160..da6da521f4 100644 --- a/superset/assets/spec/javascripts/sqllab/SouthPane_spec.jsx +++ b/superset/assets/spec/javascripts/sqllab/SouthPane_spec.jsx @@ -24,7 +24,7 @@ import { shallow } from 'enzyme'; import { STATUS_OPTIONS } from '../../../src/SqlLab/constants'; import { initialState } from './fixtures'; -import SouthPane from '../../../src/SqlLab/components/SouthPane'; +import SouthPaneContainer, { SouthPane } from '../../../src/SqlLab/components/SouthPane'; describe('SouthPane', () => { const middlewares = [thunk]; @@ -42,11 +42,16 @@ describe('SouthPane', () => { }; const getWrapper = () => ( - shallow(, { + shallow(, { context: { store }, }).dive()); let wrapper; + + beforeAll(() => { + jest.spyOn(SouthPane.prototype, 'getSouthPaneHeight').mockImplementation(() => 500); + }); + it('should render offline when the state is offline', () => { wrapper = getWrapper(); wrapper.setProps({ offline: true }); diff --git a/superset/assets/spec/javascripts/sqllab/SqlEditor_spec.jsx b/superset/assets/spec/javascripts/sqllab/SqlEditor_spec.jsx index 933524fa44..046b2e69cf 100644 --- a/superset/assets/spec/javascripts/sqllab/SqlEditor_spec.jsx +++ b/superset/assets/spec/javascripts/sqllab/SqlEditor_spec.jsx @@ -38,6 +38,11 @@ describe('SqlEditor', () => { defaultQueryLimit: 1000, maxRow: 100000, }; + + beforeAll(() => { + jest.spyOn(SqlEditor.prototype, 'getSqlEditorHeight').mockImplementation(() => 500); + }); + it('is valid', () => { expect( React.isValidElement(), diff --git a/superset/assets/src/SqlLab/components/App.jsx b/superset/assets/src/SqlLab/components/App.jsx index bf50caff18..25c61deb64 100644 --- a/superset/assets/src/SqlLab/components/App.jsx +++ b/superset/assets/src/SqlLab/components/App.jsx @@ -84,7 +84,7 @@ class App extends React.PureComponent { content = (
- +
); } diff --git a/superset/assets/src/SqlLab/components/SouthPane.jsx b/superset/assets/src/SqlLab/components/SouthPane.jsx index 0dd162636e..40d46da24d 100644 --- a/superset/assets/src/SqlLab/components/SouthPane.jsx +++ b/superset/assets/src/SqlLab/components/SouthPane.jsx @@ -48,7 +48,24 @@ const defaultProps = { offline: false, }; -class SouthPane extends React.PureComponent { +export class SouthPane extends React.PureComponent { + constructor(props) { + super(props); + this.state = { + height: props.height, + }; + this.southPaneRef = React.createRef(); + this.getSouthPaneHeight = this.getSouthPaneHeight.bind(this); + this.switchTab = this.switchTab.bind(this); + } + componentWillReceiveProps() { + // south pane expands the entire height of the tab content on mount + this.setState({ height: this.getSouthPaneHeight() }); + } + // One layer of abstraction for easy spying in unit tests + getSouthPaneHeight() { + return this.southPaneRef.current.clientHeight; + } switchTab(id) { this.props.actions.setActiveSouthPaneTab(id); } @@ -59,7 +76,7 @@ class SouthPane extends React.PureComponent { { STATUS_OPTIONS.offline } ); } - const innerTabHeight = this.props.height - 55; + const innerTabHeight = this.state.height - 55; let latestQuery; const props = this.props; if (props.editorQueries.length > 0) { @@ -98,12 +115,12 @@ class SouthPane extends React.PureComponent { )); return ( -
+
+ +
+ + {this.renderEditorBottomBar(hotkeys)} +
+ +
+
+ ); } renderEditorBottomBar(hotkeys) { let ctasControls; @@ -305,74 +356,23 @@ class SqlEditor extends React.PureComponent { ); } render() { - const height = this.sqlEditorHeight(); - const defaultNorthHeight = this.props.queryEditor.height || 200; - const hotkeys = this.getHotkeyConfig(); return ( -
- - - - - - - - -
-
- - {this.renderEditorBottomBar(hotkeys)} -
-
-
- -
-
- -
+
+ +
+ +
+
+ {this.queryPane()}
); } diff --git a/superset/assets/src/SqlLab/components/SqlEditorLeftBar.jsx b/superset/assets/src/SqlLab/components/SqlEditorLeftBar.jsx index 16170a69c3..0de18b0a6b 100644 --- a/superset/assets/src/SqlLab/components/SqlEditorLeftBar.jsx +++ b/superset/assets/src/SqlLab/components/SqlEditorLeftBar.jsx @@ -20,7 +20,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Button } from 'react-bootstrap'; import { t } from '@superset-ui/translation'; - import TableElement from './TableElement'; import TableSelector from '../../components/TableSelector'; @@ -106,7 +105,7 @@ export default class SqlEditorLeftBar extends React.PureComponent { const tableMetaDataHeight = this.props.height - 130; // 130 is the height of the selects above const qe = this.props.queryEditor; return ( -
+
{isSelected && ( xt.queryEditorId === qe.id)} queryEditor={qe} editorQueries={this.state.queriesArray} diff --git a/superset/assets/src/SqlLab/main.less b/superset/assets/src/SqlLab/main.less index cd2cdd8e1b..28755a37c3 100644 --- a/superset/assets/src/SqlLab/main.less +++ b/superset/assets/src/SqlLab/main.less @@ -135,7 +135,8 @@ div.Workspace { background-color: #e8e8e8; display: flex; justify-content: space-between; - border-bottom: 2px solid #ccc; + border: 1px solid #ccc; + border-top: 0; form { margin-block-end: 0; @@ -193,21 +194,67 @@ div.Workspace { background-color: transparent !important; } -.SqlEditor { - .Resizer { - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - box-sizing: border-box; +.SqlLab { + .tab-content { + height: 100%; } - .Resizer.horizontal { - height: 4px; + #brace-editor { + height: calc(100% - 51px); + } + + .ace_content { + height: 100%; + } + + .SouthPane { + height: 100%; + } +} + +.SqlEditor { + display: flex; + flex-direction: row; + height: 100%; + + .schemaPane { + flex-grow: 1; + transition: all .3s ease-in-out; + } + + .schemaPane-enter-done, .schemaPane-exit { + transform: translateX(0); + } + + .schemaPane-enter-active, .schemaPane-exit-active { + transform: translateX(-50%); + } + + .schemaPane-enter, .schemaPane-exit-done { + transform: translateX(-100%); + max-width: 0; + overflow: hidden; + } + + .queryPane { + flex-grow: 8; + position: relative; + margin-left: 15px; + } + + .schemaPane-exit-done + .queryPane { + margin-left: 0; + } + + .gutter { border-top: 1px solid #ccc; border-bottom: 1px solid #ccc; + width: 3%; + margin: 3px 47%; + } + + .gutter.gutter-vertical { cursor: row-resize; - width: 4%; - margin-top: 4px; - margin-left: 47%; } } @@ -298,9 +345,6 @@ a.Link { .tooltip-inner { max-width: 500px; } -.SplitPane.horizontal { - padding-right: 4px; -} .SouthPane { margin-top: 10px; position: absolute; diff --git a/superset/assets/src/components/AsyncSelect.jsx b/superset/assets/src/components/AsyncSelect.jsx index 1106402dc2..da32fab329 100644 --- a/superset/assets/src/components/AsyncSelect.jsx +++ b/superset/assets/src/components/AsyncSelect.jsx @@ -83,17 +83,15 @@ class AsyncSelect extends React.PureComponent { render() { return ( -
- ); } } diff --git a/superset/assets/src/components/TableSelector.css b/superset/assets/src/components/TableSelector.css index b4636de364..41b8d118f3 100644 --- a/superset/assets/src/components/TableSelector.css +++ b/superset/assets/src/components/TableSelector.css @@ -17,8 +17,22 @@ * under the License. */ .TableSelector .fa-refresh { - padding-top: 7px + padding-left: 9px; } .TableSelector .refresh-col { - padding-left: 0px; + display: flex; + align-items: center; + width: 30px; +} +.TableSelector .section { + padding-bottom: 5px; + display: flex; + flex-direction: row; +} +.TableSelector .select { + flex-grow: 1; +} +.TableSelector .divider { + border-bottom: 1px solid #f2f2f2; + margin: 10px 0; } diff --git a/superset/assets/src/components/TableSelector.jsx b/superset/assets/src/components/TableSelector.jsx index 9031d1a2cd..6130fc6f3d 100644 --- a/superset/assets/src/components/TableSelector.jsx +++ b/superset/assets/src/components/TableSelector.jsx @@ -20,7 +20,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import Select from 'react-virtualized-select'; import createFilterOptions from 'react-select-fast-filter-options'; -import { ControlLabel, Col, Label, Row } from 'react-bootstrap'; +import { ControlLabel, Label } from 'react-bootstrap'; import { t } from '@superset-ui/translation'; import { SupersetClient } from '@superset-ui/connection'; @@ -38,7 +38,6 @@ const propTypes = { tableNameSticky: PropTypes.bool, tableName: PropTypes.string, database: PropTypes.object, - horizontal: PropTypes.bool, sqlLabMode: PropTypes.bool, onChange: PropTypes.func, clearable: PropTypes.bool, @@ -52,7 +51,6 @@ const defaultProps = { onTableChange: () => {}, onChange: () => {}, tableNameSticky: true, - horizontal: false, sqlLabMode: true, clearable: true, }; @@ -199,10 +197,10 @@ export default class TableSelector extends React.PureComponent { } renderSelectRow(select, refreshBtn) { return ( - - {select} - {refreshBtn} - +
+ {select} + {refreshBtn} +
); } renderDatabaseSelect() { @@ -232,29 +230,25 @@ export default class TableSelector extends React.PureComponent { />); } renderSchema() { - return ( -
- {this.renderSelectRow( - ( +
+ {t('Schema:')} {o.label} +
)} -
+ isLoading={this.state.schemaLoading} + autosize={false} + onChange={this.changeSchema} + />, + this.onDatabaseChange({ id: this.props.dbId }, true)} + tooltipContent={t('Force refresh schema list')} + />, ); } renderTable() { @@ -290,20 +284,16 @@ export default class TableSelector extends React.PureComponent { value={this.state.tableName} loadOptions={this.getTableNamesBySubStr} />); - return ( -
- {this.renderSelectRow( - select, - this.changeSchema({ value: this.props.schema }, true)} - tooltipContent={t('Force refresh table list')} - />)} -
); + return this.renderSelectRow( + select, + this.changeSchema({ value: this.props.schema }, true)} + tooltipContent={t('Force refresh table list')} + />); } renderSeeTableLabel() { return ( -
-
+
{t('See table schema')}{' '} @@ -319,18 +309,11 @@ export default class TableSelector extends React.PureComponent { render() { return (
- {this.props.horizontal ? -
- {this.renderDatabaseSelect()} - {this.renderSchema()} - {this.renderTable()} -
: -
-
{this.renderDatabaseSelect()}
-
{this.renderSchema()}
- {this.props.sqlLabMode && this.renderSeeTableLabel()} -
{this.renderTable()}
-
} + {this.renderDatabaseSelect()} + {this.renderSchema()} +
+ {this.props.sqlLabMode && this.renderSeeTableLabel()} + {this.renderTable()}
); }