You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ru...@apache.org on 2020/10/02 20:08:16 UTC

[incubator-superset] branch master updated: refactor: Replace react-bootstrap tabs with Antd tabs (#11090)

This is an automated email from the ASF dual-hosted git repository.

rusackas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 4fd993c  refactor: Replace react-bootstrap tabs with Antd tabs (#11090)
4fd993c is described below

commit 4fd993c4e07f8bd1220d105fbaa18733daf5aebb
Author: Kamil Gabryjelski <ka...@gmail.com>
AuthorDate: Fri Oct 2 22:07:52 2020 +0200

    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 <ev...@preset.io>
    
    * Update superset-frontend/src/common/components/Dropdown.tsx
    
    Co-authored-by: Evan Rusackas <ev...@preset.io>
    
    * Update superset-frontend/src/common/components/Dropdown.tsx
    
    Co-authored-by: Evan Rusackas <ev...@preset.io>
    
    * Update superset-frontend/src/common/components/Dropdown.tsx
    
    Co-authored-by: Evan Rusackas <ev...@preset.io>
    
    * Update superset-frontend/src/common/components/Tabs.tsx
    
    Co-authored-by: Evan Rusackas <ev...@preset.io>
    
    * Update superset-frontend/src/common/components/Dropdown.tsx
    
    Co-authored-by: Evan Rusackas <ev...@preset.io>
    
    * Update superset-frontend/src/common/components/Dropdown.tsx
    
    Co-authored-by: Evan Rusackas <ev...@preset.io>
    
    * Remove inModal prop
    
    * Remove inModal from storybook
    
    * Move inline style to styled component
    
    Co-authored-by: Evan Rusackas <ev...@preset.io>
---
 .../cypress/integration/sqllab/tabs.test.js        |  15 +-
 .../spec/javascripts/profile/App_spec.tsx          |   5 +-
 .../javascripts/sqllab/TabbedSqlEditors_spec.jsx   |  24 +--
 .../src/SqlLab/components/SouthPane.jsx            |  24 +--
 .../src/SqlLab/components/TabbedSqlEditors.jsx     | 224 ++++++++++-----------
 .../src/common/components/Dropdown.tsx             |  78 +++++++
 superset-frontend/src/common/components/Modal.tsx  |   5 +
 superset-frontend/src/common/components/Tabs.tsx   |  75 ++++++-
 .../src/common/components/common.stories.tsx       |  61 +++++-
 superset-frontend/src/profile/components/App.tsx   |  37 ++--
 10 files changed, 369 insertions(+), 179 deletions(-)

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 ebcbadf..9a0e8f4 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 13b53b8..e45b199 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(<App {...mockedProps} />);
-    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 2d68232..2832b32 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 2b2b270..526508a 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 => (
-      <Tab
-        title={t('Preview: `%s`', decodeURIComponent(query.tableName))}
-        eventKey={query.id}
+      <Tabs.TabPane
+        tab={t('Preview: `%s`', decodeURIComponent(query.tableName))}
         key={query.id}
       >
         <ResultSet
@@ -154,29 +154,27 @@ export class SouthPane extends React.PureComponent {
           height={innerTabContentHeight}
           displayLimit={this.props.displayLimit}
         />
-      </Tab>
+      </Tabs.TabPane>
     ));
 
     return (
       <div className="SouthPane" ref={this.southPaneRef}>
         <Tabs
-          bsStyle="tabs"
-          animation={false}
+          defaultActiveKey={this.props.activeSouthPaneTab}
           className="SouthPaneTabs"
           id={shortid.generate()}
-          activeKey={this.props.activeSouthPaneTab}
-          onSelect={this.switchTab}
+          fullWidth={false}
         >
-          <Tab title={t('Results')} eventKey="Results">
+          <Tabs.TabPane tab={t('Results')} key="Results">
             {results}
-          </Tab>
-          <Tab title={t('Query History')} eventKey="History">
+          </Tabs.TabPane>
+          <Tabs.TabPane tab={t('Query History')} key="History">
             <QueryHistory
               queries={props.editorQueries}
               actions={props.actions}
               displayLimit={props.displayLimit}
             />
-          </Tab>
+          </Tabs.TabPane>
           {dataPreviewTabs}
         </Tabs>
       </div>
diff --git a/superset-frontend/src/SqlLab/components/TabbedSqlEditors.jsx b/superset-frontend/src/SqlLab/components/TabbedSqlEditors.jsx
index 8ee59d5..5461645 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} <TabStatusIcon tabState={state} />{' '}
-          <Icon
-            role="button"
-            tabIndex={0}
-            cursor="pointer"
-            name="cancel-x"
+      const menu = (
+        <Menu>
+          <Menu.Item
+            className="close-btn"
+            key="1"
             onClick={() => this.removeQueryEditor(qe)}
-          />
-        </>
+            data-test="close-tab-menu-option"
+          >
+            <div className="icon-container">
+              <i className="fa fa-close" />
+            </div>
+            {t('Close tab')}
+          </Menu.Item>
+          <Menu.Item key="2" onClick={() => this.renameTab(qe)}>
+            <div className="icon-container">
+              <i className="fa fa-i-cursor" />
+            </div>
+            {t('Rename tab')}
+          </Menu.Item>
+          <Menu.Item key="3" onClick={this.toggleLeftBar}>
+            <div className="icon-container">
+              <i className="fa fa-cogs" />
+            </div>
+            {this.state.hideLeftBar ? t('Expand tool bar') : t('Hide tool bar')}
+          </Menu.Item>
+          <Menu.Item
+            key="4"
+            onClick={() => this.removeAllOtherQueryEditors(qe)}
+          >
+            <div className="icon-container">
+              <i className="fa fa-times-circle-o" />
+            </div>
+            {t('Close all other tabs')}
+          </Menu.Item>
+          <Menu.Item key="5" onClick={() => this.duplicateQueryEditor(qe)}>
+            <div className="icon-container">
+              <i className="fa fa-files-o" />
+            </div>
+            {t('Duplicate tab')}
+          </Menu.Item>
+        </Menu>
       );
-      const tabTitle = (
+
+      const tabHeader = (
         <>
-          {isSelected && (
-            <DropdownButton
-              data-test="dropdown-toggle-button"
-              bsSize="small"
-              id={`ddbtn-tab-${i}`}
-              title={' '}
-              noCaret
-            >
-              <MenuItem
-                className="close-btn"
-                eventKey="1"
-                onClick={() => this.removeQueryEditor(qe)}
-                data-test="close-tab-menu-option"
-              >
-                <div className="icon-container">
-                  <i className="fa fa-close" />
-                </div>
-                {t('Close tab')}
-              </MenuItem>
-              <MenuItem eventKey="2" onClick={() => this.renameTab(qe)}>
-                <div className="icon-container">
-                  <i className="fa fa-i-cursor" />
-                </div>
-                {t('Rename tab')}
-              </MenuItem>
-              <MenuItem eventKey="3" onClick={this.toggleLeftBar}>
-                <div className="icon-container">
-                  <i className="fa fa-cogs" />
-                </div>
-                {this.state.hideLeftBar
-                  ? t('Expand tool bar')
-                  : t('Hide tool bar')}
-              </MenuItem>
-              <MenuItem
-                eventKey="4"
-                onClick={() => this.removeAllOtherQueryEditors(qe)}
-              >
-                <div className="icon-container">
-                  <i className="fa fa-times-circle-o" />
-                </div>
-                {t('Close all other tabs')}
-              </MenuItem>
-              <MenuItem
-                eventKey="5"
-                onClick={() => this.duplicateQueryEditor(qe)}
-              >
-                <div className="icon-container">
-                  <i className="fa fa-files-o" />
-                </div>
-                {t('Duplicate tab')}
-              </MenuItem>
-            </DropdownButton>
-          )}
-          <span className="ddbtn-tab">{title}</span>
+          <div data-test="dropdown-toggle-button">
+            <Dropdown overlay={menu} trigger={['click']} />
+          </div>
+          <TabTitle>{qe.title}</TabTitle> <TabStatusIcon tabState={state} />{' '}
         </>
       );
       return (
-        <Tab key={qe.id} title={tabTitle} eventKey={qe.id}>
-          {isSelected && (
-            <SqlEditor
-              tables={this.props.tables.filter(
-                xt => 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}
-            />
-          )}
-        </Tab>
+        <EditableTabs.TabPane
+          key={qe.id}
+          tab={tabHeader}
+          // for tests - key prop isn't handled by enzyme well bcs it's a react keyword
+          data-key={qe.id}
+        >
+          <SqlEditor
+            tables={this.props.tables.filter(xt => 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}
+          />
+        </EditableTabs.TabPane>
       );
     });
+
     return (
-      <Tabs
-        bsStyle="tabs"
-        animation={false}
+      <EditableTabs
         activeKey={this.props.tabHistory[this.props.tabHistory.length - 1]}
-        onSelect={this.handleSelect.bind(this)}
         id="a11y-query-editor-tabs"
         className="SqlEditorTabs"
         data-test="sql-editor-tabs"
+        onChange={this.handleSelect}
+        fullWidth={false}
+        hideAdd={this.props.offline}
+        onEdit={this.handleEdit}
+        addIcon={<i data-test="add-tab-icon" className="fa fa-plus-circle" />}
       >
         {editors}
-        <Tab
-          title={
-            <div>
-              <i data-test="add-tab-icon" className="fa fa-plus-circle" />
-              &nbsp;
-            </div>
-          }
-          className="addEditorTab"
-          eventKey="add_tab"
-          disabled={this.props.offline}
-        />
-      </Tabs>
+      </EditableTabs>
     );
   }
 }
diff --git a/superset-frontend/src/common/components/Dropdown.tsx b/superset-frontend/src/common/components/Dropdown.tsx
new file mode 100644
index 0000000..18ac168
--- /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) => (
+  <AntdDropdown overlay={overlay} {...rest}>
+    <MenuDotsWrapper>
+      <MenuDots />
+    </MenuDotsWrapper>
+  </AntdDropdown>
+);
diff --git a/superset-frontend/src/common/components/Modal.tsx b/superset-frontend/src/common/components/Modal.tsx
index 9e97f6f..9634cc3 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 932e444..9346d0a 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),
+})<TabsProps>`
   .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: (
+    <Icon role="button" tabIndex={0} cursor="pointer" name="cancel-x" />
+  ),
+};
+
 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 3ed541d..6b564d0 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 = () => (
-  <Tabs defaultActiveKey="1" centered={boolean('Center tabs', false)}>
+  <Tabs
+    defaultActiveKey="1"
+    centered={boolean('Center tabs', false)}
+    fullWidth={boolean('Full width', true)}
+  >
     <Tabs.TabPane
       tab="Tab 1"
       key="1"
@@ -59,3 +65,54 @@ export const StyledTabs = () => (
     </Tabs.TabPane>
   </Tabs>
 );
+
+export const StyledEditableTabs = () => (
+  <EditableTabs
+    defaultActiveKey="1"
+    centered={boolean('Center tabs', false)}
+    fullWidth={boolean('Full width', true)}
+  >
+    <Tabs.TabPane
+      tab="Tab 1"
+      key="1"
+      disabled={boolean('Tab 1 Disabled', false)}
+    >
+      Tab 1 Content!
+    </Tabs.TabPane>
+    <Tabs.TabPane
+      tab="Tab 2"
+      key="2"
+      disabled={boolean('Tab 2 Disabled', false)}
+    >
+      Tab 2 Content!
+    </Tabs.TabPane>
+  </EditableTabs>
+);
+
+export const TabsWithDropdownMenu = () => (
+  <EditableTabs
+    defaultActiveKey="1"
+    centered={boolean('Center tabs', false)}
+    fullWidth={boolean('Full width', true)}
+  >
+    <Tabs.TabPane
+      tab={
+        <>
+          <Dropdown
+            overlay={
+              <Menu>
+                <Menu.Item key="1">Item 1</Menu.Item>
+                <Menu.Item key="2">Item 2</Menu.Item>
+              </Menu>
+            }
+          />
+          Tab with dropdown menu
+        </>
+      }
+      key="1"
+      disabled={boolean('Tab 1 Disabled', false)}
+    >
+      Tab 1 Content!
+    </Tabs.TabPane>
+  </EditableTabs>
+);
diff --git a/superset-frontend/src/profile/components/App.tsx b/superset-frontend/src/profile/components/App.tsx
index 0d18199..8642dd7 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) {
           <UserInfo user={user} />
         </Col>
         <Col md={9}>
-          <Tabs id="options">
-            <Tab
-              eventKey={1}
-              title={
+          <Tabs centered>
+            <Tabs.TabPane
+              key="1"
+              tab={
                 <div>
                   <i className="fa fa-star" /> {t('Favorites')}
                 </div>
@@ -53,10 +54,10 @@ export default function App({ user }: AppProps) {
                   <Favorites user={user} />
                 </Panel.Body>
               </Panel>
-            </Tab>
-            <Tab
-              eventKey={2}
-              title={
+            </Tabs.TabPane>
+            <Tabs.TabPane
+              key="2"
+              tab={
                 <div>
                   <i className="fa fa-paint-brush" /> {t('Created Content')}
                 </div>
@@ -67,10 +68,10 @@ export default function App({ user }: AppProps) {
                   <CreatedContent user={user} />
                 </Panel.Body>
               </Panel>
-            </Tab>
-            <Tab
-              eventKey={3}
-              title={
+            </Tabs.TabPane>
+            <Tabs.TabPane
+              key="3"
+              tab={
                 <div>
                   <i className="fa fa-list" /> {t('Recent Activity')}
                 </div>
@@ -81,10 +82,10 @@ export default function App({ user }: AppProps) {
                   <RecentActivity user={user} />
                 </Panel.Body>
               </Panel>
-            </Tab>
-            <Tab
-              eventKey={4}
-              title={
+            </Tabs.TabPane>
+            <Tabs.TabPane
+              key="4"
+              tab={
                 <div>
                   <i className="fa fa-lock" /> {t('Security & Access')}
                 </div>
@@ -95,7 +96,7 @@ export default function App({ user }: AppProps) {
                   <Security user={user} />
                 </Panel.Body>
               </Panel>
-            </Tab>
+            </Tabs.TabPane>
           </Tabs>
         </Col>
       </Row>