diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx index 03d993288f4ff..347858f039eaf 100644 --- a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx +++ b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx @@ -34,7 +34,7 @@ import { Global } from '@emotion/react'; import { useDispatch, useSelector } from 'react-redux'; import ErrorBoundary from 'src/components/ErrorBoundary'; import BuilderComponentPane from 'src/dashboard/components/BuilderComponentPane'; -import DashboardHeader from 'src/dashboard/containers/DashboardHeader'; +import DashboardHeader from 'src/dashboard/components/Header'; import Icons from 'src/components/Icons'; import IconButton from 'src/dashboard/components/IconButton'; import { Droppable } from 'src/dashboard/components/dnd/DragDroppable'; diff --git a/superset-frontend/src/dashboard/components/Header/Header.test.tsx b/superset-frontend/src/dashboard/components/Header/Header.test.tsx index 9a2c414d00f7a..0c679f4415d34 100644 --- a/superset-frontend/src/dashboard/components/Header/Header.test.tsx +++ b/superset-frontend/src/dashboard/components/Header/Header.test.tsx @@ -16,19 +16,17 @@ * specific language governing permissions and limitations * under the License. */ +import * as redux from 'redux'; import { render, screen, fireEvent } from 'spec/helpers/testing-library'; import userEvent from '@testing-library/user-event'; import fetchMock from 'fetch-mock'; -import { getExtensionsRegistry } from '@superset-ui/core'; +import { getExtensionsRegistry, JsonObject } from '@superset-ui/core'; import setupExtensions from 'src/setup/setupExtensions'; import getOwnerName from 'src/utils/getOwnerName'; -import { HeaderProps } from './types'; import Header from '.'; +import { DASHBOARD_HEADER_ID } from '../../util/constants'; -const createProps = () => ({ - addSuccessToast: jest.fn(), - addDangerToast: jest.fn(), - addWarningToast: jest.fn(), +const initialState = { dashboardInfo: { id: 1, dash_edit_perm: false, @@ -69,74 +67,69 @@ const createProps = () => ({ userId: 1, username: 'admin', }, - reports: {}, - dashboardTitle: 'Dashboard Title', + dashboardState: { + sliceIds: [], + expandedSlices: {}, + refreshFrequency: 0, + shouldPersistRefreshFrequency: false, + css: '', + isStarred: false, + isPublished: false, + hasUnsavedChanges: false, + maxUndoHistoryExceeded: false, + editMode: false, + lastModifiedTime: 0, + }, charts: {}, - layout: {}, - expandedSlices: {}, - css: '', - customCss: '', - isStarred: false, - isLoading: false, - lastModifiedTime: 0, - refreshFrequency: 0, - shouldPersistRefreshFrequency: false, - onSave: jest.fn(), - onChange: jest.fn(), - fetchFaveStar: jest.fn(), - fetchCharts: jest.fn(), - onRefresh: jest.fn(), - saveFaveStar: jest.fn(), - savePublished: jest.fn(), - isPublished: false, - updateDashboardTitle: jest.fn(), - editMode: false, - setEditMode: jest.fn(), - showBuilderPane: jest.fn(), - updateCss: jest.fn(), - setColorScheme: jest.fn(), - setUnsavedChanges: jest.fn(), - logEvent: jest.fn(), - setRefreshFrequency: jest.fn(), - hasUnsavedChanges: false, - maxUndoHistoryExceeded: false, - onUndo: jest.fn(), - onRedo: jest.fn(), - undoLength: 0, - redoLength: 0, - setMaxUndoHistoryExceeded: jest.fn(), - maxUndoHistoryToast: jest.fn(), - dashboardInfoChanged: jest.fn(), - dashboardTitleChanged: jest.fn(), - showMenuDropdown: true, -}); -const props = createProps(); -const editableProps = { - ...props, - editMode: true, + dashboardLayout: { + present: { + [DASHBOARD_HEADER_ID]: { + meta: { + text: 'Dashboard Title', + }, + }, + }, + past: [], + future: [], + }, +}; + +const editableState = { + dashboardState: { + ...initialState.dashboardState, + editMode: true, + }, dashboardInfo: { - ...props.dashboardInfo, + ...initialState.dashboardInfo, dash_edit_perm: true, dash_save_perm: true, }, }; -const undoProps = { - ...editableProps, - undoLength: 1, + +const undoState = { + ...editableState, + dashboardLayout: { + ...initialState.dashboardLayout, + past: [{}], + }, }; -const redoProps = { - ...editableProps, - redoLength: 1, + +const redoState = { + ...editableState, + dashboardLayout: { + ...initialState.dashboardLayout, + future: [{}], + }, }; fetchMock.get('glob:*/csstemplateasyncmodelview/api/read', {}); -function setup(props: HeaderProps, initialState = {}) { +function setup(overrideState: JsonObject = {}) { return render(
-
+
, - { useRedux: true, initialState }, + { useRedux: true, initialState: { ...initialState, ...overrideState } }, ); } @@ -146,173 +139,223 @@ async function openActionsDropdown() { expect(await screen.findByTestId('header-actions-menu')).toBeInTheDocument(); } +const addSuccessToast = jest.fn(); +const addDangerToast = jest.fn(); +const addWarningToast = jest.fn(); +const onUndo = jest.fn(); +const onRedo = jest.fn(); +const setEditMode = jest.fn(); +const setUnsavedChanges = jest.fn(); +const fetchFaveStar = jest.fn(); +const saveFaveStar = jest.fn(); +const savePublished = jest.fn(); +const fetchCharts = jest.fn(); +const updateDashboardTitle = jest.fn(); +const updateCss = jest.fn(); +const onChange = jest.fn(); +const onSave = jest.fn(); +const setMaxUndoHistoryExceeded = jest.fn(); +const maxUndoHistoryToast = jest.fn(); +const logEvent = jest.fn(); +const setRefreshFrequency = jest.fn(); +const onRefresh = jest.fn(); +const dashboardInfoChanged = jest.fn(); +const dashboardTitleChanged = jest.fn(); + +beforeAll(() => { + jest.spyOn(redux, 'bindActionCreators').mockImplementation(() => ({ + addSuccessToast, + addDangerToast, + addWarningToast, + onUndo, + onRedo, + setEditMode, + setUnsavedChanges, + fetchFaveStar, + saveFaveStar, + savePublished, + fetchCharts, + updateDashboardTitle, + updateCss, + onChange, + onSave, + setMaxUndoHistoryExceeded, + maxUndoHistoryToast, + logEvent, + setRefreshFrequency, + onRefresh, + dashboardInfoChanged, + dashboardTitleChanged, + })); +}); + +beforeEach(() => { + jest.clearAllMocks(); +}); + test('should render', () => { - const mockedProps = createProps(); - const { container } = setup(mockedProps); + const { container } = setup(); expect(container).toBeInTheDocument(); }); test('should render the title', () => { - const mockedProps = createProps(); - setup(mockedProps); + setup(); expect(screen.getByTestId('editable-title')).toHaveTextContent( 'Dashboard Title', ); }); test('should render the editable title', () => { - setup(editableProps); + setup(editableState); expect(screen.getByDisplayValue('Dashboard Title')).toBeInTheDocument(); }); test('should edit the title', () => { - setup(editableProps); + setup(editableState); const editableTitle = screen.getByDisplayValue('Dashboard Title'); - expect(editableProps.onChange).not.toHaveBeenCalled(); + expect(onChange).not.toHaveBeenCalled(); userEvent.click(editableTitle); userEvent.clear(editableTitle); userEvent.type(editableTitle, 'New Title'); userEvent.click(document.body); - expect(editableProps.onChange).toHaveBeenCalled(); + expect(onChange).toHaveBeenCalled(); expect(screen.getByDisplayValue('New Title')).toBeInTheDocument(); }); test('should render the "Draft" status', () => { - const mockedProps = createProps(); - setup(mockedProps); + setup(); expect(screen.getByText('Draft')).toBeInTheDocument(); }); test('should publish', () => { - const mockedProps = createProps(); - const canEditProps = { - ...mockedProps, + const canEditState = { dashboardInfo: { - ...mockedProps.dashboardInfo, + ...initialState.dashboardInfo, dash_edit_perm: true, dash_save_perm: true, }, }; - setup(canEditProps); + setup(canEditState); const draft = screen.getByText('Draft'); - expect(mockedProps.savePublished).toHaveBeenCalledTimes(0); + expect(savePublished).toHaveBeenCalledTimes(0); userEvent.click(draft); - expect(mockedProps.savePublished).toHaveBeenCalledTimes(1); + expect(savePublished).toHaveBeenCalledTimes(1); }); test('should render metadata', () => { - const mockedProps = createProps(); - setup(mockedProps); + setup(); expect( - screen.getByText(getOwnerName(mockedProps.dashboardInfo.created_by)), + screen.getByText(getOwnerName(initialState.dashboardInfo.created_by)), ).toBeInTheDocument(); expect( - screen.getByText(mockedProps.dashboardInfo.changed_on_delta_humanized), + screen.getByText(initialState.dashboardInfo.changed_on_delta_humanized), ).toBeInTheDocument(); }); test('should render the "Undo" action as disabled', () => { - setup(editableProps); + setup(editableState); expect(screen.getByTestId('undo-action').parentElement).toBeDisabled(); }); test('should undo', () => { - setup(undoProps); + setup(undoState); const undo = screen.getByTestId('undo-action'); - expect(undoProps.onUndo).not.toHaveBeenCalled(); + expect(onUndo).not.toHaveBeenCalled(); userEvent.click(undo); - expect(undoProps.onUndo).toHaveBeenCalledTimes(1); + expect(onUndo).toHaveBeenCalledTimes(1); }); test('should undo with key listener', () => { - undoProps.onUndo.mockReset(); - setup(undoProps); - expect(undoProps.onUndo).not.toHaveBeenCalled(); + onUndo.mockReset(); + setup(undoState); + expect(onUndo).not.toHaveBeenCalled(); fireEvent.keyDown(document.body, { key: 'z', code: 'KeyZ', ctrlKey: true }); - expect(undoProps.onUndo).toHaveBeenCalledTimes(1); + expect(onUndo).toHaveBeenCalledTimes(1); }); test('should render the "Redo" action as disabled', () => { - setup(editableProps); + setup(editableState); expect(screen.getByTestId('redo-action').parentElement).toBeDisabled(); }); test('should redo', () => { - setup(redoProps); + setup(redoState); const redo = screen.getByTestId('redo-action'); - expect(redoProps.onRedo).not.toHaveBeenCalled(); + expect(onRedo).not.toHaveBeenCalled(); userEvent.click(redo); - expect(redoProps.onRedo).toHaveBeenCalledTimes(1); + expect(onRedo).toHaveBeenCalledTimes(1); }); test('should redo with key listener', () => { - redoProps.onRedo.mockReset(); - setup(redoProps); - expect(redoProps.onRedo).not.toHaveBeenCalled(); + setup(redoState); + expect(onRedo).not.toHaveBeenCalled(); fireEvent.keyDown(document.body, { key: 'y', code: 'KeyY', ctrlKey: true }); - expect(redoProps.onRedo).toHaveBeenCalledTimes(1); + expect(onRedo).toHaveBeenCalledTimes(1); }); test('should render the "Discard changes" button', () => { - setup(editableProps); + setup(editableState); expect(screen.getByText('Discard')).toBeInTheDocument(); }); test('should render the "Save" button as disabled', () => { - setup(editableProps); + setup(editableState); expect(screen.getByText('Save').parentElement).toBeDisabled(); }); test('should save', () => { - const unsavedProps = { - ...editableProps, - hasUnsavedChanges: true, + const unsavedState = { + ...editableState, + dashboardState: { + ...editableState.dashboardState, + hasUnsavedChanges: true, + }, }; - setup(unsavedProps); + setup(unsavedState); const save = screen.getByText('Save'); - expect(unsavedProps.onSave).not.toHaveBeenCalled(); + expect(onSave).not.toHaveBeenCalled(); userEvent.click(save); - expect(unsavedProps.onSave).toHaveBeenCalledTimes(1); + expect(onSave).toHaveBeenCalledTimes(1); }); test('should NOT render the "Draft" status', () => { - const mockedProps = createProps(); - const publishedProps = { - ...mockedProps, - isPublished: true, + const publishedState = { + ...initialState, + dashboardState: { + ...initialState.dashboardState, + isPublished: true, + }, }; - setup(publishedProps); + setup(publishedState); expect(screen.queryByText('Draft')).not.toBeInTheDocument(); }); test('should render the unselected fave icon', () => { - const mockedProps = createProps(); - setup(mockedProps); - expect(mockedProps.fetchFaveStar).toHaveBeenCalled(); + setup(); + expect(fetchFaveStar).toHaveBeenCalled(); expect( screen.getByRole('img', { name: 'favorite-unselected' }), ).toBeInTheDocument(); }); test('should render the selected fave icon', () => { - const mockedProps = createProps(); - const favedProps = { - ...mockedProps, - isStarred: true, + const favedState = { + dashboardState: { + ...initialState.dashboardState, + isStarred: true, + }, }; - setup(favedProps); + setup(favedState); expect( screen.getByRole('img', { name: 'favorite-selected' }), ).toBeInTheDocument(); }); test('should NOT render the fave icon on anonymous user', () => { - const mockedProps = createProps(); - const anonymousUserProps = { - ...mockedProps, + const anonymousUserState = { user: undefined, }; - setup(anonymousUserProps); + setup(anonymousUserState); expect(() => screen.getByRole('img', { name: 'favorite-unselected' }), ).toThrow('Unable to find'); @@ -322,42 +365,37 @@ test('should NOT render the fave icon on anonymous user', () => { }); test('should fave', async () => { - const mockedProps = createProps(); - setup(mockedProps); + setup(); const fave = screen.getByRole('img', { name: 'favorite-unselected' }); - expect(mockedProps.saveFaveStar).not.toHaveBeenCalled(); + expect(saveFaveStar).not.toHaveBeenCalled(); userEvent.click(fave); - expect(mockedProps.saveFaveStar).toHaveBeenCalledTimes(1); + expect(saveFaveStar).toHaveBeenCalledTimes(1); }); test('should toggle the edit mode', () => { - const mockedProps = createProps(); - const canEditProps = { - ...mockedProps, + const canEditState = { dashboardInfo: { - ...mockedProps.dashboardInfo, + ...initialState.dashboardInfo, dash_edit_perm: true, }, }; - setup(canEditProps); + setup(canEditState); const editDashboard = screen.getByText('Edit dashboard'); expect(screen.queryByText('Edit dashboard')).toBeInTheDocument(); userEvent.click(editDashboard); - expect(mockedProps.logEvent).toHaveBeenCalled(); + expect(logEvent).toHaveBeenCalled(); }); test('should render the dropdown icon', () => { - const mockedProps = createProps(); - setup(mockedProps); + setup(); expect(screen.getByRole('img', { name: 'more-horiz' })).toBeInTheDocument(); }); test('should refresh the charts', async () => { - const mockedProps = createProps(); - setup(mockedProps); + setup(); await openActionsDropdown(); userEvent.click(screen.getByText('Refresh dashboard')); - expect(mockedProps.onRefresh).toHaveBeenCalledTimes(1); + expect(onRefresh).toHaveBeenCalledTimes(1); }); test('should render an extension component if one is supplied', () => { @@ -367,54 +405,48 @@ test('should render an extension component if one is supplied', () => { )); setupExtensions(); - const mockedProps = createProps(); - setup(mockedProps); + setup(); expect( screen.getByText('dashboard.nav.right extension component'), ).toBeInTheDocument(); }); test('should NOT render MetadataBar when in edit mode', () => { - const mockedProps = { - ...createProps(), - editMode: true, + const state = { + ...editableState, dashboardInfo: { - ...createProps().dashboardInfo, + ...initialState.dashboardInfo, userId: '123', }, }; - setup(mockedProps); + setup(state); expect( - screen.queryByText(mockedProps.dashboardInfo.changed_on_delta_humanized), + screen.queryByText(state.dashboardInfo.changed_on_delta_humanized), ).not.toBeInTheDocument(); }); test('should NOT render MetadataBar when embedded', () => { - const mockedProps = { - ...createProps(), - editMode: false, + const state = { dashboardInfo: { - ...createProps().dashboardInfo, + ...initialState.dashboardInfo, userId: undefined, }, }; - setup(mockedProps); + setup(state); expect( - screen.queryByText(mockedProps.dashboardInfo.changed_on_delta_humanized), + screen.queryByText(state.dashboardInfo.changed_on_delta_humanized), ).not.toBeInTheDocument(); }); test('should render MetadataBar when not in edit mode and not embedded', () => { - const mockedProps = { - ...createProps(), - editMode: false, + const state = { dashboardInfo: { - ...createProps().dashboardInfo, + ...initialState.dashboardInfo, userId: '123', }, }; - setup(mockedProps); + setup(state); expect( - screen.getByText(mockedProps.dashboardInfo.changed_on_delta_humanized), + screen.getByText(state.dashboardInfo.changed_on_delta_humanized), ).toBeInTheDocument(); }); diff --git a/superset-frontend/src/dashboard/components/Header/index.jsx b/superset-frontend/src/dashboard/components/Header/index.jsx index c0fc97838cb83..01d5424706209 100644 --- a/superset-frontend/src/dashboard/components/Header/index.jsx +++ b/superset-frontend/src/dashboard/components/Header/index.jsx @@ -18,8 +18,7 @@ */ /* eslint-env browser */ import moment from 'moment'; -import { PureComponent } from 'react'; -import PropTypes from 'prop-types'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { styled, css, @@ -29,6 +28,8 @@ import { getExtensionsRegistry, } from '@superset-ui/core'; import { Global } from '@emotion/react'; +import { shallowEqual, useDispatch, useSelector } from 'react-redux'; +import { bindActionCreators } from 'redux'; import { LOG_ACTIONS_PERIODIC_RENDER_DASHBOARD, LOG_ACTIONS_FORCE_REFRESH_DASHBOARD, @@ -43,12 +44,12 @@ import ConnectedHeaderActionsDropdown from 'src/dashboard/components/Header/Head import PublishedStatus from 'src/dashboard/components/PublishedStatus'; import UndoRedoKeyListeners from 'src/dashboard/components/UndoRedoKeyListeners'; import PropertiesModal from 'src/dashboard/components/PropertiesModal'; -import { chartPropShape } from 'src/dashboard/util/propShapes'; import getOwnerName from 'src/utils/getOwnerName'; import { UNDO_LIMIT, SAVE_TYPE_OVERWRITE, DASHBOARD_POSITION_DATA_LIMIT, + DASHBOARD_HEADER_ID, } from 'src/dashboard/util/constants'; import setPeriodicRunner, { stopPeriodicRender, @@ -57,64 +58,39 @@ import { PageHeaderWithActions } from 'src/components/PageHeaderWithActions'; import MetadataBar, { MetadataType } from 'src/components/MetadataBar'; import DashboardEmbedModal from '../EmbeddedModal'; import OverwriteConfirm from '../OverwriteConfirm'; +import { + addDangerToast, + addSuccessToast, + addWarningToast, +} from '../../../components/MessageToasts/actions'; +import { + dashboardTitleChanged, + redoLayoutAction, + undoLayoutAction, + updateDashboardTitle, +} from '../../actions/dashboardLayout'; +import { + fetchCharts, + fetchFaveStar, + maxUndoHistoryToast, + onChange, + onRefresh, + saveDashboardRequest, + saveFaveStar, + savePublished, + setEditMode, + setMaxUndoHistoryExceeded, + setRefreshFrequency, + setUnsavedChanges, + updateCss, +} from '../../actions/dashboardState'; +import { logEvent } from '../../../logger/actions'; +import { dashboardInfoChanged } from '../../actions/dashboardInfo'; +import isDashboardLoading from '../../util/isDashboardLoading'; +import { useChartIds } from '../../util/charts/useChartIds'; const extensionsRegistry = getExtensionsRegistry(); -const propTypes = { - addSuccessToast: PropTypes.func.isRequired, - addDangerToast: PropTypes.func.isRequired, - addWarningToast: PropTypes.func.isRequired, - user: PropTypes.object, // UserWithPermissionsAndRoles, - dashboardInfo: PropTypes.object.isRequired, - dashboardTitle: PropTypes.string, - dataMask: PropTypes.object.isRequired, - charts: PropTypes.objectOf(chartPropShape).isRequired, - layout: PropTypes.object.isRequired, - expandedSlices: PropTypes.object, - customCss: PropTypes.string, - colorNamespace: PropTypes.string, - colorScheme: PropTypes.string, - setColorScheme: PropTypes.func.isRequired, - setUnsavedChanges: PropTypes.func.isRequired, - isStarred: PropTypes.bool.isRequired, - isPublished: PropTypes.bool.isRequired, - isLoading: PropTypes.bool.isRequired, - onSave: PropTypes.func.isRequired, - onChange: PropTypes.func.isRequired, - fetchFaveStar: PropTypes.func.isRequired, - fetchCharts: PropTypes.func.isRequired, - saveFaveStar: PropTypes.func.isRequired, - savePublished: PropTypes.func.isRequired, - updateDashboardTitle: PropTypes.func.isRequired, - editMode: PropTypes.bool.isRequired, - setEditMode: PropTypes.func.isRequired, - showBuilderPane: PropTypes.func.isRequired, - updateCss: PropTypes.func.isRequired, - logEvent: PropTypes.func.isRequired, - hasUnsavedChanges: PropTypes.bool.isRequired, - maxUndoHistoryExceeded: PropTypes.bool.isRequired, - lastModifiedTime: PropTypes.number.isRequired, - - // redux - onRefresh: PropTypes.func.isRequired, - onUndo: PropTypes.func.isRequired, - onRedo: PropTypes.func.isRequired, - undoLength: PropTypes.number.isRequired, - redoLength: PropTypes.number.isRequired, - setMaxUndoHistoryExceeded: PropTypes.func.isRequired, - maxUndoHistoryToast: PropTypes.func.isRequired, - refreshFrequency: PropTypes.number, - shouldPersistRefreshFrequency: PropTypes.bool.isRequired, - setRefreshFrequency: PropTypes.func.isRequired, - dashboardInfoChanged: PropTypes.func.isRequired, - dashboardTitleChanged: PropTypes.func.isRequired, -}; - -const defaultProps = { - colorNamespace: undefined, - colorScheme: undefined, -}; - const headerContainerStyle = theme => css` border-bottom: 1px solid ${theme.colors.grayscale.light2}; `; @@ -138,7 +114,7 @@ const actionButtonsStyle = theme => css` `; const StyledUndoRedoButton = styled(Button)` - // TODO: check if we need this. + // TODO: check if we need this padding: 0; &:hover { background: transparent; @@ -170,209 +146,251 @@ const discardBtnStyle = theme => css` height: ${theme.gridUnit * 8}px; `; -class Header extends PureComponent { - static discardChanges() { - const url = new URL(window.location.href); +const discardChanges = () => { + const url = new URL(window.location.href); - url.searchParams.delete('edit'); - window.location.assign(url); - } + url.searchParams.delete('edit'); + window.location.assign(url); +}; - constructor(props) { - super(props); - this.state = { - didNotifyMaxUndoHistoryToast: false, - emphasizeUndo: false, - emphasizeRedo: false, - showingPropertiesModal: false, - isDropdownVisible: false, - }; +const Header = () => { + const dispatch = useDispatch(); + const [didNotifyMaxUndoHistoryToast, setDidNotifyMaxUndoHistoryToast] = + useState(false); + const [emphasizeUndo, setEmphasizeUndo] = useState(false); + const [emphasizeRedo, setEmphasizeRedo] = useState(false); + const [showingPropertiesModal, setShowingPropertiesModal] = useState(false); + const [isDropdownVisible, setIsDropdownVisible] = useState(false); + const [showingEmbedModal, setShowingEmbedModal] = useState(false); + const dashboardInfo = useSelector(state => state.dashboardInfo); + const layout = useSelector(state => state.dashboardLayout.present); + const undoLength = useSelector(state => state.dashboardLayout.past.length); + const redoLength = useSelector(state => state.dashboardLayout.future.length); + const dataMask = useSelector(state => state.dataMask); + const user = useSelector(state => state.user); + const chartIds = useChartIds(); + + const { + expandedSlices, + refreshFrequency, + shouldPersistRefreshFrequency, + customCss, + colorNamespace, + colorScheme, + isStarred, + isPublished, + hasUnsavedChanges, + maxUndoHistoryExceeded, + editMode, + lastModifiedTime, + } = useSelector( + state => ({ + expandedSlices: state.dashboardState.expandedSlices, + refreshFrequency: state.dashboardState.refreshFrequency, + shouldPersistRefreshFrequency: + !!state.dashboardState.shouldPersistRefreshFrequency, + customCss: state.dashboardState.css, + colorNamespace: state.dashboardState.colorNamespace, + colorScheme: state.dashboardState.colorScheme, + isStarred: !!state.dashboardState.isStarred, + isPublished: !!state.dashboardState.isPublished, + hasUnsavedChanges: !!state.dashboardState.hasUnsavedChanges, + maxUndoHistoryExceeded: !!state.dashboardState.maxUndoHistoryExceeded, + editMode: !!state.dashboardState.editMode, + lastModifiedTime: state.lastModifiedTime, + }), + shallowEqual, + ); + const isLoading = useSelector(state => isDashboardLoading(state.charts)); + + const refreshTimer = useRef(0); + const ctrlYTimeout = useRef(0); + const ctrlZTimeout = useRef(0); + + const dashboardTitle = layout[DASHBOARD_HEADER_ID]?.meta?.text; + const { slug } = dashboardInfo; + const actualLastModifiedTime = Math.max( + lastModifiedTime, + dashboardInfo.last_modified_time, + ); + const boundActionCreators = useMemo( + () => + bindActionCreators( + { + addSuccessToast, + addDangerToast, + addWarningToast, + onUndo: undoLayoutAction, + onRedo: redoLayoutAction, + setEditMode, + setUnsavedChanges, + fetchFaveStar, + saveFaveStar, + savePublished, + fetchCharts, + updateDashboardTitle, + updateCss, + onChange, + onSave: saveDashboardRequest, + setMaxUndoHistoryExceeded, + maxUndoHistoryToast, + logEvent, + setRefreshFrequency, + onRefresh, + dashboardInfoChanged, + dashboardTitleChanged, + }, + dispatch, + ), + [dispatch], + ); + + const startPeriodicRender = useCallback( + interval => { + let intervalMessage; + + if (interval) { + const periodicRefreshOptions = + dashboardInfo.common?.conf?.DASHBOARD_AUTO_REFRESH_INTERVALS; + const predefinedValue = periodicRefreshOptions.find( + option => Number(option[0]) === interval / 1000, + ); - this.handleChangeText = this.handleChangeText.bind(this); - this.handleCtrlZ = this.handleCtrlZ.bind(this); - this.handleCtrlY = this.handleCtrlY.bind(this); - this.toggleEditMode = this.toggleEditMode.bind(this); - this.forceRefresh = this.forceRefresh.bind(this); - this.startPeriodicRender = this.startPeriodicRender.bind(this); - this.overwriteDashboard = this.overwriteDashboard.bind(this); - this.showPropertiesModal = this.showPropertiesModal.bind(this); - this.hidePropertiesModal = this.hidePropertiesModal.bind(this); - this.setIsDropdownVisible = this.setIsDropdownVisible.bind(this); - } + if (predefinedValue) { + intervalMessage = t(predefinedValue[1]); + } else { + intervalMessage = moment.duration(interval, 'millisecond').humanize(); + } + } - componentDidMount() { - const { refreshFrequency } = this.props; - this.startPeriodicRender(refreshFrequency * 1000); - } + const fetchCharts = (charts, force = false) => + boundActionCreators.fetchCharts( + charts, + force, + interval * 0.2, + dashboardInfo.id, + ); - componentDidUpdate(prevProps) { - if (this.props.refreshFrequency !== prevProps.refreshFrequency) { - const { refreshFrequency } = this.props; - this.startPeriodicRender(refreshFrequency * 1000); - } - } + const periodicRender = () => { + const { metadata } = dashboardInfo; + const immune = metadata.timed_refresh_immune_slices || []; + const affectedCharts = chartIds.filter( + chartId => immune.indexOf(chartId) === -1, + ); - UNSAFE_componentWillReceiveProps(nextProps) { - if ( - UNDO_LIMIT - nextProps.undoLength <= 0 && - !this.state.didNotifyMaxUndoHistoryToast - ) { - this.setState(() => ({ didNotifyMaxUndoHistoryToast: true })); - this.props.maxUndoHistoryToast(); + boundActionCreators.logEvent(LOG_ACTIONS_PERIODIC_RENDER_DASHBOARD, { + interval, + chartCount: affectedCharts.length, + }); + boundActionCreators.addWarningToast( + t( + `This dashboard is currently auto refreshing; the next auto refresh will be in %s.`, + intervalMessage, + ), + ); + if ( + dashboardInfo.common?.conf?.DASHBOARD_AUTO_REFRESH_MODE === 'fetch' + ) { + // force-refresh while auto-refresh in dashboard + return fetchCharts(affectedCharts); + } + return fetchCharts(affectedCharts, true); + }; + + refreshTimer.current = setPeriodicRunner({ + interval, + periodicRender, + refreshTimer: refreshTimer.current, + }); + }, + [boundActionCreators, chartIds, dashboardInfo], + ); + + useEffect(() => { + startPeriodicRender(refreshFrequency * 1000); + }, [refreshFrequency, startPeriodicRender]); + + useEffect(() => { + if (UNDO_LIMIT - undoLength <= 0 && !didNotifyMaxUndoHistoryToast) { + setDidNotifyMaxUndoHistoryToast(true); + boundActionCreators.maxUndoHistoryToast(); } - if ( - nextProps.undoLength > UNDO_LIMIT && - !this.props.maxUndoHistoryExceeded - ) { - this.props.setMaxUndoHistoryExceeded(); + if (undoLength > UNDO_LIMIT && !maxUndoHistoryExceeded) { + boundActionCreators.setMaxUndoHistoryExceeded(); } - } - - componentWillUnmount() { - stopPeriodicRender(this.refreshTimer); - this.props.setRefreshFrequency(0); - clearTimeout(this.ctrlYTimeout); - clearTimeout(this.ctrlZTimeout); - } - - handleChangeText(nextText) { - const { updateDashboardTitle, onChange } = this.props; - if (nextText && this.props.dashboardTitle !== nextText) { - updateDashboardTitle(nextText); - onChange(); + }, [ + boundActionCreators, + didNotifyMaxUndoHistoryToast, + maxUndoHistoryExceeded, + undoLength, + ]); + + useEffect( + () => () => { + stopPeriodicRender(refreshTimer.current); + boundActionCreators.setRefreshFrequency(0); + clearTimeout(ctrlYTimeout.current); + clearTimeout(ctrlZTimeout.current); + }, + [boundActionCreators], + ); + + const handleChangeText = useCallback( + nextText => { + if (nextText && dashboardTitle !== nextText) { + boundActionCreators.updateDashboardTitle(nextText); + boundActionCreators.onChange(); + } + }, + [boundActionCreators, dashboardTitle], + ); + + const setDropdownVisible = useCallback(visible => { + setIsDropdownVisible(visible); + }, []); + + const handleCtrlY = useCallback(() => { + boundActionCreators.onRedo(); + setEmphasizeRedo(true); + if (ctrlYTimeout.current) { + clearTimeout(ctrlYTimeout.current); } - } - - setIsDropdownVisible(visible) { - this.setState({ - isDropdownVisible: visible, - }); - } - - handleCtrlY() { - this.props.onRedo(); - this.setState({ emphasizeRedo: true }, () => { - if (this.ctrlYTimeout) clearTimeout(this.ctrlYTimeout); - this.ctrlYTimeout = setTimeout(() => { - this.setState({ emphasizeRedo: false }); - }, 100); - }); - } - - handleCtrlZ() { - this.props.onUndo(); - this.setState({ emphasizeUndo: true }, () => { - if (this.ctrlZTimeout) clearTimeout(this.ctrlZTimeout); - this.ctrlZTimeout = setTimeout(() => { - this.setState({ emphasizeUndo: false }); - }, 100); - }); - } - - forceRefresh() { - if (!this.props.isLoading) { - const chartList = Object.keys(this.props.charts); - this.props.logEvent(LOG_ACTIONS_FORCE_REFRESH_DASHBOARD, { + ctrlYTimeout.current = setTimeout(() => { + setEmphasizeRedo(false); + }, 100); + }, [boundActionCreators]); + + const handleCtrlZ = useCallback(() => { + boundActionCreators.onUndo(); + setEmphasizeUndo(true); + if (ctrlZTimeout.current) { + clearTimeout(ctrlZTimeout.current); + } + ctrlZTimeout.current = setTimeout(() => { + setEmphasizeUndo(false); + }, 100); + }, [boundActionCreators]); + + const forceRefresh = useCallback(() => { + if (!isLoading) { + boundActionCreators.logEvent(LOG_ACTIONS_FORCE_REFRESH_DASHBOARD, { force: true, interval: 0, - chartCount: chartList.length, + chartCount: chartIds.length, }); - return this.props.onRefresh( - chartList, - true, - 0, - this.props.dashboardInfo.id, - ); + return boundActionCreators.onRefresh(chartIds, true, 0, dashboardInfo.id); } return false; - } - - startPeriodicRender(interval) { - let intervalMessage; - - if (interval) { - const { dashboardInfo } = this.props; - const periodicRefreshOptions = - dashboardInfo.common?.conf?.DASHBOARD_AUTO_REFRESH_INTERVALS; - const predefinedValue = periodicRefreshOptions.find( - option => Number(option[0]) === interval / 1000, - ); - - if (predefinedValue) { - intervalMessage = t(predefinedValue[1]); - } else { - intervalMessage = moment.duration(interval, 'millisecond').humanize(); - } - } - - const periodicRender = () => { - const { fetchCharts, logEvent, charts, dashboardInfo } = this.props; - const { metadata } = dashboardInfo; - const immune = metadata.timed_refresh_immune_slices || []; - const affectedCharts = Object.values(charts) - .filter(chart => immune.indexOf(chart.id) === -1) - .map(chart => chart.id); + }, [boundActionCreators, chartIds, dashboardInfo.id, isLoading]); - logEvent(LOG_ACTIONS_PERIODIC_RENDER_DASHBOARD, { - interval, - chartCount: affectedCharts.length, - }); - this.props.addWarningToast( - t( - `This dashboard is currently auto refreshing; the next auto refresh will be in %s.`, - intervalMessage, - ), - ); - if (dashboardInfo.common.conf.DASHBOARD_AUTO_REFRESH_MODE === 'fetch') { - // force-refresh while auto-refresh in dashboard - return fetchCharts( - affectedCharts, - false, - interval * 0.2, - dashboardInfo.id, - ); - } - return fetchCharts( - affectedCharts, - true, - interval * 0.2, - dashboardInfo.id, - ); - }; - - this.refreshTimer = setPeriodicRunner({ - interval, - periodicRender, - refreshTimer: this.refreshTimer, + const toggleEditMode = useCallback(() => { + boundActionCreators.logEvent(LOG_ACTIONS_TOGGLE_EDIT_DASHBOARD, { + edit_mode: !editMode, }); - } - - toggleEditMode() { - this.props.logEvent(LOG_ACTIONS_TOGGLE_EDIT_DASHBOARD, { - edit_mode: !this.props.editMode, - }); - this.props.setEditMode(!this.props.editMode); - } - - overwriteDashboard() { - const { - dashboardTitle, - layout: positions, - colorScheme, - colorNamespace, - customCss, - dashboardInfo, - refreshFrequency: currentRefreshFrequency, - shouldPersistRefreshFrequency, - lastModifiedTime, - slug, - } = this.props; - - // check refresh frequency is for current session or persist - const refreshFrequency = shouldPersistRefreshFrequency - ? currentRefreshFrequency - : dashboardInfo.metadata?.refresh_frequency; + boundActionCreators.setEditMode(!editMode); + }, [boundActionCreators, editMode]); + const overwriteDashboard = useCallback(() => { const currentColorNamespace = dashboardInfo?.metadata?.color_namespace || colorNamespace; const currentColorScheme = @@ -383,7 +401,7 @@ class Header extends PureComponent { certification_details: dashboardInfo.certification_details, css: customCss, dashboard_title: dashboardTitle, - last_modified_time: lastModifiedTime, + last_modified_time: actualLastModifiedTime, owners: dashboardInfo.owners, roles: dashboardInfo.roles, slug, @@ -391,50 +409,71 @@ class Header extends PureComponent { ...dashboardInfo?.metadata, color_namespace: currentColorNamespace, color_scheme: currentColorScheme, - positions, - refresh_frequency: refreshFrequency, + positions: layout, + refresh_frequency: shouldPersistRefreshFrequency + ? refreshFrequency + : dashboardInfo.metadata?.refresh_frequency, }, }; // make sure positions data less than DB storage limitation: - const positionJSONLength = safeStringify(positions).length; + const positionJSONLength = safeStringify(layout).length; const limit = - dashboardInfo.common.conf.SUPERSET_DASHBOARD_POSITION_DATA_LIMIT || + dashboardInfo.common?.conf?.SUPERSET_DASHBOARD_POSITION_DATA_LIMIT || DASHBOARD_POSITION_DATA_LIMIT; if (positionJSONLength >= limit) { - this.props.addDangerToast( + boundActionCreators.addDangerToast( t( 'Your dashboard is too large. Please reduce its size before saving it.', ), ); } else { if (positionJSONLength >= limit * 0.9) { - this.props.addWarningToast('Your dashboard is near the size limit.'); + boundActionCreators.addWarningToast( + t('Your dashboard is near the size limit.'), + ); } - this.props.onSave(data, dashboardInfo.id, SAVE_TYPE_OVERWRITE); + boundActionCreators.onSave(data, dashboardInfo.id, SAVE_TYPE_OVERWRITE); } - } - - showPropertiesModal() { - this.setState({ showingPropertiesModal: true }); - } - - hidePropertiesModal() { - this.setState({ showingPropertiesModal: false }); - } - - showEmbedModal = () => { - this.setState({ showingEmbedModal: true }); - }; - - hideEmbedModal = () => { - this.setState({ showingEmbedModal: false }); - }; - - getMetadataItems = () => { - const { dashboardInfo } = this.props; - return [ + }, [ + actualLastModifiedTime, + boundActionCreators, + colorNamespace, + colorScheme, + customCss, + dashboardInfo.certification_details, + dashboardInfo.certified_by, + dashboardInfo.common?.conf?.SUPERSET_DASHBOARD_POSITION_DATA_LIMIT, + dashboardInfo.id, + dashboardInfo.metadata, + dashboardInfo.owners, + dashboardInfo.roles, + dashboardTitle, + layout, + refreshFrequency, + shouldPersistRefreshFrequency, + slug, + ]); + + const showPropertiesModal = useCallback(() => { + setShowingPropertiesModal(true); + }, []); + + const hidePropertiesModal = useCallback(() => { + setShowingPropertiesModal(false); + }, []); + + const showEmbedModal = useCallback(() => { + setShowingEmbedModal(true); + }, []); + + const hideEmbedModal = useCallback(() => { + setShowingEmbedModal(false); + }, []); + + const getMetadataItems = useCallback( + () => [ { type: MetadataType.LastModified, value: dashboardInfo.changed_on_delta_humanized, @@ -450,57 +489,33 @@ class Header extends PureComponent { : t('None'), createdOn: dashboardInfo.created_on_delta_humanized, }, - ]; - }; - - render() { - const { - dashboardTitle, - layout, - expandedSlices, - customCss, - colorNamespace, - dataMask, - setUnsavedChanges, - colorScheme, - onUndo, - onRedo, - undoLength, - redoLength, - onChange, - onSave, - updateCss, - editMode, - isPublished, - user, - dashboardInfo, - hasUnsavedChanges, - isLoading, - refreshFrequency, - shouldPersistRefreshFrequency, - setRefreshFrequency, - lastModifiedTime, - logEvent, - } = this.props; - - const userCanEdit = - dashboardInfo.dash_edit_perm && !dashboardInfo.is_managed_externally; - const userCanShare = dashboardInfo.dash_share_perm; - const userCanSaveAs = dashboardInfo.dash_save_perm; - const userCanCurate = - isFeatureEnabled(FeatureFlag.EmbeddedSuperset) && - findPermission('can_set_embedded', 'Dashboard', user.roles); - const refreshLimit = - dashboardInfo.common?.conf?.SUPERSET_DASHBOARD_PERIODICAL_REFRESH_LIMIT; - const refreshWarning = - dashboardInfo.common?.conf - ?.SUPERSET_DASHBOARD_PERIODICAL_REFRESH_WARNING_MESSAGE; - const isEmbedded = !dashboardInfo?.userId; - - const handleOnPropertiesChange = updates => { - const { dashboardInfoChanged, dashboardTitleChanged } = this.props; - - dashboardInfoChanged({ + ], + [ + dashboardInfo.changed_by, + dashboardInfo.changed_on_delta_humanized, + dashboardInfo.created_by, + dashboardInfo.created_on_delta_humanized, + dashboardInfo.owners, + ], + ); + + const userCanEdit = + dashboardInfo.dash_edit_perm && !dashboardInfo.is_managed_externally; + const userCanShare = dashboardInfo.dash_share_perm; + const userCanSaveAs = dashboardInfo.dash_save_perm; + const userCanCurate = + isFeatureEnabled(FeatureFlag.EmbeddedSuperset) && + findPermission('can_set_embedded', 'Dashboard', user.roles); + const refreshLimit = + dashboardInfo.common?.conf?.SUPERSET_DASHBOARD_PERIODICAL_REFRESH_LIMIT; + const refreshWarning = + dashboardInfo.common?.conf + ?.SUPERSET_DASHBOARD_PERIODICAL_REFRESH_WARNING_MESSAGE; + const isEmbedded = !dashboardInfo?.userId; + + const handleOnPropertiesChange = useCallback( + updates => { + boundActionCreators.dashboardInfoChanged({ slug: updates.slug, metadata: JSON.parse(updates.jsonMetadata || '{}'), certified_by: updates.certifiedBy, @@ -508,239 +523,327 @@ class Header extends PureComponent { owners: updates.owners, roles: updates.roles, }); - setUnsavedChanges(true); - dashboardTitleChanged(updates.title); - }; - - const NavExtension = extensionsRegistry.get('dashboard.nav.right'); - - return ( -
- - ), - !editMode && !isEmbedded && ( - - ), - ]} - rightPanelAdditionalItems={ -
- {userCanSaveAs && ( -
- {editMode && ( -
-
- - - - - - - - - - -
- - -
- )} -
- )} - {editMode ? ( - - ) : ( -
- {NavExtension && } - {userCanEdit && ( - - )} + + +
- )} -
- } - menuDropdownProps={{ - getPopupContainer: triggerNode => - triggerNode.closest('.header-with-actions'), - visible: this.state.isDropdownVisible, - onVisibleChange: this.setIsDropdownVisible, - }} - additionalActionsMenu={ - - } - showFaveStar={user?.userId && dashboardInfo?.id} - showTitlePanelItems - /> - {this.state.showingPropertiesModal && ( - + + +
+ )} + )} - - - - {userCanCurate && ( - + {editMode ? ( + + ) : ( +
+ {NavExtension && } + {userCanEdit && ( + + )} +
)} - - ); - } -} + ), + [ + NavExtension, + boundActionCreators.onRedo, + boundActionCreators.onUndo, + editMode, + emphasizeRedo, + emphasizeUndo, + handleCtrlY, + handleCtrlZ, + hasUnsavedChanges, + overwriteDashboard, + redoLength, + toggleEditMode, + undoLength, + userCanEdit, + userCanSaveAs, + ], + ); + + const menuDropdownProps = useMemo( + () => ({ + getPopupContainer: triggerNode => + triggerNode.closest('.header-with-actions'), + visible: isDropdownVisible, + onVisibleChange: setDropdownVisible, + }), + [isDropdownVisible, setDropdownVisible], + ); + + const additionalActionsMenu = useMemo( + () => ( + + ), + [ + actualLastModifiedTime, + boundActionCreators.addDangerToast, + boundActionCreators.addSuccessToast, + boundActionCreators.logEvent, + boundActionCreators.onChange, + boundActionCreators.onSave, + boundActionCreators.setRefreshFrequency, + boundActionCreators.updateCss, + colorNamespace, + colorScheme, + customCss, + dashboardInfo, + dashboardTitle, + dataMask, + editMode, + expandedSlices, + forceRefresh, + hasUnsavedChanges, + isDropdownVisible, + isLoading, + layout, + refreshFrequency, + refreshLimit, + refreshWarning, + setDropdownVisible, + shouldPersistRefreshFrequency, + showEmbedModal, + showPropertiesModal, + startPeriodicRender, + userCanCurate, + userCanEdit, + userCanSaveAs, + userCanShare, + ], + ); + + return ( +
+ + {showingPropertiesModal && ( + + )} -Header.propTypes = propTypes; -Header.defaultProps = defaultProps; + + + {userCanCurate && ( + + )} + +
+ ); +}; export default Header; diff --git a/superset-frontend/src/dashboard/containers/DashboardHeader.jsx b/superset-frontend/src/dashboard/containers/DashboardHeader.jsx deleted file mode 100644 index cc05916dccdee..0000000000000 --- a/superset-frontend/src/dashboard/containers/DashboardHeader.jsx +++ /dev/null @@ -1,137 +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. - */ -import { bindActionCreators } from 'redux'; -import { connect } from 'react-redux'; - -import { updateDataMask } from 'src/dataMask/actions'; -import DashboardHeader from 'src/dashboard/components/Header'; -import isDashboardLoading from 'src/dashboard/util/isDashboardLoading'; - -import { dashboardInfoChanged } from 'src/dashboard/actions/dashboardInfo'; - -import { - setEditMode, - showBuilderPane, - fetchFaveStar, - saveFaveStar, - savePublished, - setColorScheme, - setUnsavedChanges, - fetchCharts, - updateCss, - onChange, - saveDashboardRequest, - setMaxUndoHistoryExceeded, - maxUndoHistoryToast, - setRefreshFrequency, - onRefresh, -} from 'src/dashboard/actions/dashboardState'; - -import { - undoLayoutAction, - redoLayoutAction, - updateDashboardTitle, - dashboardTitleChanged, -} from 'src/dashboard/actions/dashboardLayout'; -import { - addSuccessToast, - addDangerToast, - addWarningToast, -} from 'src/components/MessageToasts/actions'; - -import { logEvent } from 'src/logger/actions'; -import { DASHBOARD_HEADER_ID } from 'src/dashboard/util/constants'; - -function mapStateToProps({ - dashboardLayout: undoableLayout, - dashboardState, - reports, - dashboardInfo, - charts, - dataMask, - user, -}) { - return { - dashboardInfo, - undoLength: undoableLayout.past.length, - redoLength: undoableLayout.future.length, - layout: undoableLayout.present, - dashboardTitle: ( - (undoableLayout.present[DASHBOARD_HEADER_ID] || {}).meta || {} - ).text, - expandedSlices: dashboardState.expandedSlices, - refreshFrequency: dashboardState.refreshFrequency, - shouldPersistRefreshFrequency: - !!dashboardState.shouldPersistRefreshFrequency, - customCss: dashboardState.css, - colorNamespace: dashboardState.colorNamespace, - colorScheme: dashboardState.colorScheme, - charts, - dataMask, - user, - isStarred: !!dashboardState.isStarred, - isPublished: !!dashboardState.isPublished, - isLoading: isDashboardLoading(charts), - hasUnsavedChanges: !!dashboardState.hasUnsavedChanges, - maxUndoHistoryExceeded: !!dashboardState.maxUndoHistoryExceeded, - lastModifiedTime: Math.max( - dashboardState.lastModifiedTime, - dashboardInfo.last_modified_time, - ), - editMode: !!dashboardState.editMode, - slug: dashboardInfo.slug, - metadata: dashboardInfo.metadata, - reports, - }; -} - -function mapDispatchToProps(dispatch) { - return bindActionCreators( - { - addSuccessToast, - addDangerToast, - addWarningToast, - onUndo: undoLayoutAction, - onRedo: redoLayoutAction, - setEditMode, - showBuilderPane, - setColorScheme, - setUnsavedChanges, - fetchFaveStar, - saveFaveStar, - savePublished, - fetchCharts, - updateDashboardTitle, - updateCss, - onChange, - onSave: saveDashboardRequest, - setMaxUndoHistoryExceeded, - maxUndoHistoryToast, - logEvent, - setRefreshFrequency, - onRefresh, - dashboardInfoChanged, - dashboardTitleChanged, - updateDataMask, - }, - dispatch, - ); -} - -export default connect(mapStateToProps, mapDispatchToProps)(DashboardHeader);