From 3a4176a8d5a862355d62b8a07be20d2a3a124302 Mon Sep 17 00:00:00 2001 From: Geido <60598000+geido@users.noreply.github.com> Date: Wed, 25 May 2022 12:56:19 +0200 Subject: [PATCH] chore: Implement global header in Dashboard (#20146) * Add gloal header * Reimplement report dropdown * Update unit tests * Clean up * Clean up * Remove unused import * Update Cypress * Update Cypress save dashboard test * Fix spacing --- .../dashboard/dashboard.applitools.test.ts | 4 +- .../integration/dashboard/edit_mode.test.js | 8 +- .../dashboard/edit_properties.test.ts | 15 +- .../integration/dashboard/markdown.test.ts | 8 +- .../integration/dashboard/save.test.js | 24 +- .../visualizations/download_chart.test.js | 2 +- .../cypress/support/directories.ts | 5 +- .../src/assets/images/icons/redo.svg | 21 + .../src/assets/images/icons/undo.svg | 21 + .../components/DynamicEditableTitle/index.tsx | 5 + .../src/components/Icons/index.tsx | 2 + .../PageHeaderWithActions/index.tsx | 18 +- .../HeaderReportDropdown/index.tsx | 8 +- .../components/Header/Header.test.tsx | 35 +- .../HeaderActionsDropdown.test.tsx | 81 ++-- .../Header/HeaderActionsDropdown/index.jsx | 218 +++++---- .../src/dashboard/components/Header/index.jsx | 440 ++++++++++-------- .../components/RefreshIntervalModal.test.tsx | 7 +- .../components/menu/ShareMenuItems/index.tsx | 4 +- .../stylesheets/components/header.less | 5 - .../src/dashboard/stylesheets/dashboard.less | 21 - .../components/ExploreViewContainer/index.jsx | 54 +-- 22 files changed, 560 insertions(+), 446 deletions(-) create mode 100644 superset-frontend/src/assets/images/icons/redo.svg create mode 100644 superset-frontend/src/assets/images/icons/undo.svg diff --git a/superset-frontend/cypress-base/cypress/integration/dashboard/dashboard.applitools.test.ts b/superset-frontend/cypress-base/cypress/integration/dashboard/dashboard.applitools.test.ts index 7a61087c1a8b3..d492175a5e3a6 100644 --- a/superset-frontend/cypress-base/cypress/integration/dashboard/dashboard.applitools.test.ts +++ b/superset-frontend/cypress-base/cypress/integration/dashboard/dashboard.applitools.test.ts @@ -41,8 +41,8 @@ describe('Dashboard load', () => { }); it('should load the Dashboard in edit mode', () => { - cy.get('[data-test="dashboard-header"]') - .find('[aria-label=edit-alt]') + cy.get('.header-with-actions') + .find('[aria-label="Edit dashboard"]') .click(); // wait for a chart to appear cy.get('[data-test="grid-container"]').find('.box_plot', { diff --git a/superset-frontend/cypress-base/cypress/integration/dashboard/edit_mode.test.js b/superset-frontend/cypress-base/cypress/integration/dashboard/edit_mode.test.js index d799dccd3bb6e..7a3b82705cebd 100644 --- a/superset-frontend/cypress-base/cypress/integration/dashboard/edit_mode.test.js +++ b/superset-frontend/cypress-base/cypress/integration/dashboard/edit_mode.test.js @@ -22,8 +22,8 @@ describe('Dashboard edit mode', () => { beforeEach(() => { cy.login(); cy.visit(WORLD_HEALTH_DASHBOARD); - cy.get('[data-test="dashboard-header"]') - .find('[aria-label=edit-alt]') + cy.get('.header-with-actions') + .find('[aria-label="Edit dashboard"]') .click(); }); @@ -94,9 +94,9 @@ describe('Dashboard edit mode', () => { .find('[data-test="discard-changes-button"]') .should('be.visible') .click(); - cy.get('[data-test="dashboard-header"]').within(() => { + cy.get('.header-with-actions').within(() => { cy.get('[data-test="dashboard-edit-actions"]').should('not.be.visible'); - cy.get('[aria-label="edit-alt"]').should('be.visible'); + cy.get('[aria-label="Edit dashboard"]').should('be.visible'); }); }); }); 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 bf4bd319c1ddd..b3061cdb7d40a 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 @@ -66,9 +66,13 @@ function openAdvancedProperties() { function openDashboardEditProperties() { // open dashboard properties edit modal - cy.get('#save-dash-split-button').trigger('click', { force: true }); + cy.get( + '.header-with-actions .right-button-panel .ant-dropdown-trigger', + ).trigger('click', { + force: true, + }); cy.get('[data-test=header-actions-menu]') - .contains('Edit dashboard properties') + .contains('Edit properties') .click({ force: true }); } @@ -80,7 +84,7 @@ describe('Dashboard edit action', () => { cy.get('.dashboard-grid', { timeout: 50000 }) .should('be.visible') // wait for 50 secs to load dashboard .then(() => { - cy.get('.dashboard-header [aria-label=edit-alt]') + cy.get('.header-with-actions [aria-label="Edit dashboard"]') .should('be.visible') .click(); openDashboardEditProperties(); @@ -106,7 +110,10 @@ describe('Dashboard edit action', () => { cy.get('.ant-modal-body').should('not.exist'); // assert title has been updated - cy.get('.editable-title input').should('have.value', dashboardTitle); + cy.get('[data-test="editable-title-input"]').should( + 'have.value', + dashboardTitle, + ); }); }); describe('the color picker is changed', () => { 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 7ce391868809b..2964241b184a2 100644 --- a/superset-frontend/cypress-base/cypress/integration/dashboard/markdown.test.ts +++ b/superset-frontend/cypress-base/cypress/integration/dashboard/markdown.test.ts @@ -25,14 +25,12 @@ describe('Dashboard edit markdown', () => { }); it('should add markdown component to dashboard', () => { - cy.get('[data-test="dashboard-header"]') - .find('[aria-label="edit-alt"]') + cy.get('.header-with-actions') + .find('[aria-label="Edit dashboard"]') .click(); // lazy load - need to open dropdown for the scripts to load - cy.get('[data-test="dashboard-header"]') - .find('[aria-label="more-horiz"]') - .click(); + cy.get('.header-with-actions').find('[aria-label="more-horiz"]').click(); cy.get('[data-test="grid-row-background--transparent"]') .first() .as('component-background-first'); diff --git a/superset-frontend/cypress-base/cypress/integration/dashboard/save.test.js b/superset-frontend/cypress-base/cypress/integration/dashboard/save.test.js index 8064f81fa14da..b0e9d1141cd30 100644 --- a/superset-frontend/cypress-base/cypress/integration/dashboard/save.test.js +++ b/superset-frontend/cypress-base/cypress/integration/dashboard/save.test.js @@ -25,9 +25,11 @@ import { } from './dashboard.helper'; function openDashboardEditProperties() { - cy.get('.dashboard-header [aria-label=edit-alt]').click(); - cy.get('#save-dash-split-button').trigger('click', { force: true }); - cy.get('.dropdown-menu').contains('Edit dashboard properties').click(); + cy.get('.header-with-actions [aria-label="Edit dashboard"]').click(); + cy.get( + '.header-with-actions .right-button-panel .ant-dropdown-trigger', + ).trigger('click', { force: true }); + cy.get('.dropdown-menu').contains('Edit properties').click(); } describe('Dashboard save action', () => { @@ -35,8 +37,8 @@ describe('Dashboard save action', () => { cy.login(); cy.visit(WORLD_HEALTH_DASHBOARD); cy.get('#app').then(data => { - cy.get('[data-test="dashboard-header"]').then(headerElement => { - const dashboardId = headerElement.attr('data-test-id'); + cy.get('.dashboard-header-container').then(headerContainerElement => { + const dashboardId = headerContainerElement.attr('data-test-id'); cy.intercept('POST', `/superset/copy_dash/${dashboardId}/`).as( 'copyRequest', @@ -56,7 +58,7 @@ describe('Dashboard save action', () => { // change to what the title should be it('should save as new dashboard', () => { cy.wait('@copyRequest').then(xhr => { - cy.get('[data-test="editable-title-input"]').then(element => { + cy.get('[data-test="editable-title"]').then(element => { const dashboardTitle = element.attr('title'); expect(dashboardTitle).to.not.equal(`World Bank's Data`); }); @@ -68,7 +70,7 @@ describe('Dashboard save action', () => { WORLD_HEALTH_CHARTS.forEach(waitForChartLoad); // remove box_plot chart from dashboard - cy.get('[aria-label="edit-alt"]').click({ timeout: 5000 }); + cy.get('[aria-label="Edit dashboard"]').click({ timeout: 5000 }); cy.get('[data-test="dashboard-delete-component-button"]') .last() .trigger('mouseenter') @@ -79,15 +81,15 @@ describe('Dashboard save action', () => { .should('not.exist'); cy.intercept('PUT', '/api/v1/dashboard/**').as('putDashboardRequest'); - cy.get('[data-test="dashboard-header"]') + cy.get('.header-with-actions') .find('[data-test="header-save-button"]') .contains('Save') .click(); // go back to view mode cy.wait('@putDashboardRequest'); - cy.get('[data-test="dashboard-header"]') - .find('[aria-label="edit-alt"]') + cy.get('.header-with-actions') + .find('[aria-label="Edit dashboard"]') .click(); // deleted boxplot should still not exist @@ -142,7 +144,7 @@ describe('Dashboard save action', () => { cy.get('.ant-modal-body').should('not.exist'); // save dashboard changes - cy.get('.dashboard-header').contains('Save').click(); + cy.get('.header-with-actions').contains('Save').click(); // assert success flash cy.contains('saved successfully').should('be.visible'); diff --git a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/download_chart.test.js b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/download_chart.test.js index 42f9c13123bd8..ce4a871f8e2de 100644 --- a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/download_chart.test.js +++ b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/download_chart.test.js @@ -34,7 +34,7 @@ describe('Download Chart > Distribution bar chart', () => { }; cy.visitChartByParams(JSON.stringify(formData)); - cy.get('.right-button-panel .ant-dropdown-trigger').click(); + cy.get('.header-with-actions .ant-dropdown-trigger').click(); cy.get(':nth-child(1) > .ant-dropdown-menu-submenu-title').click(); cy.get( '.ant-dropdown-menu-submenu > .ant-dropdown-menu li:nth-child(3)', diff --git a/superset-frontend/cypress-base/cypress/support/directories.ts b/superset-frontend/cypress-base/cypress/support/directories.ts index 9c783a7220e80..fde9ee0cdeacf 100644 --- a/superset-frontend/cypress-base/cypress/support/directories.ts +++ b/superset-frontend/cypress-base/cypress/support/directories.ts @@ -630,7 +630,8 @@ export const dashboardView = { trashIcon: dataTestLocator('dashboard-delete-component-button'), refreshChart: dataTestLocator('refresh-chart-menu-item'), }, - threeDotsMenuIcon: '#save-dash-split-button', + threeDotsMenuIcon: + '.header-with-actions .right-button-panel .ant-dropdown-trigger', threeDotsMenuDropdown: dataTestLocator('header-actions-menu'), refreshDashboard: dataTestLocator('refresh-dashboard-menu-item'), saveAsMenuOption: dataTestLocator('save-as-menu-item'), @@ -660,7 +661,7 @@ export const dashboardView = { }, sliceThreeDots: '[aria-label="More Options"]', sliceThreeDotsDropdown: '[role="menu"]', - editDashboardButton: '[aria-label=edit-alt]', + editDashboardButton: '[aria-label="Edit dashboard"]', starIcon: dataTestLocator('fave-unfave-icon'), dashboardHeader: dataTestLocator('dashboard-header'), dashboardSectionContainer: dataTestLocator( diff --git a/superset-frontend/src/assets/images/icons/redo.svg b/superset-frontend/src/assets/images/icons/redo.svg new file mode 100644 index 0000000000000..a35cf022525e7 --- /dev/null +++ b/superset-frontend/src/assets/images/icons/redo.svg @@ -0,0 +1,21 @@ + + + + diff --git a/superset-frontend/src/assets/images/icons/undo.svg b/superset-frontend/src/assets/images/icons/undo.svg new file mode 100644 index 0000000000000..b680a68649681 --- /dev/null +++ b/superset-frontend/src/assets/images/icons/undo.svg @@ -0,0 +1,21 @@ + + + + diff --git a/superset-frontend/src/components/DynamicEditableTitle/index.tsx b/superset-frontend/src/components/DynamicEditableTitle/index.tsx index 969aea19b7302..d9e7066330130 100644 --- a/superset-frontend/src/components/DynamicEditableTitle/index.tsx +++ b/superset-frontend/src/components/DynamicEditableTitle/index.tsx @@ -92,6 +92,10 @@ export const DynamicEditableTitle = ({ refreshMode: 'debounce', }); + useEffect(() => { + setCurrentTitle(title); + }, [title]); + useEffect(() => { if (isEditing && contentRef?.current) { contentRef.current.focus(); @@ -202,6 +206,7 @@ export const DynamicEditableTitle = ({ className="dynamic-title" aria-label={label ?? t('Title')} ref={contentRef} + data-test="editable-title" > {currentTitle} diff --git a/superset-frontend/src/components/Icons/index.tsx b/superset-frontend/src/components/Icons/index.tsx index 08b13404a04d2..27efbe4c2e29f 100644 --- a/superset-frontend/src/components/Icons/index.tsx +++ b/superset-frontend/src/components/Icons/index.tsx @@ -155,6 +155,8 @@ const IconFileNames = [ 'tags', 'ballot', 'category', + 'undo', + 'redo', ]; const iconOverrides: Record = {}; diff --git a/superset-frontend/src/components/PageHeaderWithActions/index.tsx b/superset-frontend/src/components/PageHeaderWithActions/index.tsx index 204a82b235d1d..4449d1c6b3472 100644 --- a/superset-frontend/src/components/PageHeaderWithActions/index.tsx +++ b/superset-frontend/src/components/PageHeaderWithActions/index.tsx @@ -50,7 +50,21 @@ const headerStyles = (theme: SupersetTheme) => css` align-items: center; flex-wrap: nowrap; justify-content: space-between; - height: 100%; + background-color: ${theme.colors.grayscale.light5}; + height: ${theme.gridUnit * 16}px; + padding: 0 ${theme.gridUnit * 4}px; + + .editable-title { + overflow: hidden; + + & > input[type='button'], + & > span { + overflow: hidden; + text-overflow: ellipsis; + max-width: 100%; + white-space: nowrap; + } + } span[role='button'] { display: flex; @@ -113,7 +127,7 @@ export const PageHeaderWithActions = ({ }: PageHeaderWithActionsProps) => { const theme = useTheme(); return ( -
+
{showTitlePanelItems && ( diff --git a/superset-frontend/src/components/ReportModal/HeaderReportDropdown/index.tsx b/superset-frontend/src/components/ReportModal/HeaderReportDropdown/index.tsx index cd741e5c338ba..7beec04ffd7a3 100644 --- a/superset-frontend/src/components/ReportModal/HeaderReportDropdown/index.tsx +++ b/superset-frontend/src/components/ReportModal/HeaderReportDropdown/index.tsx @@ -243,7 +243,11 @@ export default function HeaderReportDropDown({ triggerNode.closest('.action-button') } > - + @@ -253,7 +257,7 @@ export default function HeaderReportDropDown({ role="button" title={t('Schedule email report')} tabIndex={0} - className="action-button" + className="action-button action-schedule-report" onClick={() => setShowModal(true)} > diff --git a/superset-frontend/src/dashboard/components/Header/Header.test.tsx b/superset-frontend/src/dashboard/components/Header/Header.test.tsx index e5851fb2d500b..730596a9f4b2b 100644 --- a/superset-frontend/src/dashboard/components/Header/Header.test.tsx +++ b/superset-frontend/src/dashboard/components/Header/Header.test.tsx @@ -122,7 +122,7 @@ function setup(props: HeaderProps, initialState = {}) { async function openActionsDropdown() { const btn = screen.getByRole('img', { name: 'more-horiz' }); userEvent.click(btn); - expect(await screen.findByRole('menu')).toBeInTheDocument(); + expect(await screen.findByTestId('header-actions-menu')).toBeInTheDocument(); } test('should render', () => { @@ -134,7 +134,9 @@ test('should render', () => { test('should render the title', () => { const mockedProps = createProps(); setup(mockedProps); - expect(screen.getByText('Dashboard Title')).toBeInTheDocument(); + expect(screen.getByTestId('editable-title')).toHaveTextContent( + 'Dashboard Title', + ); }); test('should render the editable title', () => { @@ -161,21 +163,30 @@ test('should render the "Draft" status', () => { }); test('should publish', () => { - setup(editableProps); + const mockedProps = createProps(); + const canEditProps = { + ...mockedProps, + dashboardInfo: { + ...mockedProps.dashboardInfo, + dash_edit_perm: true, + dash_save_perm: true, + }, + }; + setup(canEditProps); const draft = screen.getByText('Draft'); - expect(editableProps.savePublished).not.toHaveBeenCalled(); + expect(mockedProps.savePublished).toHaveBeenCalledTimes(0); userEvent.click(draft); - expect(editableProps.savePublished).toHaveBeenCalledTimes(1); + expect(mockedProps.savePublished).toHaveBeenCalledTimes(1); }); test('should render the "Undo" action as disabled', () => { setup(editableProps); - expect(screen.getByTitle('Undo').parentElement).toBeDisabled(); + expect(screen.getByTestId('undo-action').parentElement).toBeDisabled(); }); test('should undo', () => { setup(undoProps); - const undo = screen.getByTitle('Undo'); + const undo = screen.getByTestId('undo-action'); expect(undoProps.onUndo).not.toHaveBeenCalled(); userEvent.click(undo); expect(undoProps.onUndo).toHaveBeenCalledTimes(1); @@ -191,12 +202,12 @@ test('should undo with key listener', () => { test('should render the "Redo" action as disabled', () => { setup(editableProps); - expect(screen.getByTitle('Redo').parentElement).toBeDisabled(); + expect(screen.getByTestId('redo-action').parentElement).toBeDisabled(); }); test('should redo', () => { setup(redoProps); - const redo = screen.getByTitle('Redo'); + const redo = screen.getByTestId('redo-action'); expect(redoProps.onRedo).not.toHaveBeenCalled(); userEvent.click(redo); expect(redoProps.onRedo).toHaveBeenCalledTimes(1); @@ -212,7 +223,7 @@ test('should redo with key listener', () => { test('should render the "Discard changes" button', () => { setup(editableProps); - expect(screen.getByText('Discard changes')).toBeInTheDocument(); + expect(screen.getByText('Discard')).toBeInTheDocument(); }); test('should render the "Save" button as disabled', () => { @@ -297,8 +308,8 @@ test('should toggle the edit mode', () => { }, }; setup(canEditProps); - const editDashboard = screen.getByTitle('Edit dashboard'); - expect(screen.queryByTitle('Edit dashboard')).toBeInTheDocument(); + const editDashboard = screen.getByText('Edit dashboard'); + expect(screen.queryByText('Edit dashboard')).toBeInTheDocument(); userEvent.click(editDashboard); expect(mockedProps.logEvent).toHaveBeenCalled(); }); diff --git a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx index 57fe7a1333973..eb3c6aeb4e973 100644 --- a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx +++ b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx @@ -29,7 +29,7 @@ import HeaderActionsDropdown from '.'; const createProps = () => ({ addSuccessToast: jest.fn(), addDangerToast: jest.fn(), - customCss: '#save-dash-split-button{margin-left: 100px;}', + customCss: '.ant-menu {margin-left: 100px;}', dashboardId: 1, dashboardInfo: { id: 1, @@ -59,7 +59,10 @@ const createProps = () => ({ userCanEdit: false, userCanSave: false, userCanShare: false, + userCanCurate: false, lastModifiedTime: 0, + isDropdownVisible: true, + dataMask: {}, }); const editModeOnProps = { ...createProps(), @@ -67,50 +70,31 @@ const editModeOnProps = { }; function setup(props: HeaderDropdownProps) { - return ( + return render(
-
+
, + { useRedux: true }, ); } fetchMock.get('glob:*/csstemplateasyncmodelview/api/read', {}); -async function openDropdown() { - const btn = screen.getByRole('img', { name: 'more-horiz' }); - userEvent.click(btn); - expect(await screen.findByRole('menu')).toBeInTheDocument(); -} - test('should render', () => { const mockedProps = createProps(); - const { container } = render(setup(mockedProps)); + const { container } = setup(mockedProps); expect(container).toBeInTheDocument(); }); test('should render the dropdown button', () => { const mockedProps = createProps(); - render(setup(mockedProps)); + setup(mockedProps); expect(screen.getByRole('button')).toBeInTheDocument(); }); -test('should render the dropdown icon', () => { - const mockedProps = createProps(); - render(setup(mockedProps)); - expect(screen.getByRole('img', { name: 'more-horiz' })).toBeInTheDocument(); -}); - -test('should open the dropdown', async () => { - const mockedProps = createProps(); - render(setup(mockedProps)); - await openDropdown(); - expect(await screen.findByRole('menu')).toBeInTheDocument(); -}); - test('should render the menu items', async () => { const mockedProps = createProps(); - render(setup(mockedProps)); - await openDropdown(); + setup(mockedProps); expect(screen.getAllByRole('menuitem')).toHaveLength(4); expect(screen.getByText('Refresh dashboard')).toBeInTheDocument(); expect(screen.getByText('Set auto-refresh interval')).toBeInTheDocument(); @@ -119,13 +103,11 @@ test('should render the menu items', async () => { }); test('should render the menu items in edit mode', async () => { - render(setup(editModeOnProps)); - await openDropdown(); - expect(screen.getAllByRole('menuitem')).toHaveLength(5); - expect(screen.getByText('Refresh dashboard')).toBeInTheDocument(); + setup(editModeOnProps); + expect(screen.getAllByRole('menuitem')).toHaveLength(4); expect(screen.getByText('Set auto-refresh interval')).toBeInTheDocument(); expect(screen.getByText('Set filter mapping')).toBeInTheDocument(); - expect(screen.getByText('Edit dashboard properties')).toBeInTheDocument(); + expect(screen.getByText('Edit properties')).toBeInTheDocument(); expect(screen.getByText('Edit CSS')).toBeInTheDocument(); }); @@ -135,10 +117,9 @@ test('should show the share actions', async () => { ...mockedProps, userCanShare: true, }; - render(setup(canShareProps)); - await openDropdown(); - expect(screen.getByText('Copy permalink to clipboard')).toBeInTheDocument(); - expect(screen.getByText('Share permalink by email')).toBeInTheDocument(); + setup(canShareProps); + + expect(screen.getByText('Share')).toBeInTheDocument(); }); test('should render the "Save Modal" when user can save', async () => { @@ -147,15 +128,13 @@ test('should render the "Save Modal" when user can save', async () => { ...mockedProps, userCanSave: true, }; - render(setup(canSaveProps)); - await openDropdown(); + setup(canSaveProps); expect(screen.getByText('Save as')).toBeInTheDocument(); }); test('should NOT render the "Save Modal" menu item when user cannot save', async () => { const mockedProps = createProps(); - render(setup(mockedProps)); - await openDropdown(); + setup(mockedProps); expect(screen.queryByText('Save as')).not.toBeInTheDocument(); }); @@ -165,43 +144,41 @@ test('should render the "Refresh dashboard" menu item as disabled when loading', ...mockedProps, isLoading: true, }; - render(setup(loadingProps)); - await openDropdown(); + setup(loadingProps); expect(screen.getByText('Refresh dashboard')).toHaveClass( - 'ant-dropdown-menu-item-disabled', + 'ant-menu-item-disabled', ); }); test('should NOT render the "Refresh dashboard" menu item as disabled', async () => { const mockedProps = createProps(); - render(setup(mockedProps)); - await openDropdown(); + setup(mockedProps); expect(screen.getByText('Refresh dashboard')).not.toHaveClass( - 'ant-dropdown-menu-item-disabled', + 'ant-menu-item-disabled', ); }); test('should render with custom css', () => { const mockedProps = createProps(); const { customCss } = mockedProps; - render(setup(mockedProps)); + setup(mockedProps); injectCustomCss(customCss); - expect(screen.getByRole('button')).toHaveStyle('margin-left: 100px'); + expect(screen.getByTestId('header-actions-menu')).toHaveStyle( + 'margin-left: 100px', + ); }); test('should refresh the charts', async () => { const mockedProps = createProps(); - render(setup(mockedProps)); - await openDropdown(); + setup(mockedProps); userEvent.click(screen.getByText('Refresh dashboard')); expect(mockedProps.forceRefreshAllCharts).toHaveBeenCalledTimes(1); expect(mockedProps.addSuccessToast).toHaveBeenCalledTimes(1); }); test('should show the properties modal', async () => { - render(setup(editModeOnProps)); - await openDropdown(); - userEvent.click(screen.getByText('Edit dashboard properties')); + setup(editModeOnProps); + userEvent.click(screen.getByText('Edit properties')); expect(editModeOnProps.showPropertiesModal).toHaveBeenCalledTimes(1); }); diff --git a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/index.jsx b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/index.jsx index ad3dd91ec7ee5..a7860af30f378 100644 --- a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/index.jsx +++ b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/index.jsx @@ -19,16 +19,15 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { styled, SupersetClient, t } from '@superset-ui/core'; +import { SupersetClient, t } from '@superset-ui/core'; import { Menu } from 'src/components/Menu'; -import { NoAnimationDropdown } from 'src/components/Dropdown'; -import Icons from 'src/components/Icons'; import { URL_PARAMS } from 'src/constants'; import ShareMenuItems from 'src/dashboard/components/menu/ShareMenuItems'; import CssEditor from 'src/dashboard/components/CssEditor'; import RefreshIntervalModal from 'src/dashboard/components/RefreshIntervalModal'; import SaveModal from 'src/dashboard/components/SaveModal'; +import HeaderReportDropdown from 'src/components/ReportModal/HeaderReportDropdown'; import injectCustomCss from 'src/dashboard/util/injectCustomCss'; import { SAVE_TYPE_NEWDASHBOARD } from 'src/dashboard/util/constants'; import FilterScopeModal from 'src/dashboard/components/filterscope/FilterScopeModal'; @@ -91,15 +90,9 @@ const MENU_KEYS = { DOWNLOAD_AS_IMAGE: 'download-as-image', TOGGLE_FULLSCREEN: 'toggle-fullscreen', MANAGE_EMBEDDED: 'manage-embedded', + MANAGE_EMAIL_REPORT: 'manage-email-report', }; -const DropdownButton = styled.div` - margin-left: ${({ theme }) => theme.gridUnit * 2.5}px; - span { - color: ${({ theme }) => theme.colors.grayscale.base}; - } -`; - const SCREENSHOT_NODE_SELECTOR = '.dashboard'; class HeaderActionsDropdown extends React.PureComponent { @@ -112,11 +105,13 @@ class HeaderActionsDropdown extends React.PureComponent { this.state = { css: props.customCss, cssTemplates: [], + showReportSubMenu: null, }; this.changeCss = this.changeCss.bind(this); this.changeRefreshInterval = this.changeRefreshInterval.bind(this); this.handleMenuClick = this.handleMenuClick.bind(this); + this.setShowReportSubMenu = this.setShowReportSubMenu.bind(this); } UNSAFE_componentWillMount() { @@ -144,6 +139,12 @@ class HeaderActionsDropdown extends React.PureComponent { } } + setShowReportSubMenu(show) { + this.setState({ + showReportSubMenu: show, + }); + } + changeCss(css) { this.props.onChange(); this.props.updateCss(css); @@ -224,6 +225,9 @@ class HeaderActionsDropdown extends React.PureComponent { addSuccessToast, addDangerToast, filterboxMigrationState, + setIsDropdownVisible, + isDropdownVisible, + ...rest } = this.props; const emailTitle = t('Superset dashboard'); @@ -236,12 +240,47 @@ class HeaderActionsDropdown extends React.PureComponent { hash: window.location.hash, }); - const menu = ( - + return ( + + {!editMode && ( + + {t('Refresh dashboard')} + + )} + {!editMode && ( + + {getUrlParam(URL_PARAMS.standalone) + ? t('Exit fullscreen') + : t('Enter fullscreen')} + + )} + {editMode && ( + + {t('Edit properties')} + + )} + {editMode && ( + + {t('Edit CSS')}} + initialCss={this.state.css} + templates={this.state.cssTemplates} + onChange={this.changeCss} + /> + + )} + {userCanSave && ( )} + {!editMode && ( + + {t('Download as image')} + + )} {userCanShare && ( - + + + + )} + {!editMode && userCanCurate && ( + + {t('Embed dashboard')} + )} - - {t('Refresh dashboard')} - + {!editMode ? ( + this.state.showReportSubMenu ? ( + <> + + + + + + ) : ( + + + + ) + ) : null} + {editMode && + filterboxMigrationState !== FILTER_BOX_MIGRATION_STATES.CONVERTED && ( + + + + )} + {t('Set auto-refresh interval')}} /> - - {editMode && - filterboxMigrationState !== FILTER_BOX_MIGRATION_STATES.CONVERTED && ( - - - - )} - - {editMode && ( - - {t('Edit dashboard properties')} - - )} - - {editMode && ( - - {t('Edit CSS')}} - initialCss={this.state.css} - templates={this.state.cssTemplates} - onChange={this.changeCss} - /> - - )} - - {!editMode && userCanCurate && ( - - {t('Embed dashboard')} - - )} - - {!editMode && ( - - {t('Download as image')} - - )} - - {!editMode && ( - - {getUrlParam(URL_PARAMS.standalone) - ? t('Exit fullscreen') - : t('Enter fullscreen')} - - )} ); - return ( - - triggerNode.closest('.dashboard-header') - } - > - - - - - ); } } diff --git a/superset-frontend/src/dashboard/components/Header/index.jsx b/superset-frontend/src/dashboard/components/Header/index.jsx index f60a0d85d7ae2..ab85bcda04f55 100644 --- a/superset-frontend/src/dashboard/components/Header/index.jsx +++ b/superset-frontend/src/dashboard/components/Header/index.jsx @@ -20,8 +20,8 @@ import moment from 'moment'; import React from 'react'; import PropTypes from 'prop-types'; -import { styled, t, getSharedLabelColor } from '@superset-ui/core'; -import ButtonGroup from 'src/components/ButtonGroup'; +import { styled, css, t, getSharedLabelColor } from '@superset-ui/core'; +import { Global } from '@emotion/react'; import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags'; import { LOG_ACTIONS_PERIODIC_RENDER_DASHBOARD, @@ -30,11 +30,10 @@ import { } from 'src/logger/LogUtils'; import Icons from 'src/components/Icons'; import Button from 'src/components/Button'; -import EditableTitle from 'src/components/EditableTitle'; -import FaveStar from 'src/components/FaveStar'; +import { AntdButton } from 'src/components/'; +import { Tooltip } from 'src/components/Tooltip'; import { safeStringify } from 'src/utils/safeStringify'; import HeaderActionsDropdown from 'src/dashboard/components/Header/HeaderActionsDropdown'; -import HeaderReportDropdown from 'src/components/ReportModal/HeaderReportDropdown'; import PublishedStatus from 'src/dashboard/components/PublishedStatus'; import UndoRedoKeyListeners from 'src/dashboard/components/UndoRedoKeyListeners'; import PropertiesModal from 'src/dashboard/components/PropertiesModal'; @@ -51,6 +50,7 @@ import setPeriodicRunner, { import { options as PeriodicRefreshOptions } from 'src/dashboard/components/RefreshIntervalModal'; import findPermission from 'src/dashboard/util/findPermission'; import { FILTER_BOX_MIGRATION_STATES } from 'src/explore/constants'; +import { PageHeaderWithActions } from 'src/components/PageHeaderWithActions'; import { DashboardEmbedModal } from '../DashboardEmbedControls'; const propTypes = { @@ -107,34 +107,59 @@ const defaultProps = { colorScheme: undefined, }; -// Styled Components -const StyledDashboardHeader = styled.div` - background: ${({ theme }) => theme.colors.grayscale.light5}; +const headerContainerStyle = theme => css` + border-bottom: 1px solid ${theme.colors.grayscale.light2}; +`; + +const editButtonStyle = theme => css` + color: ${theme.colors.primary.dark2}; +`; + +const actionButtonsStyle = theme => css` display: flex; - flex-direction: row; align-items: center; - justify-content: space-between; - padding: 0 ${({ theme }) => theme.gridUnit * 6}px; - border-bottom: 1px solid ${({ theme }) => theme.colors.grayscale.light2}; - .action-button > span { - color: ${({ theme }) => theme.colors.grayscale.base}; + .action-schedule-report { + margin-left: ${theme.gridUnit * 2}px; } - button, - .fave-unfave-icon { - margin-left: ${({ theme }) => theme.gridUnit * 2}px; + + .undoRedo { + margin-right: ${theme.gridUnit * 2}px; } - .button-container { - display: flex; - flex-direction: row; - flex-wrap: nowrap; - .action-button { - font-size: ${({ theme }) => theme.typography.sizes.xl}px; - margin-left: ${({ theme }) => theme.gridUnit * 2.5}px; - } +`; + +const StyledUndoRedoButton = styled(AntdButton)` + padding: 0; + &:hover { + background: transparent; + } +`; + +const undoRedoStyle = theme => css` + color: ${theme.colors.grayscale.light1}; + &:hover { + color: ${theme.colors.grayscale.base}; } `; +const undoRedoEmphasized = theme => css` + color: ${theme.colors.grayscale.base}; +`; + +const undoRedoDisabled = theme => css` + color: ${theme.colors.grayscale.light2}; +`; + +const saveBtnStyle = theme => css` + min-width: ${theme.gridUnit * 17}px; + height: ${theme.gridUnit * 8}px; +`; + +const discardBtnStyle = theme => css` + min-width: ${theme.gridUnit * 22}px; + height: ${theme.gridUnit * 8}px; +`; + class Header extends React.PureComponent { static discardChanges() { const url = new URL(window.location.href); @@ -148,7 +173,9 @@ class Header extends React.PureComponent { this.state = { didNotifyMaxUndoHistoryToast: false, emphasizeUndo: false, + emphasizeRedo: false, showingPropertiesModal: false, + isDropdownVisible: false, }; this.handleChangeText = this.handleChangeText.bind(this); @@ -160,6 +187,7 @@ class Header extends React.PureComponent { this.overwriteDashboard = this.overwriteDashboard.bind(this); this.showPropertiesModal = this.showPropertiesModal.bind(this); this.hidePropertiesModal = this.hidePropertiesModal.bind(this); + this.setIsDropdownVisible = this.setIsDropdownVisible.bind(this); } componentDidMount() { @@ -205,6 +233,12 @@ class Header extends React.PureComponent { } } + setIsDropdownVisible(visible) { + this.setState({ + isDropdownVisible: visible, + }); + } + handleCtrlY() { this.props.onRedo(); this.setState({ emphasizeRedo: true }, () => { @@ -450,180 +484,214 @@ class Header extends React.PureComponent { }; return ( - -
- - - {user?.userId && dashboardInfo?.id && ( - - )} -
- -
- {userCanSaveAs && ( -
- {editMode && ( - <> - - + } + rightPanelAdditionalItems={ +
+ {userCanSaveAs && ( +
+ {editMode && ( +
+
+ + + + + + + + + + +
+ + +
+ )} +
+ )} + {editMode ? ( + + ) : ( +
+ {userCanEdit && ( - - - - + )} +
)}
- )} - {editMode ? ( - - ) : ( - <> - {userCanEdit && ( - - - - )} - - - )} - - {this.state.showingPropertiesModal && ( - + triggerNode.closest('.header-with-actions'), + visible: this.state.isDropdownVisible, + onVisibleChange: this.setIsDropdownVisible, + }} + additionalActionsMenu={ + - )} - - {userCanCurate && ( - - )} - - + {this.state.showingPropertiesModal && ( + + )} + + {userCanCurate && ( + -
- + )} + +
); } } diff --git a/superset-frontend/src/dashboard/components/RefreshIntervalModal.test.tsx b/superset-frontend/src/dashboard/components/RefreshIntervalModal.test.tsx index 9f2f68a9d6451..9151275e800de 100644 --- a/superset-frontend/src/dashboard/components/RefreshIntervalModal.test.tsx +++ b/superset-frontend/src/dashboard/components/RefreshIntervalModal.test.tsx @@ -68,7 +68,8 @@ describe('RefreshIntervalModal - Enzyme', () => { const createProps = () => ({ addSuccessToast: jest.fn(), addDangerToast: jest.fn(), - customCss: '#save-dash-split-button{margin-left: 100px;}', + customCss: + '.header-with-actions .right-button-panel .ant-dropdown-trigger{margin-left: 100px;}', dashboardId: 1, dashboardInfo: { id: 1, @@ -100,6 +101,7 @@ const createProps = () => ({ userCanSave: false, userCanShare: false, lastModifiedTime: 0, + isDropdownVisible: true, }); const editModeOnProps = { @@ -116,9 +118,6 @@ const setup = (overrides?: any) => ( fetchMock.get('glob:*/csstemplateasyncmodelview/api/read', {}); const openRefreshIntervalModal = async () => { - const headerActionsButton = screen.getByRole('img', { name: 'more-horiz' }); - userEvent.click(headerActionsButton); - const autoRefreshOption = screen.getByText('Set auto-refresh interval'); userEvent.click(autoRefreshOption); }; diff --git a/superset-frontend/src/dashboard/components/menu/ShareMenuItems/index.tsx b/superset-frontend/src/dashboard/components/menu/ShareMenuItems/index.tsx index b196100734cc3..92e5665aa01b5 100644 --- a/superset-frontend/src/dashboard/components/menu/ShareMenuItems/index.tsx +++ b/superset-frontend/src/dashboard/components/menu/ShareMenuItems/index.tsx @@ -87,7 +87,7 @@ const ShareMenuItems = (props: ShareMenuItemProps) => { } return ( - <> +
{copyMenuItemTitle} @@ -98,7 +98,7 @@ const ShareMenuItems = (props: ShareMenuItemProps) => { {emailMenuItemTitle}
- +
); }; diff --git a/superset-frontend/src/dashboard/stylesheets/components/header.less b/superset-frontend/src/dashboard/stylesheets/components/header.less index 7db5924b71265..355385d373fd6 100644 --- a/superset-frontend/src/dashboard/stylesheets/components/header.less +++ b/superset-frontend/src/dashboard/stylesheets/components/header.less @@ -55,11 +55,6 @@ color: @almost-black; } -.dashboard-header .dashboard-component-header { - font-weight: @font-weight-normal; - width: auto; -} - .dashboard--editing /* note: sizes should be a multiple of the 8px grid unit so that rows in the grid align */ diff --git a/superset-frontend/src/dashboard/stylesheets/dashboard.less b/superset-frontend/src/dashboard/stylesheets/dashboard.less index b9b2b0aab92f8..4586913b0ac36 100644 --- a/superset-frontend/src/dashboard/stylesheets/dashboard.less +++ b/superset-frontend/src/dashboard/stylesheets/dashboard.less @@ -150,27 +150,6 @@ body { margin: 0 20px; } -.dashboard-header .dashboard-component-header { - display: flex; - flex-direction: row; - align-items: center; - - .editable-title { - margin-right: 8px; - } - - .favstar { - font-size: @font-size-xl; - position: relative; - margin-left: 8px; - } - - .publish { - position: relative; - margin-left: 8px; - } -} - .slice_container .alert { margin: 10px; } diff --git a/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx b/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx index 97e30d335b7a6..2a3c45cf9ee8a 100644 --- a/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx +++ b/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx @@ -86,26 +86,6 @@ const ExploreContainer = styled.div` height: 100%; `; -const ExploreHeaderContainer = styled.div` - ${({ theme }) => css` - background-color: ${theme.colors.grayscale.light5}; - height: ${theme.gridUnit * 16}px; - padding: 0 ${theme.gridUnit * 4}px; - - .editable-title { - overflow: hidden; - - & > input[type='button'], - & > span { - overflow: hidden; - text-overflow: ellipsis; - max-width: 100%; - white-space: nowrap; - } - } - `} -`; - const ExplorePanelContainer = styled.div` ${({ theme }) => css` background: ${theme.colors.grayscale.light5}; @@ -530,24 +510,22 @@ function ExploreViewContainer(props) { return ( - - - +