mirror of
https://github.com/apache/superset.git
synced 2024-09-17 19:19:38 -04:00
refactor: Replace react-bootstrap Tabs with Antd Tabs in DashboardBuilder (#11160)
* Replace tabs in DashboardBuilder * Fix tests * Fix styling of anchor * Fix * Fix cypress test * Fix tests * Fix e2e tests * Use data-tests * Move tabs styles from superset.less to Emotion * Restyle tabs in DashboardBuilder * Test fix * Fix styling
This commit is contained in:
parent
6c6ded139b
commit
a874b14a8a
@ -97,15 +97,24 @@ describe('Dashboard tabs', () => {
|
|||||||
|
|
||||||
cy.get('[data-test="dashboard-component-tabs"]')
|
cy.get('[data-test="dashboard-component-tabs"]')
|
||||||
.first()
|
.first()
|
||||||
.find('[data-test="nav-list"]')
|
.find('[data-test="nav-list"] .ant-tabs-nav-list > .ant-tabs-tab')
|
||||||
.children()
|
|
||||||
.as('top-level-tabs');
|
.as('top-level-tabs');
|
||||||
|
|
||||||
cy.get('@top-level-tabs').first().click().should('have.class', 'active');
|
cy.get('@top-level-tabs')
|
||||||
cy.get('@top-level-tabs').last().should('not.have.class', 'active');
|
.first()
|
||||||
|
.click()
|
||||||
|
.should('have.class', 'ant-tabs-tab-active');
|
||||||
|
cy.get('@top-level-tabs')
|
||||||
|
.last()
|
||||||
|
.should('not.have.class', 'ant-tabs-tab-active');
|
||||||
|
|
||||||
cy.get('@top-level-tabs').last().click().should('have.class', 'active');
|
cy.get('@top-level-tabs')
|
||||||
cy.get('@top-level-tabs').first().should('not.have.class', 'active');
|
.last()
|
||||||
|
.click()
|
||||||
|
.should('have.class', 'ant-tabs-tab-active');
|
||||||
|
cy.get('@top-level-tabs')
|
||||||
|
.first()
|
||||||
|
.should('not.have.class', 'ant-tabs-tab-active');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should load charts when tab is visible', () => {
|
it('should load charts when tab is visible', () => {
|
||||||
@ -128,8 +137,7 @@ describe('Dashboard tabs', () => {
|
|||||||
// click row level tab, see 1 more chart
|
// click row level tab, see 1 more chart
|
||||||
cy.get('[data-test="dashboard-component-tabs"]')
|
cy.get('[data-test="dashboard-component-tabs"]')
|
||||||
.last()
|
.last()
|
||||||
.find('[data-test="nav-list"]')
|
.find('[data-test="nav-list"] .ant-tabs-nav-list > .ant-tabs-tab')
|
||||||
.children()
|
|
||||||
.as('row-level-tabs');
|
.as('row-level-tabs');
|
||||||
|
|
||||||
cy.get('@row-level-tabs').last().click();
|
cy.get('@row-level-tabs').last().click();
|
||||||
@ -141,8 +149,7 @@ describe('Dashboard tabs', () => {
|
|||||||
handleException();
|
handleException();
|
||||||
cy.get('[data-test="dashboard-component-tabs"]')
|
cy.get('[data-test="dashboard-component-tabs"]')
|
||||||
.first()
|
.first()
|
||||||
.find('[data-test="nav-list"]')
|
.find('[data-test="nav-list"] .ant-tabs-nav-list > .ant-tabs-tab')
|
||||||
.children()
|
|
||||||
.as('top-level-tabs');
|
.as('top-level-tabs');
|
||||||
|
|
||||||
cy.get('@top-level-tabs').last().click();
|
cy.get('@top-level-tabs').last().click();
|
||||||
|
@ -179,7 +179,7 @@ describe('DashboardBuilder', () => {
|
|||||||
expect(wrapper.find(TabContainer).prop('activeKey')).toBe(0);
|
expect(wrapper.find(TabContainer).prop('activeKey')).toBe(0);
|
||||||
|
|
||||||
wrapper
|
wrapper
|
||||||
.find('.dashboard-component-tabs .nav-tabs a')
|
.find('.dashboard-component-tabs .ant-tabs .ant-tabs-tab')
|
||||||
.at(1)
|
.at(1)
|
||||||
.simulate('click');
|
.simulate('click');
|
||||||
|
|
||||||
|
@ -22,10 +22,8 @@ import { styledMount as mount } from 'spec/helpers/theming';
|
|||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
|
|
||||||
import DashboardComponent from 'src/dashboard/containers/DashboardComponent';
|
import DashboardComponent from 'src/dashboard/containers/DashboardComponent';
|
||||||
import DeleteComponentModal from 'src/dashboard/components/DeleteComponentModal';
|
|
||||||
import DragDroppable from 'src/dashboard/components/dnd/DragDroppable';
|
import DragDroppable from 'src/dashboard/components/dnd/DragDroppable';
|
||||||
import EditableTitle from 'src/components/EditableTitle';
|
import EditableTitle from 'src/components/EditableTitle';
|
||||||
import WithPopoverMenu from 'src/dashboard/components/menu/WithPopoverMenu';
|
|
||||||
import Tab, {
|
import Tab, {
|
||||||
RENDER_TAB,
|
RENDER_TAB,
|
||||||
RENDER_TAB_CONTENT,
|
RENDER_TAB_CONTENT,
|
||||||
@ -96,42 +94,6 @@ describe('Tabs', () => {
|
|||||||
'New title',
|
'New title',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render a WithPopoverMenu', () => {
|
|
||||||
const wrapper = setup();
|
|
||||||
expect(wrapper.find(WithPopoverMenu)).toExist();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render a DeleteComponentModal when focused if its not the only tab', () => {
|
|
||||||
let wrapper = setup();
|
|
||||||
wrapper.find(WithPopoverMenu).simulate('click'); // focus
|
|
||||||
expect(wrapper.find(DeleteComponentModal)).not.toExist();
|
|
||||||
|
|
||||||
wrapper = setup({ editMode: true });
|
|
||||||
wrapper.find(WithPopoverMenu).simulate('click');
|
|
||||||
expect(wrapper.find(DeleteComponentModal)).toExist();
|
|
||||||
|
|
||||||
wrapper = setup({
|
|
||||||
editMode: true,
|
|
||||||
parentComponent: {
|
|
||||||
...props.parentComponent,
|
|
||||||
children: props.parentComponent.children.slice(0, 1),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
wrapper.find(WithPopoverMenu).simulate('click');
|
|
||||||
expect(wrapper.find(DeleteComponentModal)).not.toExist();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should show modal when clicked delete icon', () => {
|
|
||||||
const deleteComponent = sinon.spy();
|
|
||||||
const wrapper = setup({ editMode: true, deleteComponent });
|
|
||||||
wrapper.find(WithPopoverMenu).simulate('click'); // focus
|
|
||||||
wrapper.find('.icon-button').simulate('click');
|
|
||||||
|
|
||||||
const modal = document.getElementsByClassName('ant-modal');
|
|
||||||
expect(modal).toHaveLength(1);
|
|
||||||
expect(deleteComponent.callCount).toBe(0);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('renderType=RENDER_TAB_CONTENT', () => {
|
describe('renderType=RENDER_TAB_CONTENT', () => {
|
||||||
|
@ -18,11 +18,12 @@
|
|||||||
*/
|
*/
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mount, shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
import { Tabs as BootstrapTabs, Tab as BootstrapTab } from 'react-bootstrap';
|
import { LineEditableTabs } from 'src/common/components/Tabs';
|
||||||
import { supersetTheme, ThemeProvider } from '@superset-ui/core';
|
import { Modal } from 'src/common/components';
|
||||||
|
|
||||||
|
import { styledMount as mount } from 'spec/helpers/theming';
|
||||||
import DashboardComponent from 'src/dashboard/containers/DashboardComponent';
|
import DashboardComponent from 'src/dashboard/containers/DashboardComponent';
|
||||||
import DeleteComponentButton from 'src/dashboard/components/DeleteComponentButton';
|
import DeleteComponentButton from 'src/dashboard/components/DeleteComponentButton';
|
||||||
import HoverMenu from 'src/dashboard/components/menu/HoverMenu';
|
import HoverMenu from 'src/dashboard/components/menu/HoverMenu';
|
||||||
@ -54,6 +55,7 @@ describe('Tabs', () => {
|
|||||||
deleteComponent() {},
|
deleteComponent() {},
|
||||||
updateComponents() {},
|
updateComponents() {},
|
||||||
logEvent() {},
|
logEvent() {},
|
||||||
|
setMountedTab() {},
|
||||||
};
|
};
|
||||||
|
|
||||||
function setup(overrideProps) {
|
function setup(overrideProps) {
|
||||||
@ -65,10 +67,6 @@ describe('Tabs', () => {
|
|||||||
<Tabs {...props} {...overrideProps} />
|
<Tabs {...props} {...overrideProps} />
|
||||||
</WithDragDropContext>
|
</WithDragDropContext>
|
||||||
</Provider>,
|
</Provider>,
|
||||||
{
|
|
||||||
wrappingComponent: ThemeProvider,
|
|
||||||
wrappingComponentProps: { theme: supersetTheme },
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
return wrapper;
|
return wrapper;
|
||||||
}
|
}
|
||||||
@ -79,31 +77,23 @@ describe('Tabs', () => {
|
|||||||
expect(wrapper.find(DragDroppable)).toExist();
|
expect(wrapper.find(DragDroppable)).toExist();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render BootstrapTabs', () => {
|
it('should render non-editable tabs', () => {
|
||||||
const wrapper = setup();
|
const wrapper = setup();
|
||||||
expect(wrapper.find(BootstrapTabs)).toExist();
|
expect(wrapper.find(LineEditableTabs)).toExist();
|
||||||
|
expect(wrapper.find('.ant-tabs-nav-add').exists()).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set animation=true, mountOnEnter=true, and unmounOnExit=false on BootstrapTabs for perf', () => {
|
it('should render a tab pane for each child', () => {
|
||||||
const wrapper = setup();
|
const wrapper = setup();
|
||||||
const tabProps = wrapper.find(BootstrapTabs).props();
|
expect(wrapper.find(LineEditableTabs.TabPane)).toHaveLength(
|
||||||
expect(tabProps.animation).toBe(true);
|
|
||||||
expect(tabProps.mountOnEnter).toBe(true);
|
|
||||||
expect(tabProps.unmountOnExit).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render a BootstrapTab for each child', () => {
|
|
||||||
const wrapper = setup();
|
|
||||||
expect(wrapper.find(BootstrapTab)).toHaveLength(
|
|
||||||
props.component.children.length,
|
props.component.children.length,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render an extra (+) BootstrapTab in editMode', () => {
|
it('should render editable tabs in editMode', () => {
|
||||||
const wrapper = setup({ editMode: true });
|
const wrapper = setup({ editMode: true });
|
||||||
expect(wrapper.find(BootstrapTab)).toHaveLength(
|
expect(wrapper.find(LineEditableTabs)).toExist();
|
||||||
props.component.children.length + 1,
|
expect(wrapper.find('.ant-tabs-nav-add')).toExist();
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render a DashboardComponent for each child', () => {
|
it('should render a DashboardComponent for each child', () => {
|
||||||
@ -118,7 +108,7 @@ describe('Tabs', () => {
|
|||||||
const createComponent = sinon.spy();
|
const createComponent = sinon.spy();
|
||||||
const wrapper = setup({ editMode: true, createComponent });
|
const wrapper = setup({ editMode: true, createComponent });
|
||||||
wrapper
|
wrapper
|
||||||
.find('.dashboard-component-tabs .nav-tabs a')
|
.find('[data-test="dashboard-component-tabs"] .ant-tabs-nav-add')
|
||||||
.last()
|
.last()
|
||||||
.simulate('click');
|
.simulate('click');
|
||||||
|
|
||||||
@ -129,7 +119,7 @@ describe('Tabs', () => {
|
|||||||
const onChangeTab = sinon.spy();
|
const onChangeTab = sinon.spy();
|
||||||
const wrapper = setup({ editMode: true, onChangeTab });
|
const wrapper = setup({ editMode: true, onChangeTab });
|
||||||
wrapper
|
wrapper
|
||||||
.find('.dashboard-component-tabs .nav-tabs a')
|
.find('[data-test="dashboard-component-tabs"] .ant-tabs-tab')
|
||||||
.at(1) // will not call if it is already selected
|
.at(1) // will not call if it is already selected
|
||||||
.simulate('click');
|
.simulate('click');
|
||||||
|
|
||||||
@ -140,7 +130,9 @@ describe('Tabs', () => {
|
|||||||
const onChangeTab = sinon.spy();
|
const onChangeTab = sinon.spy();
|
||||||
const wrapper = setup({ editMode: true, onChangeTab });
|
const wrapper = setup({ editMode: true, onChangeTab });
|
||||||
wrapper
|
wrapper
|
||||||
.find('.dashboard-component-tabs .nav-tabs a .short-link-trigger')
|
.find(
|
||||||
|
'[data-test="dashboard-component-tabs"] .ant-tabs-tab [data-test="short-link-button"]',
|
||||||
|
)
|
||||||
.at(1) // will not call if it is already selected
|
.at(1) // will not call if it is already selected
|
||||||
.simulate('click');
|
.simulate('click');
|
||||||
|
|
||||||
@ -186,4 +178,13 @@ describe('Tabs', () => {
|
|||||||
wrapper = shallow(<Tabs {...directLinkProps} />);
|
wrapper = shallow(<Tabs {...directLinkProps} />);
|
||||||
expect(wrapper.state('tabIndex')).toBe(1);
|
expect(wrapper.state('tabIndex')).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should render Modal when clicked remove tab button', () => {
|
||||||
|
const deleteComponent = sinon.spy();
|
||||||
|
const modalMock = jest.spyOn(Modal, 'confirm');
|
||||||
|
const wrapper = setup({ editMode: true, deleteComponent });
|
||||||
|
wrapper.find('.ant-tabs-tab-remove').at(0).simulate('click');
|
||||||
|
expect(modalMock.mock.calls).toHaveLength(1);
|
||||||
|
expect(deleteComponent.callCount).toBe(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -40,6 +40,24 @@ const StyledTabs = styled(AntdTabs, {
|
|||||||
&.ant-tabs-tab-active .ant-tabs-tab-btn {
|
&.ant-tabs-tab-active .ant-tabs-tab-btn {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.anchor-link-container {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.fa.fa-link {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.short-link-trigger.btn {
|
||||||
|
padding: 0 ${({ theme }) => theme.gridUnit}px;
|
||||||
|
|
||||||
|
& > .fa.fa-link {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
${({ fullWidth }) =>
|
${({ fullWidth }) =>
|
||||||
@ -124,15 +142,37 @@ EditableTabs.TabPane.defaultProps = {
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
const StyledCardTabs = styled(EditableTabs)``;
|
const StyledLineEditableTabs = styled(EditableTabs)`
|
||||||
|
&.ant-tabs-card > .ant-tabs-nav .ant-tabs-tab {
|
||||||
|
margin: 0 ${({ theme }) => theme.gridUnit * 4}px;
|
||||||
|
padding: ${({ theme }) => `${theme.gridUnit * 3}px ${theme.gridUnit}px`};
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
const CardTabs = Object.assign(StyledCardTabs, {
|
&.ant-tabs-card > .ant-tabs-nav .ant-tabs-ink-bar {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tabs-tab-btn {
|
||||||
|
font-size: ${({ theme }) => theme.typography.sizes.m}px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tabs-tab-remove {
|
||||||
|
margin-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tabs-nav-add {
|
||||||
|
min-width: unset !important;
|
||||||
|
background: transparent !important;
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const LineEditableTabs = Object.assign(StyledLineEditableTabs, {
|
||||||
TabPane: StyledTabPane,
|
TabPane: StyledTabPane,
|
||||||
});
|
});
|
||||||
|
|
||||||
CardTabs.defaultProps = {
|
|
||||||
type: 'card',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Tabs;
|
export default Tabs;
|
||||||
export { CardTabs, EditableTabs };
|
export { EditableTabs, LineEditableTabs };
|
||||||
|
@ -48,7 +48,8 @@ class URLShortLinkButton extends React.Component {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
getCopyUrl() {
|
getCopyUrl(e) {
|
||||||
|
e.stopPropagation();
|
||||||
getShortUrl(this.props.url)
|
getShortUrl(this.props.url)
|
||||||
.then(this.onShortUrlSuccess)
|
.then(this.onShortUrlSuccess)
|
||||||
.catch(this.props.addDangerToast);
|
.catch(this.props.addDangerToast);
|
||||||
|
@ -101,7 +101,7 @@ class DashboardBuilder extends React.Component {
|
|||||||
static shouldFocusTabs(event, container) {
|
static shouldFocusTabs(event, container) {
|
||||||
// don't focus the tabs when we click on a tab
|
// don't focus the tabs when we click on a tab
|
||||||
return (
|
return (
|
||||||
event.target.tagName === 'UL' ||
|
event.target.className === 'ant-tabs-nav-wrap' ||
|
||||||
(/icon-button/.test(event.target.className) &&
|
(/icon-button/.test(event.target.className) &&
|
||||||
container.contains(event.target))
|
container.contains(event.target))
|
||||||
);
|
);
|
||||||
|
@ -23,8 +23,6 @@ import DashboardComponent from '../../containers/DashboardComponent';
|
|||||||
import DragDroppable from '../dnd/DragDroppable';
|
import DragDroppable from '../dnd/DragDroppable';
|
||||||
import EditableTitle from '../../../components/EditableTitle';
|
import EditableTitle from '../../../components/EditableTitle';
|
||||||
import AnchorLink from '../../../components/AnchorLink';
|
import AnchorLink from '../../../components/AnchorLink';
|
||||||
import DeleteComponentModal from '../DeleteComponentModal';
|
|
||||||
import WithPopoverMenu from '../menu/WithPopoverMenu';
|
|
||||||
import { componentShape } from '../../util/propShapes';
|
import { componentShape } from '../../util/propShapes';
|
||||||
|
|
||||||
export const RENDER_TAB = 'RENDER_TAB';
|
export const RENDER_TAB = 'RENDER_TAB';
|
||||||
@ -39,7 +37,6 @@ const propTypes = {
|
|||||||
depth: PropTypes.number.isRequired,
|
depth: PropTypes.number.isRequired,
|
||||||
renderType: PropTypes.oneOf([RENDER_TAB, RENDER_TAB_CONTENT]).isRequired,
|
renderType: PropTypes.oneOf([RENDER_TAB, RENDER_TAB_CONTENT]).isRequired,
|
||||||
onDropOnTab: PropTypes.func,
|
onDropOnTab: PropTypes.func,
|
||||||
onDeleteTab: PropTypes.func,
|
|
||||||
editMode: PropTypes.bool.isRequired,
|
editMode: PropTypes.bool.isRequired,
|
||||||
filters: PropTypes.object.isRequired,
|
filters: PropTypes.object.isRequired,
|
||||||
|
|
||||||
@ -52,7 +49,6 @@ const propTypes = {
|
|||||||
|
|
||||||
// redux
|
// redux
|
||||||
handleComponentDrop: PropTypes.func.isRequired,
|
handleComponentDrop: PropTypes.func.isRequired,
|
||||||
deleteComponent: PropTypes.func.isRequired,
|
|
||||||
updateComponents: PropTypes.func.isRequired,
|
updateComponents: PropTypes.func.isRequired,
|
||||||
setDirectPathToChild: PropTypes.func.isRequired,
|
setDirectPathToChild: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
@ -61,7 +57,6 @@ const defaultProps = {
|
|||||||
availableColumnCount: 0,
|
availableColumnCount: 0,
|
||||||
columnWidth: 0,
|
columnWidth: 0,
|
||||||
onDropOnTab() {},
|
onDropOnTab() {},
|
||||||
onDeleteTab() {},
|
|
||||||
onResizeStart() {},
|
onResizeStart() {},
|
||||||
onResize() {},
|
onResize() {},
|
||||||
onResizeStop() {},
|
onResizeStop() {},
|
||||||
@ -70,21 +65,12 @@ const defaultProps = {
|
|||||||
export default class Tab extends React.PureComponent {
|
export default class Tab extends React.PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
|
||||||
isFocused: false,
|
|
||||||
};
|
|
||||||
this.handleChangeFocus = this.handleChangeFocus.bind(this);
|
|
||||||
this.handleChangeText = this.handleChangeText.bind(this);
|
this.handleChangeText = this.handleChangeText.bind(this);
|
||||||
this.handleDeleteComponent = this.handleDeleteComponent.bind(this);
|
|
||||||
this.handleDrop = this.handleDrop.bind(this);
|
this.handleDrop = this.handleDrop.bind(this);
|
||||||
this.handleTopDropTargetDrop = this.handleTopDropTargetDrop.bind(this);
|
this.handleTopDropTargetDrop = this.handleTopDropTargetDrop.bind(this);
|
||||||
this.handleChangeTab = this.handleChangeTab.bind(this);
|
this.handleChangeTab = this.handleChangeTab.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleChangeFocus(nextFocus) {
|
|
||||||
this.setState(() => ({ isFocused: nextFocus }));
|
|
||||||
}
|
|
||||||
|
|
||||||
handleChangeTab({ pathToTabIndex }) {
|
handleChangeTab({ pathToTabIndex }) {
|
||||||
this.props.setDirectPathToChild(pathToTabIndex);
|
this.props.setDirectPathToChild(pathToTabIndex);
|
||||||
}
|
}
|
||||||
@ -104,12 +90,6 @@ export default class Tab extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDeleteComponent() {
|
|
||||||
const { index, id, parentId } = this.props;
|
|
||||||
this.props.deleteComponent(id, parentId);
|
|
||||||
this.props.onDeleteTab(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleDrop(dropResult) {
|
handleDrop(dropResult) {
|
||||||
this.props.handleComponentDrop(dropResult);
|
this.props.handleComponentDrop(dropResult);
|
||||||
this.props.onDropOnTab(dropResult);
|
this.props.onDropOnTab(dropResult);
|
||||||
@ -204,7 +184,6 @@ export default class Tab extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderTab() {
|
renderTab() {
|
||||||
const { isFocused } = this.state;
|
|
||||||
const {
|
const {
|
||||||
component,
|
component,
|
||||||
parentComponent,
|
parentComponent,
|
||||||
@ -212,12 +191,8 @@ export default class Tab extends React.PureComponent {
|
|||||||
depth,
|
depth,
|
||||||
editMode,
|
editMode,
|
||||||
filters,
|
filters,
|
||||||
|
isFocused,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const deleteTabIcon = (
|
|
||||||
<div className="icon-button">
|
|
||||||
<span className="fa fa-trash" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DragDroppable
|
<DragDroppable
|
||||||
@ -229,22 +204,9 @@ export default class Tab extends React.PureComponent {
|
|||||||
onDrop={this.handleDrop}
|
onDrop={this.handleDrop}
|
||||||
editMode={editMode}
|
editMode={editMode}
|
||||||
>
|
>
|
||||||
{({ dropIndicatorProps, dragSourceRef }) => (
|
{({ dropIndicatorProps, dragSourceRef }) => {
|
||||||
|
return (
|
||||||
<div className="dragdroppable-tab" ref={dragSourceRef}>
|
<div className="dragdroppable-tab" ref={dragSourceRef}>
|
||||||
<WithPopoverMenu
|
|
||||||
onChangeFocus={this.handleChangeFocus}
|
|
||||||
menuItems={
|
|
||||||
parentComponent.children.length <= 1
|
|
||||||
? []
|
|
||||||
: [
|
|
||||||
<DeleteComponentModal
|
|
||||||
triggerNode={deleteTabIcon}
|
|
||||||
onDelete={this.handleDeleteComponent}
|
|
||||||
/>,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
editMode={editMode}
|
|
||||||
>
|
|
||||||
<EditableTitle
|
<EditableTitle
|
||||||
title={component.meta.text}
|
title={component.meta.text}
|
||||||
canEdit={editMode && isFocused}
|
canEdit={editMode && isFocused}
|
||||||
@ -259,11 +221,11 @@ export default class Tab extends React.PureComponent {
|
|||||||
placement={index >= 5 ? 'left' : 'right'}
|
placement={index >= 5 ? 'left' : 'right'}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</WithPopoverMenu>
|
|
||||||
|
|
||||||
{dropIndicatorProps && <div {...dropIndicatorProps} />}
|
{dropIndicatorProps && <div {...dropIndicatorProps} />}
|
||||||
</div>
|
</div>
|
||||||
)}
|
);
|
||||||
|
}}
|
||||||
</DragDroppable>
|
</DragDroppable>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -18,8 +18,10 @@
|
|||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Tabs as BootstrapTabs, Tab as BootstrapTab } from 'react-bootstrap';
|
import { LineEditableTabs } from 'src/common/components/Tabs';
|
||||||
|
import { LOG_ACTIONS_SELECT_DASHBOARD_TAB } from 'src/logger/LogUtils';
|
||||||
|
import { Modal } from 'src/common/components';
|
||||||
|
import { styled, t } from '@superset-ui/core';
|
||||||
import DragDroppable from '../dnd/DragDroppable';
|
import DragDroppable from '../dnd/DragDroppable';
|
||||||
import DragHandle from '../dnd/DragHandle';
|
import DragHandle from '../dnd/DragHandle';
|
||||||
import DashboardComponent from '../../containers/DashboardComponent';
|
import DashboardComponent from '../../containers/DashboardComponent';
|
||||||
@ -32,9 +34,7 @@ import { componentShape } from '../../util/propShapes';
|
|||||||
import { NEW_TAB_ID, DASHBOARD_ROOT_ID } from '../../util/constants';
|
import { NEW_TAB_ID, DASHBOARD_ROOT_ID } from '../../util/constants';
|
||||||
import { RENDER_TAB, RENDER_TAB_CONTENT } from './Tab';
|
import { RENDER_TAB, RENDER_TAB_CONTENT } from './Tab';
|
||||||
import { TAB_TYPE } from '../../util/componentTypes';
|
import { TAB_TYPE } from '../../util/componentTypes';
|
||||||
import { LOG_ACTIONS_SELECT_DASHBOARD_TAB } from '../../../logger/LogUtils';
|
|
||||||
|
|
||||||
const NEW_TAB_INDEX = -1;
|
|
||||||
const MAX_TAB_COUNT = 10;
|
const MAX_TAB_COUNT = 10;
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
@ -79,6 +79,38 @@ const defaultProps = {
|
|||||||
onResizeStop() {},
|
onResizeStop() {},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const StyledTabsContainer = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
background-color: ${({ theme }) => theme.colors.grayscale.light5};
|
||||||
|
|
||||||
|
.dashboard-component-tabs-content {
|
||||||
|
min-height: ${({ theme }) => theme.gridUnit * 12}px;
|
||||||
|
margin-top: ${({ theme }) => theme.gridUnit / 4}px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-indicator--left {
|
||||||
|
left: ${({ theme }) => -theme.gridUnit * 3}px !important;
|
||||||
|
}
|
||||||
|
.drop-indicator--right {
|
||||||
|
left: ${({ theme }) => `calc(100% + ${theme.gridUnit * 6}px)`} !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-indicator--bottom,
|
||||||
|
.drop-indicator--top {
|
||||||
|
width: ${({ theme }) => `calc(100% + ${theme.gridUnit * 6}px)`} !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-indicator--top {
|
||||||
|
top: ${({ theme }) => theme.gridUnit * 2}px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editable-title input {
|
||||||
|
cursor: pointer;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
class Tabs extends React.PureComponent {
|
class Tabs extends React.PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
@ -127,19 +159,32 @@ class Tabs extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClickTab(tabIndex, ev) {
|
showDeleteConfirmModal = key => {
|
||||||
if (ev) {
|
const { component, deleteComponent } = this.props;
|
||||||
const { target } = ev;
|
Modal.confirm({
|
||||||
// special handler for clicking on anchor link icon (or whitespace nearby):
|
title: t('Delete dashboard tab?'),
|
||||||
// will show short link popover but do not change tab
|
content: (
|
||||||
if (target && target.classList.contains('short-link-trigger')) {
|
<span>
|
||||||
return;
|
Deleting a tab will remove all content within it. You may still
|
||||||
}
|
reverse this action with the <b>undo</b> button (cmd + z) until you
|
||||||
}
|
save your changes.
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
onOk: () => {
|
||||||
|
deleteComponent(key, component.id);
|
||||||
|
const tabIndex = component.children.indexOf(key);
|
||||||
|
this.handleClickTab(Math.max(0, tabIndex - 1));
|
||||||
|
},
|
||||||
|
okType: 'danger',
|
||||||
|
okText: 'DELETE',
|
||||||
|
cancelText: 'CANCEL',
|
||||||
|
icon: null,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
handleEdit = (key, action) => {
|
||||||
const { component, createComponent } = this.props;
|
const { component, createComponent } = this.props;
|
||||||
|
if (action === 'add') {
|
||||||
if (tabIndex === NEW_TAB_INDEX) {
|
|
||||||
createComponent({
|
createComponent({
|
||||||
destination: {
|
destination: {
|
||||||
id: component.id,
|
id: component.id,
|
||||||
@ -151,7 +196,15 @@ class Tabs extends React.PureComponent {
|
|||||||
type: TAB_TYPE,
|
type: TAB_TYPE,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else if (tabIndex !== this.state.tabIndex) {
|
} else if (action === 'remove') {
|
||||||
|
this.showDeleteConfirmModal(key);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleClickTab(tabIndex) {
|
||||||
|
const { component, renderTabContent } = this.props;
|
||||||
|
|
||||||
|
if (tabIndex !== this.state.tabIndex) {
|
||||||
const pathToTabIndex = getDirectPathToTabIndex(component, tabIndex);
|
const pathToTabIndex = getDirectPathToTabIndex(component, tabIndex);
|
||||||
const targetTabId = pathToTabIndex[pathToTabIndex.length - 1];
|
const targetTabId = pathToTabIndex[pathToTabIndex.length - 1];
|
||||||
this.props.logEvent(LOG_ACTIONS_SELECT_DASHBOARD_TAB, {
|
this.props.logEvent(LOG_ACTIONS_SELECT_DASHBOARD_TAB, {
|
||||||
@ -161,6 +214,11 @@ class Tabs extends React.PureComponent {
|
|||||||
|
|
||||||
this.props.onChangeTab({ pathToTabIndex });
|
this.props.onChangeTab({ pathToTabIndex });
|
||||||
}
|
}
|
||||||
|
if (renderTabContent) {
|
||||||
|
const tabIds = component.children;
|
||||||
|
const activeKey = tabIds[this.state.tabIndex];
|
||||||
|
this.props.setMountedTab(activeKey);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDeleteComponent() {
|
handleDeleteComponent() {
|
||||||
@ -212,6 +270,8 @@ class Tabs extends React.PureComponent {
|
|||||||
const { tabIndex: selectedTabIndex } = this.state;
|
const { tabIndex: selectedTabIndex } = this.state;
|
||||||
const { children: tabIds } = tabsComponent;
|
const { children: tabIds } = tabsComponent;
|
||||||
|
|
||||||
|
const activeKey = tabIds[selectedTabIndex];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DragDroppable
|
<DragDroppable
|
||||||
component={tabsComponent}
|
component={tabsComponent}
|
||||||
@ -226,7 +286,7 @@ class Tabs extends React.PureComponent {
|
|||||||
dropIndicatorProps: tabsDropIndicatorProps,
|
dropIndicatorProps: tabsDropIndicatorProps,
|
||||||
dragSourceRef: tabsDragSourceRef,
|
dragSourceRef: tabsDragSourceRef,
|
||||||
}) => (
|
}) => (
|
||||||
<div
|
<StyledTabsContainer
|
||||||
className="dashboard-component dashboard-component-tabs"
|
className="dashboard-component dashboard-component-tabs"
|
||||||
data-test="dashboard-component-tabs"
|
data-test="dashboard-component-tabs"
|
||||||
>
|
>
|
||||||
@ -237,23 +297,21 @@ class Tabs extends React.PureComponent {
|
|||||||
</HoverMenu>
|
</HoverMenu>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<BootstrapTabs
|
<LineEditableTabs
|
||||||
id={tabsComponent.id}
|
id={tabsComponent.id}
|
||||||
activeKey={selectedTabIndex}
|
activeKey={activeKey}
|
||||||
onSelect={this.handleClickTab}
|
onChange={key => {
|
||||||
animation
|
this.handleClickTab(tabIds.indexOf(key));
|
||||||
mountOnEnter
|
}}
|
||||||
unmountOnExit={false}
|
onEdit={this.handleEdit}
|
||||||
|
hideAdd={tabIds.length >= MAX_TAB_COUNT}
|
||||||
data-test="nav-list"
|
data-test="nav-list"
|
||||||
|
type={editMode ? 'editable-card' : 'card'}
|
||||||
>
|
>
|
||||||
{tabIds.map((tabId, tabIndex) => (
|
{tabIds.map((tabId, tabIndex) => (
|
||||||
// react-bootstrap doesn't render a Tab if we move this to its own Tab.jsx so we
|
<LineEditableTabs.TabPane
|
||||||
// use `renderType` to indicate what the DashboardComponent should render. This
|
|
||||||
// prevents us from passing the entire dashboard component lookup to render Tabs.jsx
|
|
||||||
<BootstrapTab
|
|
||||||
key={tabId}
|
key={tabId}
|
||||||
eventKey={tabIndex}
|
tab={
|
||||||
title={
|
|
||||||
<DashboardComponent
|
<DashboardComponent
|
||||||
id={tabId}
|
id={tabId}
|
||||||
parentId={tabsComponent.id}
|
parentId={tabsComponent.id}
|
||||||
@ -263,15 +321,9 @@ class Tabs extends React.PureComponent {
|
|||||||
availableColumnCount={availableColumnCount}
|
availableColumnCount={availableColumnCount}
|
||||||
columnWidth={columnWidth}
|
columnWidth={columnWidth}
|
||||||
onDropOnTab={this.handleDropOnTab}
|
onDropOnTab={this.handleDropOnTab}
|
||||||
onDeleteTab={this.handleDeleteTab}
|
isFocused={activeKey === tabId}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
onEntering={() => {
|
|
||||||
// Entering current tab, DOM is visible and has dimension
|
|
||||||
if (renderTabContent) {
|
|
||||||
this.props.setMountedTab(tabId);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{renderTabContent && (
|
{renderTabContent && (
|
||||||
<DashboardComponent
|
<DashboardComponent
|
||||||
@ -291,23 +343,16 @@ class Tabs extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</BootstrapTab>
|
</LineEditableTabs.TabPane>
|
||||||
))}
|
))}
|
||||||
|
</LineEditableTabs>
|
||||||
{editMode && tabIds.length < MAX_TAB_COUNT && (
|
|
||||||
<BootstrapTab
|
|
||||||
eventKey={NEW_TAB_INDEX}
|
|
||||||
title={<div className="fa fa-plus" />}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</BootstrapTabs>
|
|
||||||
|
|
||||||
{/* don't indicate that a drop on root is allowed when tabs already exist */}
|
{/* don't indicate that a drop on root is allowed when tabs already exist */}
|
||||||
{tabsDropIndicatorProps &&
|
{tabsDropIndicatorProps &&
|
||||||
parentComponent.id !== DASHBOARD_ROOT_ID && (
|
parentComponent.id !== DASHBOARD_ROOT_ID && (
|
||||||
<div {...tabsDropIndicatorProps} />
|
<div {...tabsDropIndicatorProps} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</StyledTabsContainer>
|
||||||
)}
|
)}
|
||||||
</DragDroppable>
|
</DragDroppable>
|
||||||
);
|
);
|
||||||
|
@ -22,5 +22,4 @@
|
|||||||
@import './header.less';
|
@import './header.less';
|
||||||
@import './new-component.less';
|
@import './new-component.less';
|
||||||
@import './row.less';
|
@import './row.less';
|
||||||
@import './tabs.less';
|
|
||||||
@import './markdown.less';
|
@import './markdown.less';
|
||||||
|
@ -1,106 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
.dashboard-component-tabs {
|
|
||||||
width: 100%;
|
|
||||||
background-color: @lightest;
|
|
||||||
|
|
||||||
& .nav-tabs {
|
|
||||||
border-bottom: none;
|
|
||||||
|
|
||||||
/* by moving padding from <a/> to <li/> we can restrict the selected tab indicator to text width */
|
|
||||||
& > li {
|
|
||||||
margin: 0 16px;
|
|
||||||
|
|
||||||
& > a {
|
|
||||||
color: @almost-black;
|
|
||||||
border: none;
|
|
||||||
padding: 12px 0 14px 0;
|
|
||||||
font-size: @font-size-m;
|
|
||||||
margin-right: 0;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border: none;
|
|
||||||
background: inherit;
|
|
||||||
color: @almost-black;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
outline: none;
|
|
||||||
background: @lightest;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& .dragdroppable-tab[draggable='true'] {
|
|
||||||
cursor: move;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .drop-indicator {
|
|
||||||
top: -12px !important;
|
|
||||||
height: ~'calc(100% + 24px)' !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .drop-indicator--left {
|
|
||||||
left: -12px !important;
|
|
||||||
}
|
|
||||||
& .drop-indicator--right {
|
|
||||||
right: -12px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .drop-indicator--bottom,
|
|
||||||
& .drop-indicator--top {
|
|
||||||
left: -12px !important;
|
|
||||||
width: ~'calc(100% + 24px)' !important; /* escape for .less */
|
|
||||||
opacity: 0.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .fa-plus {
|
|
||||||
color: @gray-dark;
|
|
||||||
font-size: @font-size-m;
|
|
||||||
margin-top: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .editable-title input[type='button'] {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& li.active > a {
|
|
||||||
border: none;
|
|
||||||
|
|
||||||
&:after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
height: 3px;
|
|
||||||
width: 100%;
|
|
||||||
bottom: 0;
|
|
||||||
background: linear-gradient(
|
|
||||||
to right,
|
|
||||||
@indicator-color,
|
|
||||||
shade(@indicator-color, @colorstop-two)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& .dashboard-component-tabs-content {
|
|
||||||
min-height: 48px;
|
|
||||||
margin-top: 1px;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
}
|
|
@ -283,18 +283,12 @@ table.table-no-hover tr:hover {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav.nav-tabs li .anchor-link-container {
|
|
||||||
top: 0;
|
|
||||||
right: -32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard-component.dashboard-component-header .anchor-link-container {
|
.dashboard-component.dashboard-component-header .anchor-link-container {
|
||||||
.fa.fa-link {
|
.fa.fa-link {
|
||||||
font-size: @font-size-l;
|
font-size: @font-size-l;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav.nav-tabs li:hover,
|
|
||||||
.dashboard-component.dashboard-component-header:hover {
|
.dashboard-component.dashboard-component-header:hover {
|
||||||
.anchor-link-container {
|
.anchor-link-container {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
Loading…
Reference in New Issue
Block a user