mirror of https://github.com/apache/superset.git
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"]')
|
||||
.first()
|
||||
.find('[data-test="nav-list"]')
|
||||
.children()
|
||||
.find('[data-test="nav-list"] .ant-tabs-nav-list > .ant-tabs-tab')
|
||||
.as('top-level-tabs');
|
||||
|
||||
cy.get('@top-level-tabs').first().click().should('have.class', 'active');
|
||||
cy.get('@top-level-tabs').last().should('not.have.class', 'active');
|
||||
cy.get('@top-level-tabs')
|
||||
.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').first().should('not.have.class', 'active');
|
||||
cy.get('@top-level-tabs')
|
||||
.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', () => {
|
||||
|
@ -128,8 +137,7 @@ describe('Dashboard tabs', () => {
|
|||
// click row level tab, see 1 more chart
|
||||
cy.get('[data-test="dashboard-component-tabs"]')
|
||||
.last()
|
||||
.find('[data-test="nav-list"]')
|
||||
.children()
|
||||
.find('[data-test="nav-list"] .ant-tabs-nav-list > .ant-tabs-tab')
|
||||
.as('row-level-tabs');
|
||||
|
||||
cy.get('@row-level-tabs').last().click();
|
||||
|
@ -141,8 +149,7 @@ describe('Dashboard tabs', () => {
|
|||
handleException();
|
||||
cy.get('[data-test="dashboard-component-tabs"]')
|
||||
.first()
|
||||
.find('[data-test="nav-list"]')
|
||||
.children()
|
||||
.find('[data-test="nav-list"] .ant-tabs-nav-list > .ant-tabs-tab')
|
||||
.as('top-level-tabs');
|
||||
|
||||
cy.get('@top-level-tabs').last().click();
|
||||
|
|
|
@ -179,7 +179,7 @@ describe('DashboardBuilder', () => {
|
|||
expect(wrapper.find(TabContainer).prop('activeKey')).toBe(0);
|
||||
|
||||
wrapper
|
||||
.find('.dashboard-component-tabs .nav-tabs a')
|
||||
.find('.dashboard-component-tabs .ant-tabs .ant-tabs-tab')
|
||||
.at(1)
|
||||
.simulate('click');
|
||||
|
||||
|
|
|
@ -22,10 +22,8 @@ import { styledMount as mount } from 'spec/helpers/theming';
|
|||
import sinon from 'sinon';
|
||||
|
||||
import DashboardComponent from 'src/dashboard/containers/DashboardComponent';
|
||||
import DeleteComponentModal from 'src/dashboard/components/DeleteComponentModal';
|
||||
import DragDroppable from 'src/dashboard/components/dnd/DragDroppable';
|
||||
import EditableTitle from 'src/components/EditableTitle';
|
||||
import WithPopoverMenu from 'src/dashboard/components/menu/WithPopoverMenu';
|
||||
import Tab, {
|
||||
RENDER_TAB,
|
||||
RENDER_TAB_CONTENT,
|
||||
|
@ -96,42 +94,6 @@ describe('Tabs', () => {
|
|||
'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', () => {
|
||||
|
|
|
@ -18,11 +18,12 @@
|
|||
*/
|
||||
import { Provider } from 'react-redux';
|
||||
import React from 'react';
|
||||
import { mount, shallow } from 'enzyme';
|
||||
import { shallow } from 'enzyme';
|
||||
import sinon from 'sinon';
|
||||
import { Tabs as BootstrapTabs, Tab as BootstrapTab } from 'react-bootstrap';
|
||||
import { supersetTheme, ThemeProvider } from '@superset-ui/core';
|
||||
import { LineEditableTabs } from 'src/common/components/Tabs';
|
||||
import { Modal } from 'src/common/components';
|
||||
|
||||
import { styledMount as mount } from 'spec/helpers/theming';
|
||||
import DashboardComponent from 'src/dashboard/containers/DashboardComponent';
|
||||
import DeleteComponentButton from 'src/dashboard/components/DeleteComponentButton';
|
||||
import HoverMenu from 'src/dashboard/components/menu/HoverMenu';
|
||||
|
@ -54,6 +55,7 @@ describe('Tabs', () => {
|
|||
deleteComponent() {},
|
||||
updateComponents() {},
|
||||
logEvent() {},
|
||||
setMountedTab() {},
|
||||
};
|
||||
|
||||
function setup(overrideProps) {
|
||||
|
@ -65,10 +67,6 @@ describe('Tabs', () => {
|
|||
<Tabs {...props} {...overrideProps} />
|
||||
</WithDragDropContext>
|
||||
</Provider>,
|
||||
{
|
||||
wrappingComponent: ThemeProvider,
|
||||
wrappingComponentProps: { theme: supersetTheme },
|
||||
},
|
||||
);
|
||||
return wrapper;
|
||||
}
|
||||
|
@ -79,31 +77,23 @@ describe('Tabs', () => {
|
|||
expect(wrapper.find(DragDroppable)).toExist();
|
||||
});
|
||||
|
||||
it('should render BootstrapTabs', () => {
|
||||
it('should render non-editable tabs', () => {
|
||||
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 tabProps = wrapper.find(BootstrapTabs).props();
|
||||
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(
|
||||
expect(wrapper.find(LineEditableTabs.TabPane)).toHaveLength(
|
||||
props.component.children.length,
|
||||
);
|
||||
});
|
||||
|
||||
it('should render an extra (+) BootstrapTab in editMode', () => {
|
||||
it('should render editable tabs in editMode', () => {
|
||||
const wrapper = setup({ editMode: true });
|
||||
expect(wrapper.find(BootstrapTab)).toHaveLength(
|
||||
props.component.children.length + 1,
|
||||
);
|
||||
expect(wrapper.find(LineEditableTabs)).toExist();
|
||||
expect(wrapper.find('.ant-tabs-nav-add')).toExist();
|
||||
});
|
||||
|
||||
it('should render a DashboardComponent for each child', () => {
|
||||
|
@ -118,7 +108,7 @@ describe('Tabs', () => {
|
|||
const createComponent = sinon.spy();
|
||||
const wrapper = setup({ editMode: true, createComponent });
|
||||
wrapper
|
||||
.find('.dashboard-component-tabs .nav-tabs a')
|
||||
.find('[data-test="dashboard-component-tabs"] .ant-tabs-nav-add')
|
||||
.last()
|
||||
.simulate('click');
|
||||
|
||||
|
@ -129,7 +119,7 @@ describe('Tabs', () => {
|
|||
const onChangeTab = sinon.spy();
|
||||
const wrapper = setup({ editMode: true, onChangeTab });
|
||||
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
|
||||
.simulate('click');
|
||||
|
||||
|
@ -140,7 +130,9 @@ describe('Tabs', () => {
|
|||
const onChangeTab = sinon.spy();
|
||||
const wrapper = setup({ editMode: true, onChangeTab });
|
||||
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
|
||||
.simulate('click');
|
||||
|
||||
|
@ -186,4 +178,13 @@ describe('Tabs', () => {
|
|||
wrapper = shallow(<Tabs {...directLinkProps} />);
|
||||
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 {
|
||||
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 }) =>
|
||||
|
@ -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,
|
||||
});
|
||||
|
||||
CardTabs.defaultProps = {
|
||||
type: 'card',
|
||||
};
|
||||
|
||||
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)
|
||||
.then(this.onShortUrlSuccess)
|
||||
.catch(this.props.addDangerToast);
|
||||
|
|
|
@ -101,7 +101,7 @@ class DashboardBuilder extends React.Component {
|
|||
static shouldFocusTabs(event, container) {
|
||||
// don't focus the tabs when we click on a tab
|
||||
return (
|
||||
event.target.tagName === 'UL' ||
|
||||
event.target.className === 'ant-tabs-nav-wrap' ||
|
||||
(/icon-button/.test(event.target.className) &&
|
||||
container.contains(event.target))
|
||||
);
|
||||
|
|
|
@ -23,8 +23,6 @@ import DashboardComponent from '../../containers/DashboardComponent';
|
|||
import DragDroppable from '../dnd/DragDroppable';
|
||||
import EditableTitle from '../../../components/EditableTitle';
|
||||
import AnchorLink from '../../../components/AnchorLink';
|
||||
import DeleteComponentModal from '../DeleteComponentModal';
|
||||
import WithPopoverMenu from '../menu/WithPopoverMenu';
|
||||
import { componentShape } from '../../util/propShapes';
|
||||
|
||||
export const RENDER_TAB = 'RENDER_TAB';
|
||||
|
@ -39,7 +37,6 @@ const propTypes = {
|
|||
depth: PropTypes.number.isRequired,
|
||||
renderType: PropTypes.oneOf([RENDER_TAB, RENDER_TAB_CONTENT]).isRequired,
|
||||
onDropOnTab: PropTypes.func,
|
||||
onDeleteTab: PropTypes.func,
|
||||
editMode: PropTypes.bool.isRequired,
|
||||
filters: PropTypes.object.isRequired,
|
||||
|
||||
|
@ -52,7 +49,6 @@ const propTypes = {
|
|||
|
||||
// redux
|
||||
handleComponentDrop: PropTypes.func.isRequired,
|
||||
deleteComponent: PropTypes.func.isRequired,
|
||||
updateComponents: PropTypes.func.isRequired,
|
||||
setDirectPathToChild: PropTypes.func.isRequired,
|
||||
};
|
||||
|
@ -61,7 +57,6 @@ const defaultProps = {
|
|||
availableColumnCount: 0,
|
||||
columnWidth: 0,
|
||||
onDropOnTab() {},
|
||||
onDeleteTab() {},
|
||||
onResizeStart() {},
|
||||
onResize() {},
|
||||
onResizeStop() {},
|
||||
|
@ -70,21 +65,12 @@ const defaultProps = {
|
|||
export default class Tab extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isFocused: false,
|
||||
};
|
||||
this.handleChangeFocus = this.handleChangeFocus.bind(this);
|
||||
this.handleChangeText = this.handleChangeText.bind(this);
|
||||
this.handleDeleteComponent = this.handleDeleteComponent.bind(this);
|
||||
this.handleDrop = this.handleDrop.bind(this);
|
||||
this.handleTopDropTargetDrop = this.handleTopDropTargetDrop.bind(this);
|
||||
this.handleChangeTab = this.handleChangeTab.bind(this);
|
||||
}
|
||||
|
||||
handleChangeFocus(nextFocus) {
|
||||
this.setState(() => ({ isFocused: nextFocus }));
|
||||
}
|
||||
|
||||
handleChangeTab({ 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) {
|
||||
this.props.handleComponentDrop(dropResult);
|
||||
this.props.onDropOnTab(dropResult);
|
||||
|
@ -204,7 +184,6 @@ export default class Tab extends React.PureComponent {
|
|||
}
|
||||
|
||||
renderTab() {
|
||||
const { isFocused } = this.state;
|
||||
const {
|
||||
component,
|
||||
parentComponent,
|
||||
|
@ -212,12 +191,8 @@ export default class Tab extends React.PureComponent {
|
|||
depth,
|
||||
editMode,
|
||||
filters,
|
||||
isFocused,
|
||||
} = this.props;
|
||||
const deleteTabIcon = (
|
||||
<div className="icon-button">
|
||||
<span className="fa fa-trash" />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<DragDroppable
|
||||
|
@ -229,22 +204,9 @@ export default class Tab extends React.PureComponent {
|
|||
onDrop={this.handleDrop}
|
||||
editMode={editMode}
|
||||
>
|
||||
{({ dropIndicatorProps, dragSourceRef }) => (
|
||||
<div className="dragdroppable-tab" ref={dragSourceRef}>
|
||||
<WithPopoverMenu
|
||||
onChangeFocus={this.handleChangeFocus}
|
||||
menuItems={
|
||||
parentComponent.children.length <= 1
|
||||
? []
|
||||
: [
|
||||
<DeleteComponentModal
|
||||
triggerNode={deleteTabIcon}
|
||||
onDelete={this.handleDeleteComponent}
|
||||
/>,
|
||||
]
|
||||
}
|
||||
editMode={editMode}
|
||||
>
|
||||
{({ dropIndicatorProps, dragSourceRef }) => {
|
||||
return (
|
||||
<div className="dragdroppable-tab" ref={dragSourceRef}>
|
||||
<EditableTitle
|
||||
title={component.meta.text}
|
||||
canEdit={editMode && isFocused}
|
||||
|
@ -259,11 +221,11 @@ export default class Tab extends React.PureComponent {
|
|||
placement={index >= 5 ? 'left' : 'right'}
|
||||
/>
|
||||
)}
|
||||
</WithPopoverMenu>
|
||||
|
||||
{dropIndicatorProps && <div {...dropIndicatorProps} />}
|
||||
</div>
|
||||
)}
|
||||
{dropIndicatorProps && <div {...dropIndicatorProps} />}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</DragDroppable>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -18,8 +18,10 @@
|
|||
*/
|
||||
import React from 'react';
|
||||
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 DragHandle from '../dnd/DragHandle';
|
||||
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 { RENDER_TAB, RENDER_TAB_CONTENT } from './Tab';
|
||||
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 propTypes = {
|
||||
|
@ -79,6 +79,38 @@ const defaultProps = {
|
|||
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 {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
@ -127,19 +159,32 @@ class Tabs extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
handleClickTab(tabIndex, ev) {
|
||||
if (ev) {
|
||||
const { target } = ev;
|
||||
// special handler for clicking on anchor link icon (or whitespace nearby):
|
||||
// will show short link popover but do not change tab
|
||||
if (target && target.classList.contains('short-link-trigger')) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
showDeleteConfirmModal = key => {
|
||||
const { component, deleteComponent } = this.props;
|
||||
Modal.confirm({
|
||||
title: t('Delete dashboard tab?'),
|
||||
content: (
|
||||
<span>
|
||||
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;
|
||||
|
||||
if (tabIndex === NEW_TAB_INDEX) {
|
||||
if (action === 'add') {
|
||||
createComponent({
|
||||
destination: {
|
||||
id: component.id,
|
||||
|
@ -151,7 +196,15 @@ class Tabs extends React.PureComponent {
|
|||
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 targetTabId = pathToTabIndex[pathToTabIndex.length - 1];
|
||||
this.props.logEvent(LOG_ACTIONS_SELECT_DASHBOARD_TAB, {
|
||||
|
@ -161,6 +214,11 @@ class Tabs extends React.PureComponent {
|
|||
|
||||
this.props.onChangeTab({ pathToTabIndex });
|
||||
}
|
||||
if (renderTabContent) {
|
||||
const tabIds = component.children;
|
||||
const activeKey = tabIds[this.state.tabIndex];
|
||||
this.props.setMountedTab(activeKey);
|
||||
}
|
||||
}
|
||||
|
||||
handleDeleteComponent() {
|
||||
|
@ -212,6 +270,8 @@ class Tabs extends React.PureComponent {
|
|||
const { tabIndex: selectedTabIndex } = this.state;
|
||||
const { children: tabIds } = tabsComponent;
|
||||
|
||||
const activeKey = tabIds[selectedTabIndex];
|
||||
|
||||
return (
|
||||
<DragDroppable
|
||||
component={tabsComponent}
|
||||
|
@ -226,7 +286,7 @@ class Tabs extends React.PureComponent {
|
|||
dropIndicatorProps: tabsDropIndicatorProps,
|
||||
dragSourceRef: tabsDragSourceRef,
|
||||
}) => (
|
||||
<div
|
||||
<StyledTabsContainer
|
||||
className="dashboard-component dashboard-component-tabs"
|
||||
data-test="dashboard-component-tabs"
|
||||
>
|
||||
|
@ -237,23 +297,21 @@ class Tabs extends React.PureComponent {
|
|||
</HoverMenu>
|
||||
)}
|
||||
|
||||
<BootstrapTabs
|
||||
<LineEditableTabs
|
||||
id={tabsComponent.id}
|
||||
activeKey={selectedTabIndex}
|
||||
onSelect={this.handleClickTab}
|
||||
animation
|
||||
mountOnEnter
|
||||
unmountOnExit={false}
|
||||
activeKey={activeKey}
|
||||
onChange={key => {
|
||||
this.handleClickTab(tabIds.indexOf(key));
|
||||
}}
|
||||
onEdit={this.handleEdit}
|
||||
hideAdd={tabIds.length >= MAX_TAB_COUNT}
|
||||
data-test="nav-list"
|
||||
type={editMode ? 'editable-card' : 'card'}
|
||||
>
|
||||
{tabIds.map((tabId, tabIndex) => (
|
||||
// react-bootstrap doesn't render a Tab if we move this to its own Tab.jsx so we
|
||||
// use `renderType` to indicate what the DashboardComponent should render. This
|
||||
// prevents us from passing the entire dashboard component lookup to render Tabs.jsx
|
||||
<BootstrapTab
|
||||
<LineEditableTabs.TabPane
|
||||
key={tabId}
|
||||
eventKey={tabIndex}
|
||||
title={
|
||||
tab={
|
||||
<DashboardComponent
|
||||
id={tabId}
|
||||
parentId={tabsComponent.id}
|
||||
|
@ -263,15 +321,9 @@ class Tabs extends React.PureComponent {
|
|||
availableColumnCount={availableColumnCount}
|
||||
columnWidth={columnWidth}
|
||||
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 && (
|
||||
<DashboardComponent
|
||||
|
@ -291,23 +343,16 @@ class Tabs extends React.PureComponent {
|
|||
}
|
||||
/>
|
||||
)}
|
||||
</BootstrapTab>
|
||||
</LineEditableTabs.TabPane>
|
||||
))}
|
||||
|
||||
{editMode && tabIds.length < MAX_TAB_COUNT && (
|
||||
<BootstrapTab
|
||||
eventKey={NEW_TAB_INDEX}
|
||||
title={<div className="fa fa-plus" />}
|
||||
/>
|
||||
)}
|
||||
</BootstrapTabs>
|
||||
</LineEditableTabs>
|
||||
|
||||
{/* don't indicate that a drop on root is allowed when tabs already exist */}
|
||||
{tabsDropIndicatorProps &&
|
||||
parentComponent.id !== DASHBOARD_ROOT_ID && (
|
||||
<div {...tabsDropIndicatorProps} />
|
||||
)}
|
||||
</div>
|
||||
</StyledTabsContainer>
|
||||
)}
|
||||
</DragDroppable>
|
||||
);
|
||||
|
|
|
@ -22,5 +22,4 @@
|
|||
@import './header.less';
|
||||
@import './new-component.less';
|
||||
@import './row.less';
|
||||
@import './tabs.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 {
|
||||
.fa.fa-link {
|
||||
font-size: @font-size-l;
|
||||
}
|
||||
}
|
||||
|
||||
.nav.nav-tabs li:hover,
|
||||
.dashboard-component.dashboard-component-header:hover {
|
||||
.anchor-link-container {
|
||||
cursor: pointer;
|
||||
|
|
Loading…
Reference in New Issue