diff --git a/superset-frontend/cypress-base/cypress/integration/dashboard/controls.test.js b/superset-frontend/cypress-base/cypress/integration/dashboard/controls.test.js
index 795ff64adf4..ee2e40fee8c 100644
--- a/superset-frontend/cypress-base/cypress/integration/dashboard/controls.test.js
+++ b/superset-frontend/cypress-base/cypress/integration/dashboard/controls.test.js
@@ -70,27 +70,24 @@ describe('Dashboard top-level controls', () => {
.find('.world_map')
.should('be.exist');
cy.get(`#slice_${mapId}-controls`).click();
- cy.get(`#slice_${mapId}-controls`)
- .next()
- .find('[data-test="dashboard-slice-refresh-tooltip"]')
- .trigger('click', { force: true });
+ cy.get(`[data-test="slice_${mapId}-menu"]`)
+ .find('[data-test="refresh-dashboard-menu-item"]')
+ .click({ force: true });
// not allow dashboard level force refresh when any chart is loading
cy.get('[data-test="refresh-dashboard-menu-item"]').should(
'have.class',
- 'ant-menu-item-disabled',
+ 'ant-dropdown-menu-item-disabled',
);
// not allow chart level force refresh when it is loading
- cy.get(`#slice_${mapId}-controls`)
- .next()
- .find('[data-test="dashboard-slice-refresh-tooltip"]')
- .parent()
- .should('have.class', 'ant-menu-item-disabled');
+ cy.get(`[data-test="slice_${mapId}-menu"]`)
+ .find('[data-test="refresh-dashboard-menu-item"]')
+ .should('have.class', 'ant-dropdown-menu-item-disabled');
cy.wait(`@postJson_${mapId}_force`);
cy.get('[data-test="refresh-dashboard-menu-item"]').should(
'not.have.class',
- 'ant-menu-item-disabled',
+ 'ant-dropdown-menu-item-disabled',
);
});
@@ -100,15 +97,15 @@ describe('Dashboard top-level controls', () => {
cy.get('[data-test="more-horiz"]').click();
cy.get('[data-test="refresh-dashboard-menu-item"]').should(
'not.have.class',
- 'ant-menu-item-disabled',
+ 'ant-dropdown-menu-item-disabled',
);
// wait the all dash finish loading.
cy.wait(sliceRequests);
- cy.get('[data-test="refresh-dashboard-menu-item"]').click();
+ cy.get('[data-test="refresh-dashboard-menu-item"]').click({ force: true });
cy.get('[data-test="refresh-dashboard-menu-item"]').should(
'have.class',
- 'ant-menu-item-disabled',
+ 'ant-dropdown-menu-item-disabled',
);
// wait all charts force refreshed
@@ -124,7 +121,7 @@ describe('Dashboard top-level controls', () => {
cy.get('[data-test="more-horiz"]').click();
cy.get('[data-test="refresh-dashboard-menu-item"]').should(
'not.have.class',
- 'ant-menu-item-disabled',
+ 'ant-dropdown-menu-item-disabled',
);
});
});
diff --git a/superset-frontend/cypress-base/cypress/integration/dashboard/edit_properties.test.ts b/superset-frontend/cypress-base/cypress/integration/dashboard/edit_properties.test.ts
index b33ad43c458..0fff9126960 100644
--- a/superset-frontend/cypress-base/cypress/integration/dashboard/edit_properties.test.ts
+++ b/superset-frontend/cypress-base/cypress/integration/dashboard/edit_properties.test.ts
@@ -64,7 +64,9 @@ function openAdvancedProperties() {
function openDashboardEditProperties() {
// open dashboard properties edit modal
cy.get('#save-dash-split-button').trigger('click', { force: true });
- cy.get('.dropdown-menu').contains('Edit dashboard properties').click();
+ cy.get('[data-test=header-actions-menu]')
+ .contains('Edit dashboard properties')
+ .click({ force: true });
}
describe('Dashboard edit action', () => {
diff --git a/superset-frontend/cypress-base/cypress/integration/dashboard/markdown.test.ts b/superset-frontend/cypress-base/cypress/integration/dashboard/markdown.test.ts
index 833bdfaf482..612e11837a3 100644
--- a/superset-frontend/cypress-base/cypress/integration/dashboard/markdown.test.ts
+++ b/superset-frontend/cypress-base/cypress/integration/dashboard/markdown.test.ts
@@ -33,6 +33,11 @@ describe('Dashboard edit markdown', () => {
cy.get('[data-test="dashboard-header"]')
.find('[data-test="edit-alt"]')
.click();
+
+ // lazy load - need to open dropdown for the scripts to load
+ cy.get('[data-test="dashboard-header"]')
+ .find('[data-test="more-horiz"]')
+ .click();
cy.get('script').then(nodes => {
// load 5 new script chunks for css editor
expect(nodes.length).to.greaterThan(numScripts);
diff --git a/superset-frontend/spec/javascripts/dashboard/components/HeaderActionsDropdown_spec.jsx b/superset-frontend/spec/javascripts/dashboard/components/HeaderActionsDropdown_spec.jsx
index deb94504524..4d6262981db 100644
--- a/superset-frontend/spec/javascripts/dashboard/components/HeaderActionsDropdown_spec.jsx
+++ b/superset-frontend/spec/javascripts/dashboard/components/HeaderActionsDropdown_spec.jsx
@@ -18,8 +18,7 @@
*/
import React from 'react';
import { shallow } from 'enzyme';
-import { DropdownButton } from 'react-bootstrap';
-import { Menu } from 'src/common/components';
+import { Menu, NoAnimationDropdown } from 'src/common/components';
import RefreshIntervalModal from 'src/dashboard/components/RefreshIntervalModal';
import URLShortLinkModal from 'src/components/URLShortLinkModal';
import HeaderActionsDropdown from 'src/dashboard/components/HeaderActionsDropdown';
@@ -57,40 +56,43 @@ describe('HeaderActionsDropdown', () => {
const wrapper = shallow(
,
);
- return wrapper;
+ const menu = shallow(
+
{wrapper.find(NoAnimationDropdown).props().overlay}
,
+ );
+ return { wrapper, menu };
}
describe('readonly-user', () => {
const overrideProps = { userCanSave: false };
it('should render the DropdownButton', () => {
- const wrapper = setup(overrideProps);
- expect(wrapper.find(DropdownButton)).toExist();
+ const { wrapper } = setup(overrideProps);
+ expect(wrapper.find(NoAnimationDropdown)).toExist();
});
it('should not render the SaveModal', () => {
- const wrapper = setup(overrideProps);
- expect(wrapper.find(SaveModal)).not.toExist();
+ const { menu } = setup(overrideProps);
+ expect(menu.find(SaveModal)).not.toExist();
});
it('should render five Menu items', () => {
- const wrapper = setup(overrideProps);
- expect(wrapper.find(Menu.Item)).toHaveLength(5);
+ const { menu } = setup(overrideProps);
+ expect(menu.find(Menu.Item)).toHaveLength(5);
});
it('should render the RefreshIntervalModal', () => {
- const wrapper = setup(overrideProps);
- expect(wrapper.find(RefreshIntervalModal)).toExist();
+ const { menu } = setup(overrideProps);
+ expect(menu.find(RefreshIntervalModal)).toExist();
});
it('should render the URLShortLinkModal', () => {
- const wrapper = setup(overrideProps);
- expect(wrapper.find(URLShortLinkModal)).toExist();
+ const { menu } = setup(overrideProps);
+ expect(menu.find(URLShortLinkModal)).toExist();
});
it('should not render the CssEditor', () => {
- const wrapper = setup(overrideProps);
- expect(wrapper.find(CssEditor)).not.toExist();
+ const { menu } = setup(overrideProps);
+ expect(menu.find(CssEditor)).not.toExist();
});
});
@@ -98,33 +100,33 @@ describe('HeaderActionsDropdown', () => {
const overrideProps = { userCanSave: true };
it('should render the DropdownButton', () => {
- const wrapper = setup(overrideProps);
- expect(wrapper.find(DropdownButton)).toExist();
+ const { wrapper } = setup(overrideProps);
+ expect(wrapper.find(NoAnimationDropdown)).toExist();
});
it('should render the SaveModal', () => {
- const wrapper = setup(overrideProps);
- expect(wrapper.find(SaveModal)).toExist();
+ const { menu } = setup(overrideProps);
+ expect(menu.find(SaveModal)).toExist();
});
it('should render six Menu items', () => {
- const wrapper = setup(overrideProps);
- expect(wrapper.find(Menu.Item)).toHaveLength(6);
+ const { menu } = setup(overrideProps);
+ expect(menu.find(Menu.Item)).toHaveLength(6);
});
it('should render the RefreshIntervalModal', () => {
- const wrapper = setup(overrideProps);
- expect(wrapper.find(RefreshIntervalModal)).toExist();
+ const { menu } = setup(overrideProps);
+ expect(menu.find(RefreshIntervalModal)).toExist();
});
it('should render the URLShortLinkModal', () => {
- const wrapper = setup(overrideProps);
- expect(wrapper.find(URLShortLinkModal)).toExist();
+ const { menu } = setup(overrideProps);
+ expect(menu.find(URLShortLinkModal)).toExist();
});
it('should not render the CssEditor', () => {
- const wrapper = setup(overrideProps);
- expect(wrapper.find(CssEditor)).not.toExist();
+ const { menu } = setup(overrideProps);
+ expect(menu.find(CssEditor)).not.toExist();
});
});
@@ -132,33 +134,33 @@ describe('HeaderActionsDropdown', () => {
const overrideProps = { userCanSave: true, editMode: true };
it('should render the DropdownButton', () => {
- const wrapper = setup(overrideProps);
- expect(wrapper.find(DropdownButton)).toExist();
+ const { wrapper } = setup(overrideProps);
+ expect(wrapper.find(NoAnimationDropdown)).toExist();
});
it('should render the SaveModal', () => {
- const wrapper = setup(overrideProps);
- expect(wrapper.find(SaveModal)).toExist();
+ const { menu } = setup(overrideProps);
+ expect(menu.find(SaveModal)).toExist();
});
it('should render seven MenuItems', () => {
- const wrapper = setup(overrideProps);
- expect(wrapper.find(Menu.Item)).toHaveLength(7);
+ const { menu } = setup(overrideProps);
+ expect(menu.find(Menu.Item)).toHaveLength(7);
});
it('should render the RefreshIntervalModal', () => {
- const wrapper = setup(overrideProps);
- expect(wrapper.find(RefreshIntervalModal)).toExist();
+ const { menu } = setup(overrideProps);
+ expect(menu.find(RefreshIntervalModal)).toExist();
});
it('should render the URLShortLinkModal', () => {
- const wrapper = setup(overrideProps);
- expect(wrapper.find(URLShortLinkModal)).toExist();
+ const { menu } = setup(overrideProps);
+ expect(menu.find(URLShortLinkModal)).toExist();
});
it('should render the CssEditor', () => {
- const wrapper = setup(overrideProps);
- expect(wrapper.find(CssEditor)).toExist();
+ const { menu } = setup(overrideProps);
+ expect(menu.find(CssEditor)).toExist();
});
});
});
diff --git a/superset-frontend/src/common/components/index.tsx b/superset-frontend/src/common/components/index.tsx
index 99fafb3dd38..f8f611173a5 100644
--- a/superset-frontend/src/common/components/index.tsx
+++ b/superset-frontend/src/common/components/index.tsx
@@ -16,9 +16,11 @@
* specific language governing permissions and limitations
* under the License.
*/
+import React from 'react';
import { styled } from '@superset-ui/core';
// eslint-disable-next-line no-restricted-imports
-import { Skeleton, Menu as AntdMenu } from 'antd';
+import { Dropdown, Skeleton, Menu as AntdMenu } from 'antd';
+import { DropDownProps } from 'antd/lib/dropdown';
/*
Antd is re-exported from here so we can override components with Emotion as needed.
@@ -57,6 +59,13 @@ export const Menu = Object.assign(AntdMenu, {
Item: MenuItem,
});
+export const NoAnimationDropdown = (props: DropDownProps) => (
+
+);
+
export const ThinSkeleton = styled(Skeleton)`
h3 {
margin: ${({ theme }) => theme.gridUnit}px 0;
diff --git a/superset-frontend/src/components/ModalTrigger.jsx b/superset-frontend/src/components/ModalTrigger.jsx
index ddcc5744ef8..69f26d42c6e 100644
--- a/superset-frontend/src/components/ModalTrigger.jsx
+++ b/superset-frontend/src/components/ModalTrigger.jsx
@@ -102,9 +102,9 @@ export default class ModalTrigger extends React.Component {
/* eslint-disable jsx-a11y/interactive-supports-focus */
return (
<>
-
+
{this.props.triggerNode}
-
+
{this.renderModal()}
>
);
diff --git a/superset-frontend/src/dashboard/components/HeaderActionsDropdown.jsx b/superset-frontend/src/dashboard/components/HeaderActionsDropdown.jsx
index ede085f4d92..ac592afce9f 100644
--- a/superset-frontend/src/dashboard/components/HeaderActionsDropdown.jsx
+++ b/superset-frontend/src/dashboard/components/HeaderActionsDropdown.jsx
@@ -19,10 +19,9 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { SupersetClient, t } from '@superset-ui/core';
-import { DropdownButton } from 'react-bootstrap';
+import { styled, SupersetClient, t } from '@superset-ui/core';
-import { Menu } from 'src/common/components';
+import { Menu, NoAnimationDropdown } from 'src/common/components';
import Icon from 'src/components/Icon';
import CssEditor from './CssEditor';
@@ -84,6 +83,10 @@ const MENU_KEYS = {
TOGGLE_FULLSCREEN: 'toggle-fullscreen',
};
+const DropdownButton = styled.div`
+ margin-left: ${({ theme }) => theme.gridUnit * 2.5}px;
+`;
+
class HeaderActionsDropdown extends React.PureComponent {
static discardChanges() {
window.location.reload();
@@ -188,112 +191,114 @@ class HeaderActionsDropdown extends React.PureComponent {
const emailSubject = `${emailTitle} ${dashboardTitle}`;
const emailBody = t('Check out this dashboard: ');
- return (
- }
- noCaret
- id="save-dash-split-button"
- bsSize="large"
- style={{ border: 'none', padding: 0, marginLeft: '4px' }}
- pullRight
+ const menu = (
+
- }
- canOverwrite={userCanEdit}
- />
-
- )}
-
-
+ {t('Share dashboard')}}
- />
-
-
- {t('Refresh dashboard')}
-
-
-
- {t('Set auto-refresh interval')}}
+ shouldPersistRefreshFrequency={shouldPersistRefreshFrequency}
+ lastModifiedTime={lastModifiedTime}
+ customCss={customCss}
+ colorNamespace={colorNamespace}
+ colorScheme={colorScheme}
+ onSave={onSave}
+ triggerNode={
+ {t('Save as')}
+ }
+ canOverwrite={userCanEdit}
/>
+ )}
+
+ {t('Share dashboard')}}
+ />
+
+
+ {t('Refresh dashboard')}
+
+
+
+ {t('Set auto-refresh interval')}}
+ />
+
- {editMode && (
-
-
-
- )}
+ {editMode && (
+
+
+
+ )}
- {editMode && (
-
- {t('Edit dashboard properties')}
-
- )}
+ {editMode && (
+
+ {t('Edit dashboard properties')}
+
+ )}
- {editMode && (
-
- {t('Edit CSS')}}
- initialCss={this.state.css}
- templates={this.state.cssTemplates}
- onChange={this.changeCss}
- />
-
- )}
+ {editMode && (
+
+ {t('Edit CSS')}}
+ initialCss={this.state.css}
+ templates={this.state.cssTemplates}
+ onChange={this.changeCss}
+ />
+
+ )}
- {!editMode && (
-
- {t('Download as image')}
-
- )}
+ {!editMode && (
+
+ {t('Download as image')}
+
+ )}
- {!editMode && (
-
- {t('Toggle FullScreen')}
-
- )}
-
-
+ {!editMode && (
+
+ {t('Toggle FullScreen')}
+
+ )}
+
+ );
+ return (
+
+
+
+
+
);
}
}
diff --git a/superset-frontend/src/dashboard/components/SliceHeaderControls.jsx b/superset-frontend/src/dashboard/components/SliceHeaderControls.jsx
index a453daa004b..c1a8b9c25e2 100644
--- a/superset-frontend/src/dashboard/components/SliceHeaderControls.jsx
+++ b/superset-frontend/src/dashboard/components/SliceHeaderControls.jsx
@@ -19,9 +19,8 @@
import React from 'react';
import PropTypes from 'prop-types';
import moment from 'moment';
-import { DropdownButton } from 'react-bootstrap';
import { styled, t } from '@superset-ui/core';
-import { Menu } from 'src/common/components';
+import { Menu, NoAnimationDropdown } from 'src/common/components';
import URLShortLinkModal from '../../components/URLShortLinkModal';
import downloadAsImage from '../../utils/downloadAsImage';
import getDashboardUrl from '../util/getDashboardUrl';
@@ -82,6 +81,12 @@ const VerticalDotsContainer = styled.div`
}
`;
+const RefreshTooltip = styled.div`
+ height: ${({ theme }) => theme.gridUnit * 4}px;
+ margin: ${({ theme }) => theme.gridUnit}px 0;
+ color: ${({ theme }) => theme.colors.grayscale.base};
+`;
+
const VerticalDotsTrigger = () => (
@@ -161,70 +166,77 @@ class SliceHeaderControls extends React.PureComponent {
? t('Cached %s', cachedWhen)
: (updatedWhen && t('Fetched %s', updatedWhen)) || '';
const resizeLabel = isFullSize ? t('Minimize') : t('Maximize');
- return (
- }
- style={{ padding: 0 }}
- // react-bootstrap handles visibility, but call toggle to force a re-render
- // and update the fetched/cached timestamps
- onToggle={this.toggleControls}
+
+ const menu = (
+
-
-
- {t('Force refresh')}
-
- {refreshTooltip}
-
+
+ {t('Force refresh')}
+
+ {refreshTooltip}
+
+
+
+
+
+ {slice.description && (
+
+ {t('Toggle chart description')}
+ )}
-
-
- {slice.description && (
-
- {t('Toggle chart description')}
-
- )}
-
- {this.props.supersetCanExplore && (
-
- {t('Explore chart')}
-
- )}
-
- {this.props.supersetCanCSV && (
- {t('Export CSV')}
- )}
-
- {resizeLabel}
-
-
- {t('Share chart')}}
- />
+ {this.props.supersetCanExplore && (
+
+ {t('Explore chart')}
+ )}
-
- {t('Download as image')}
-
-
-
+ {this.props.supersetCanCSV && (
+ {t('Export CSV')}
+ )}
+
+ {resizeLabel}
+
+
+ {t('Share chart')}}
+ />
+
+
+
+ {t('Download as image')}
+
+
+ );
+
+ return (
+
+
+
+
+
);
}
}
diff --git a/superset-frontend/src/dashboard/stylesheets/dashboard.less b/superset-frontend/src/dashboard/stylesheets/dashboard.less
index 1b51069dbf2..ac5774dacf2 100644
--- a/superset-frontend/src/dashboard/stylesheets/dashboard.less
+++ b/superset-frontend/src/dashboard/stylesheets/dashboard.less
@@ -109,29 +109,6 @@ body {
}
}
-.dashboard .dashboard-header {
- #save-dash-split-button {
- border-radius: 0;
- margin-left: -9px;
- height: 30px;
- width: 30px;
-
- &.btn.btn-primary {
- border-left-color: @lightest;
- }
-
- & + .dropdown-menu.dropdown-menu-right {
- min-width: unset;
- }
-
- .caret {
- display: inline-block;
- width: 100%;
- height: 100%;
- }
- }
-}
-
.dashboard .chart-header,
.dashboard .dashboard-header {
.dropdown-menu {