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 = ( + - - {userCanSave && ( - - {t('Save as')} - } - 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 {