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:
Kamil Gabryjelski 2020-11-02 22:31:55 +01:00 committed by GitHub
parent 6c6ded139b
commit a874b14a8a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 194 additions and 289 deletions

View File

@ -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();

View File

@ -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');

View File

@ -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', () => {

View File

@ -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);
});
});

View File

@ -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 };

View File

@ -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);

View File

@ -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))
);

View File

@ -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>
);
}

View File

@ -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>
);

View File

@ -22,5 +22,4 @@
@import './header.less';
@import './new-component.less';
@import './row.less';
@import './tabs.less';
@import './markdown.less';

View File

@ -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;
}
}

View File

@ -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;