From 4fd993c4e07f8bd1220d105fbaa18733daf5aebb Mon Sep 17 00:00:00 2001 From: Kamil Gabryjelski Date: Fri, 2 Oct 2020 22:07:52 +0200 Subject: [PATCH] refactor: Replace react-bootstrap tabs with Antd tabs (#11090) * Replace tabs in profile * Replace tabs in SouthPane * Replace tabs in TabbedSqlEditors * Add typing for dropdown * Add license * Remove isSelected * Fixes * Add data-test * Fix test * Remove unnecessary style * Remove unnecessary style * Tests fix * Tests fix * Update superset-frontend/src/common/components/Dropdown.tsx Co-authored-by: Evan Rusackas * Update superset-frontend/src/common/components/Dropdown.tsx Co-authored-by: Evan Rusackas * Update superset-frontend/src/common/components/Dropdown.tsx Co-authored-by: Evan Rusackas * Update superset-frontend/src/common/components/Dropdown.tsx Co-authored-by: Evan Rusackas * Update superset-frontend/src/common/components/Tabs.tsx Co-authored-by: Evan Rusackas * Update superset-frontend/src/common/components/Dropdown.tsx Co-authored-by: Evan Rusackas * Update superset-frontend/src/common/components/Dropdown.tsx Co-authored-by: Evan Rusackas * Remove inModal prop * Remove inModal from storybook * Move inline style to styled component Co-authored-by: Evan Rusackas --- .../cypress/integration/sqllab/tabs.test.js | 15 +- .../spec/javascripts/profile/App_spec.tsx | 5 +- .../sqllab/TabbedSqlEditors_spec.jsx | 24 +- .../src/SqlLab/components/SouthPane.jsx | 24 +- .../SqlLab/components/TabbedSqlEditors.jsx | 224 ++++++++---------- .../src/common/components/Dropdown.tsx | 78 ++++++ .../src/common/components/Modal.tsx | 5 + .../src/common/components/Tabs.tsx | 75 +++++- .../src/common/components/common.stories.tsx | 61 ++++- .../src/profile/components/App.tsx | 37 +-- 10 files changed, 369 insertions(+), 179 deletions(-) create mode 100644 superset-frontend/src/common/components/Dropdown.tsx diff --git a/superset-frontend/cypress-base/cypress/integration/sqllab/tabs.test.js b/superset-frontend/cypress-base/cypress/integration/sqllab/tabs.test.js index ebcbadff7e..9a0e8f4df6 100644 --- a/superset-frontend/cypress-base/cypress/integration/sqllab/tabs.test.js +++ b/superset-frontend/cypress-base/cypress/integration/sqllab/tabs.test.js @@ -27,15 +27,15 @@ describe('SqlLab query tabs', () => { cy.get('[data-test="sql-editor-tabs"]').then(tabList => { const initialTabCount = tabList.length; // add tab - cy.get('[data-test="add-tab-icon"]').click(); + cy.get('[data-test="add-tab-icon"]').first().click(); // wait until we find the new tab cy.get('[data-test="sql-editor-tabs"]') .children() - .eq(initialTabCount - 1) + .eq(0) .contains(`Untitled Query ${initialTabCount + 1}`); cy.get('[data-test="sql-editor-tabs"]') .children() - .eq(initialTabCount) + .eq(0) .contains(`Untitled Query ${initialTabCount + 2}`); }); }); @@ -47,9 +47,12 @@ describe('SqlLab query tabs', () => { const initialTabCount = tabListA.length; // open the tab dropdown to remove - cy.get('[data-test="dropdown-toggle-button"]').click({ - force: true, - }); + cy.get('[data-test="dropdown-toggle-button"]') + .children() + .first() + .click({ + force: true, + }); // first item is close cy.get('[data-test="close-tab-menu-option"]').click(); diff --git a/superset-frontend/spec/javascripts/profile/App_spec.tsx b/superset-frontend/spec/javascripts/profile/App_spec.tsx index 13b53b8b76..e45b1999be 100644 --- a/superset-frontend/spec/javascripts/profile/App_spec.tsx +++ b/superset-frontend/spec/javascripts/profile/App_spec.tsx @@ -17,9 +17,10 @@ * under the License. */ import React from 'react'; -import { Col, Row, Tab } from 'react-bootstrap'; +import { Col, Row } from 'react-bootstrap'; import { shallow } from 'enzyme'; import App from 'src/profile/components/App'; +import Tabs from 'src/common/components/Tabs'; import { user } from './fixtures'; @@ -39,6 +40,6 @@ describe('App', () => { it('renders 4 Tabs', () => { const wrapper = shallow(); - expect(wrapper.find(Tab)).toHaveLength(4); + expect(wrapper.find(Tabs.TabPane)).toHaveLength(4); }); }); diff --git a/superset-frontend/spec/javascripts/sqllab/TabbedSqlEditors_spec.jsx b/superset-frontend/spec/javascripts/sqllab/TabbedSqlEditors_spec.jsx index 2d6823222c..2832b32a10 100644 --- a/superset-frontend/spec/javascripts/sqllab/TabbedSqlEditors_spec.jsx +++ b/superset-frontend/spec/javascripts/sqllab/TabbedSqlEditors_spec.jsx @@ -20,10 +20,10 @@ import React from 'react'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import URI from 'urijs'; -import { Tab } from 'react-bootstrap'; import { shallow, mount } from 'enzyme'; import sinon from 'sinon'; import { supersetTheme, ThemeProvider } from '@superset-ui/core'; +import { EditableTabs } from 'src/common/components/Tabs'; import TabbedSqlEditors from 'src/SqlLab/components/TabbedSqlEditors'; import SqlEditor from 'src/SqlLab/components/SqlEditor'; @@ -206,36 +206,36 @@ describe('TabbedSqlEditors', () => { }, }; wrapper = getWrapper(); - sinon.spy(wrapper.instance(), 'newQueryEditor'); sinon.stub(wrapper.instance().props.actions, 'switchQueryEditor'); - wrapper.instance().handleSelect('add_tab', mockEvent); - expect(wrapper.instance().newQueryEditor.callCount).toBe(1); - // cannot switch to current tab, switchQueryEditor never gets called wrapper.instance().handleSelect('dfsadfs', mockEvent); expect( wrapper.instance().props.actions.switchQueryEditor.callCount, ).toEqual(0); + }); + it('should handle add tab', () => { + wrapper = getWrapper(); + sinon.spy(wrapper.instance(), 'newQueryEditor'); + + wrapper.instance().handleEdit('1', 'add'); + expect(wrapper.instance().newQueryEditor.callCount).toBe(1); wrapper.instance().newQueryEditor.restore(); }); it('should render', () => { wrapper = getWrapper(); wrapper.setState({ hideLeftBar: true }); - const firstTab = wrapper.find(Tab).first(); - expect(firstTab.props().eventKey).toContain( + const firstTab = wrapper.find(EditableTabs.TabPane).first(); + expect(firstTab.props()['data-key']).toContain( initialState.sqlLab.queryEditors[0].id, ); expect(firstTab.find(SqlEditor)).toHaveLength(1); - - const lastTab = wrapper.find(Tab).last(); - expect(lastTab.props().eventKey).toContain('add_tab'); }); it('should disable new tab when offline', () => { wrapper = getWrapper(); - expect(wrapper.find(Tab).last().props().disabled).toBe(false); + expect(wrapper.find(EditableTabs).props().hideAdd).toBe(false); wrapper.setProps({ offline: true }); - expect(wrapper.find(Tab).last().props().disabled).toBe(true); + expect(wrapper.find(EditableTabs).props().hideAdd).toBe(true); }); }); diff --git a/superset-frontend/src/SqlLab/components/SouthPane.jsx b/superset-frontend/src/SqlLab/components/SouthPane.jsx index 2b2b27052c..526508a2d4 100644 --- a/superset-frontend/src/SqlLab/components/SouthPane.jsx +++ b/superset-frontend/src/SqlLab/components/SouthPane.jsx @@ -19,7 +19,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import shortid from 'shortid'; -import { Alert, Tab, Tabs } from 'react-bootstrap'; +import { Alert } from 'react-bootstrap'; +import Tabs from 'src/common/components/Tabs'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import { t } from '@superset-ui/core'; @@ -140,9 +141,8 @@ export class SouthPane extends React.PureComponent { ); } const dataPreviewTabs = props.dataPreviewQueries.map(query => ( - - + )); return (
- + {results} - - + + - + {dataPreviewTabs}
diff --git a/superset-frontend/src/SqlLab/components/TabbedSqlEditors.jsx b/superset-frontend/src/SqlLab/components/TabbedSqlEditors.jsx index 8ee59d5541..5461645476 100644 --- a/superset-frontend/src/SqlLab/components/TabbedSqlEditors.jsx +++ b/superset-frontend/src/SqlLab/components/TabbedSqlEditors.jsx @@ -18,18 +18,19 @@ */ import React from 'react'; import PropTypes from 'prop-types'; -import { MenuItem, DropdownButton, Tab, Tabs } from 'react-bootstrap'; +import { EditableTabs } from 'src/common/components/Tabs'; +import { Dropdown } from 'src/common/components/Dropdown'; +import { Menu } from 'src/common/components'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import URI from 'urijs'; -import { t } from '@superset-ui/core'; +import { styled, t } from '@superset-ui/core'; import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags'; +import { areArraysShallowEqual } from 'src/reduxUtils'; import * as Actions from '../actions/sqlLab'; import SqlEditor from './SqlEditor'; -import { areArraysShallowEqual } from '../../reduxUtils'; import TabStatusIcon from './TabStatusIcon'; -import Icon from '../../components/Icon'; const propTypes = { actions: PropTypes.object.isRequired, @@ -57,6 +58,10 @@ const defaultProps = { let queryCount = 1; +const TabTitle = styled.span` + margin-right: ${({ theme }) => theme.gridUnit * 2}px; +`; + class TabbedSqlEditors extends React.PureComponent { constructor(props) { super(props); @@ -74,6 +79,8 @@ class TabbedSqlEditors extends React.PureComponent { this, ); this.duplicateQueryEditor = this.duplicateQueryEditor.bind(this); + this.handleSelect = this.handleSelect.bind(this); + this.handleEdit = this.handleEdit.bind(this); } componentDidMount() { @@ -253,17 +260,23 @@ class TabbedSqlEditors extends React.PureComponent { } handleSelect(key) { - if (key === 'add_tab') { + const qeid = this.props.tabHistory[this.props.tabHistory.length - 1]; + if (key !== qeid) { + const queryEditor = this.props.queryEditors.find(qe => qe.id === key); + this.props.actions.switchQueryEditor( + queryEditor, + this.props.displayLimit, + ); + } + } + + handleEdit(key, action) { + if (action === 'remove') { + const qe = this.props.queryEditors.find(qe => qe.id === key); + this.removeQueryEditor(qe); + } + if (action === 'add') { this.newQueryEditor(); - } else { - const qeid = this.props.tabHistory[this.props.tabHistory.length - 1]; - if (key !== qeid) { - const queryEditor = this.props.queryEditors.find(qe => qe.id === key); - this.props.actions.switchQueryEditor( - queryEditor, - this.props.displayLimit, - ); - } } } @@ -286,10 +299,7 @@ class TabbedSqlEditors extends React.PureComponent { } render() { - const editors = this.props.queryEditors.map((qe, i) => { - const isSelected = - this.activeQueryEditor() && this.activeQueryEditor().id === qe.id; - + const editors = this.props.queryEditors.map(qe => { let latestQuery; if (qe.latestQueryId) { latestQuery = this.props.queries[qe.latestQueryId]; @@ -300,123 +310,97 @@ class TabbedSqlEditors extends React.PureComponent { } const state = latestQuery ? latestQuery.state : ''; - const title = ( - <> - {qe.title} {' '} - + this.removeQueryEditor(qe)} - /> - + data-test="close-tab-menu-option" + > +
+ +
+ {t('Close tab')} +
+ this.renameTab(qe)}> +
+ +
+ {t('Rename tab')} +
+ +
+ +
+ {this.state.hideLeftBar ? t('Expand tool bar') : t('Hide tool bar')} +
+ this.removeAllOtherQueryEditors(qe)} + > +
+ +
+ {t('Close all other tabs')} +
+ this.duplicateQueryEditor(qe)}> +
+ +
+ {t('Duplicate tab')} +
+ ); - const tabTitle = ( + + const tabHeader = ( <> - {isSelected && ( - - this.removeQueryEditor(qe)} - data-test="close-tab-menu-option" - > -
- -
- {t('Close tab')} -
- this.renameTab(qe)}> -
- -
- {t('Rename tab')} -
- -
- -
- {this.state.hideLeftBar - ? t('Expand tool bar') - : t('Hide tool bar')} -
- this.removeAllOtherQueryEditors(qe)} - > -
- -
- {t('Close all other tabs')} -
- this.duplicateQueryEditor(qe)} - > -
- -
- {t('Duplicate tab')} -
-
- )} - {title} +
+ +
+ {qe.title} {' '} ); return ( - - {isSelected && ( - xt.queryEditorId === qe.id, - )} - queryEditor={qe} - editorQueries={this.state.queriesArray} - dataPreviewQueries={this.state.dataPreviewQueries} - latestQuery={latestQuery} - database={database} - actions={this.props.actions} - hideLeftBar={this.state.hideLeftBar} - defaultQueryLimit={this.props.defaultQueryLimit} - maxRow={this.props.maxRow} - displayLimit={this.props.displayLimit} - saveQueryWarning={this.props.saveQueryWarning} - scheduleQueryWarning={this.props.scheduleQueryWarning} - /> - )} - + + xt.queryEditorId === qe.id)} + queryEditor={qe} + editorQueries={this.state.queriesArray} + dataPreviewQueries={this.state.dataPreviewQueries} + latestQuery={latestQuery} + database={database} + actions={this.props.actions} + hideLeftBar={this.state.hideLeftBar} + defaultQueryLimit={this.props.defaultQueryLimit} + maxRow={this.props.maxRow} + displayLimit={this.props.displayLimit} + saveQueryWarning={this.props.saveQueryWarning} + scheduleQueryWarning={this.props.scheduleQueryWarning} + /> + ); }); + return ( - } > {editors} - - -   - - } - className="addEditorTab" - eventKey="add_tab" - disabled={this.props.offline} - /> - + ); } } diff --git a/superset-frontend/src/common/components/Dropdown.tsx b/superset-frontend/src/common/components/Dropdown.tsx new file mode 100644 index 0000000000..18ac168129 --- /dev/null +++ b/superset-frontend/src/common/components/Dropdown.tsx @@ -0,0 +1,78 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { Dropdown as AntdDropdown } from 'src/common/components'; +import { css } from '@emotion/core'; +import { styled } from '@superset-ui/core'; + +const dotStyle = css` + width: 3px; + height: 3px; + border-radius: 1.5px; + background-color: #bababa; +`; + +const MenuDots = styled.div` + ${dotStyle}; + font-weight: ${({ theme }) => theme.typography.weights.normal}; + display: inline-flex; + + &:hover { + background-color: ${({ theme }) => theme.colors.primary.base}; + + &::before, + &::after { + background-color: ${({ theme }) => theme.colors.primary.base}; + } + } + + &::before, + &::after { + position: absolute; + content: ' '; + ${dotStyle}; + } + + &::before { + transform: translateY(-${({ theme }) => theme.gridUnit}px); + } + + &::after { + transform: translateY(${({ theme }) => theme.gridUnit}px); + } +`; + +const MenuDotsWrapper = styled.div` + display: flex; + align-items: center; + padding: ${({ theme }) => theme.gridUnit * 2}px; + padding-left: ${({ theme }) => theme.gridUnit}px; +`; + +interface DropdownProps { + overlay: React.ReactElement; +} + +export const Dropdown = ({ overlay, ...rest }: DropdownProps) => ( + + + + + +); diff --git a/superset-frontend/src/common/components/Modal.tsx b/superset-frontend/src/common/components/Modal.tsx index 9e97f6f2d8..9634cc3c07 100644 --- a/superset-frontend/src/common/components/Modal.tsx +++ b/superset-frontend/src/common/components/Modal.tsx @@ -78,6 +78,11 @@ const StyledModal = styled(BaseModal)` margin-left: 8px; } } + + // styling for Tabs component + .ant-tabs { + margin-top: -18px; + } `; export default function Modal({ diff --git a/superset-frontend/src/common/components/Tabs.tsx b/superset-frontend/src/common/components/Tabs.tsx index 932e444b1d..9346d0aa4c 100644 --- a/superset-frontend/src/common/components/Tabs.tsx +++ b/superset-frontend/src/common/components/Tabs.tsx @@ -16,27 +16,46 @@ * specific language governing permissions and limitations * under the License. */ +import React from 'react'; import { styled } from '@superset-ui/core'; import { Tabs as AntdTabs } from 'src/common/components'; +import { css } from '@emotion/core'; +import Icon from '../../components/Icon'; -const StyledTabs = styled(AntdTabs)` - margin-top: -18px; +interface TabsProps { + fullWidth?: boolean; +} - .ant-tabs-nav-list { - width: 100%; - } +const notForwardedProps = ['fullWidth']; +const StyledTabs = styled(AntdTabs, { + shouldForwardProp: prop => !notForwardedProps.includes(prop), +})` .ant-tabs-tab { flex: 1 1 auto; - width: 0; &.ant-tabs-tab-active .ant-tabs-tab-btn { color: inherit; } } + ${({ fullWidth }) => + fullWidth && + css` + .ant-tabs-nav-list { + width: 100%; + } + + .ant-tabs-tab { + width: 0; + } + `}; + .ant-tabs-tab-btn { + display: flex; flex: 1 1 auto; + align-items: center; + justify-content: center; font-size: ${({ theme }) => theme.typography.sizes.s}px; text-align: center; text-transform: uppercase; @@ -59,4 +78,48 @@ const Tabs = Object.assign(StyledTabs, { TabPane: StyledTabPane, }); +Tabs.defaultProps = { + fullWidth: true, +}; + +const StyledEditableTabs = styled(StyledTabs)` + .ant-tabs-content-holder { + background: white; + } + + & > .ant-tabs-nav { + margin-bottom: 0; + } + + .ant-tabs-tab-remove { + padding-top: 0; + padding-bottom: 0; + height: ${({ theme }) => theme.gridUnit * 6}px; + } + + ${({ fullWidth }) => + fullWidth && + css` + .ant-tabs-nav-list { + width: 100%; + } + `} +`; + +const EditableTabs = Object.assign(StyledEditableTabs, { + TabPane: StyledTabPane, +}); + +EditableTabs.defaultProps = { + type: 'editable-card', + fullWidth: false, +}; + +EditableTabs.TabPane.defaultProps = { + closeIcon: ( + + ), +}; + export default Tabs; +export { EditableTabs }; diff --git a/superset-frontend/src/common/components/common.stories.tsx b/superset-frontend/src/common/components/common.stories.tsx index 3ed541d26a..6b564d026f 100644 --- a/superset-frontend/src/common/components/common.stories.tsx +++ b/superset-frontend/src/common/components/common.stories.tsx @@ -20,7 +20,9 @@ import React from 'react'; import { action } from '@storybook/addon-actions'; import { withKnobs, boolean } from '@storybook/addon-knobs'; import Modal from './Modal'; -import Tabs from './Tabs'; +import Tabs, { EditableTabs } from './Tabs'; +import { Menu } from '.'; +import { Dropdown } from './Dropdown'; export default { title: 'Common Components', @@ -42,7 +44,11 @@ export const StyledModal = () => ( ); export const StyledTabs = () => ( - + ( ); + +export const StyledEditableTabs = () => ( + + + Tab 1 Content! + + + Tab 2 Content! + + +); + +export const TabsWithDropdownMenu = () => ( + + + + Item 1 + Item 2 + + } + /> + Tab with dropdown menu + + } + key="1" + disabled={boolean('Tab 1 Disabled', false)} + > + Tab 1 Content! + + +); diff --git a/superset-frontend/src/profile/components/App.tsx b/superset-frontend/src/profile/components/App.tsx index 0d18199c11..8642dd78e1 100644 --- a/superset-frontend/src/profile/components/App.tsx +++ b/superset-frontend/src/profile/components/App.tsx @@ -17,7 +17,8 @@ * under the License. */ import React from 'react'; -import { Col, Row, Tabs, Tab, Panel } from 'react-bootstrap'; +import { Col, Row, Panel } from 'react-bootstrap'; +import Tabs from 'src/common/components/Tabs'; import { t } from '@superset-ui/core'; import Favorites from './Favorites'; @@ -39,10 +40,10 @@ export default function App({ user }: AppProps) { - - + {t('Favorites')} @@ -53,10 +54,10 @@ export default function App({ user }: AppProps) { - - + {t('Created Content')} @@ -67,10 +68,10 @@ export default function App({ user }: AppProps) { - - + {t('Recent Activity')} @@ -81,10 +82,10 @@ export default function App({ user }: AppProps) { - - + {t('Security & Access')} @@ -95,7 +96,7 @@ export default function App({ user }: AppProps) { - +