Dashboard: Fixes save modal rendered ontop of save drawer (#46916)

* Dashboard: Fixes save modal rendered ontop of save drawer

* removed commented line

* Simplified dismiss -> hideModal mapping

* Fixed issue with new dashboard

* Fixing issues
This commit is contained in:
Torkel Ödegaard 2022-03-25 13:08:42 +01:00 committed by GitHub
parent c00f488f89
commit 3516821012
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 52 additions and 78 deletions

View File

@ -7,7 +7,7 @@ interface ModalsContextState {
hideModal: () => void; hideModal: () => void;
} }
const ModalsContext = React.createContext<ModalsContextState>({ export const ModalsContext = React.createContext<ModalsContextState>({
component: null, component: null,
props: {}, props: {},
showModal: () => {}, showModal: () => {},

View File

@ -62,7 +62,7 @@ export { Modal } from './Modal/Modal';
export { ModalHeader } from './Modal/ModalHeader'; export { ModalHeader } from './Modal/ModalHeader';
export { ModalTabsHeader } from './Modal/ModalTabsHeader'; export { ModalTabsHeader } from './Modal/ModalTabsHeader';
export { ModalTabContent } from './Modal/ModalTabContent'; export { ModalTabContent } from './Modal/ModalTabContent';
export { ModalsProvider, ModalRoot, ModalsController } from './Modal/ModalsContext'; export { ModalsProvider, ModalRoot, ModalsController, ModalsContext } from './Modal/ModalsContext';
export { PageToolbar } from './PageLayout/PageToolbar'; export { PageToolbar } from './PageLayout/PageToolbar';
// Renderless // Renderless

View File

@ -1,7 +1,6 @@
import { locationService } from '@grafana/runtime'; import { locationService } from '@grafana/runtime';
import { appEvents } from 'app/core/core';
import { contextSrv } from 'app/core/services/context_srv'; import { contextSrv } from 'app/core/services/context_srv';
import React, { useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import { Prompt } from 'react-router-dom'; import { Prompt } from 'react-router-dom';
import { DashboardModel } from '../../state/DashboardModel'; import { DashboardModel } from '../../state/DashboardModel';
import { each, filter, find } from 'lodash'; import { each, filter, find } from 'lodash';
@ -11,6 +10,8 @@ import { SaveLibraryPanelModal } from 'app/features/library-panels/components/Sa
import { PanelModelWithLibraryPanel } from 'app/features/library-panels/types'; import { PanelModelWithLibraryPanel } from 'app/features/library-panels/types';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { discardPanelChanges, exitPanelEditor } from '../PanelEditor/state/actions'; import { discardPanelChanges, exitPanelEditor } from '../PanelEditor/state/actions';
import { ModalsContext } from '@grafana/ui';
import { appEvents } from 'app/core/app_events';
import { DashboardSavedEvent } from 'app/types/events'; import { DashboardSavedEvent } from 'app/types/events';
export interface Props { export interface Props {
@ -20,19 +21,13 @@ export interface Props {
interface State { interface State {
original: object | null; original: object | null;
originalPath?: string; originalPath?: string;
modal: PromptModal | null;
blockedLocation?: H.Location | null;
}
enum PromptModal {
UnsavedChangesModal,
SaveLibraryPanelModal,
} }
export const DashboardPrompt = React.memo(({ dashboard }: Props) => { export const DashboardPrompt = React.memo(({ dashboard }: Props) => {
const [state, setState] = useState<State>({ original: null, modal: null }); const [state, setState] = useState<State>({ original: null });
const dispatch = useDispatch(); const dispatch = useDispatch();
const { original, originalPath, blockedLocation, modal } = state; const { original, originalPath } = state;
const { showModal, hideModal } = useContext(ModalsContext);
useEffect(() => { useEffect(() => {
// This timeout delay is to wait for panels to load and migrate scheme before capturing the original state // This timeout delay is to wait for panels to load and migrate scheme before capturing the original state
@ -40,14 +35,19 @@ export const DashboardPrompt = React.memo(({ dashboard }: Props) => {
const timeoutId = setTimeout(() => { const timeoutId = setTimeout(() => {
const originalPath = locationService.getLocation().pathname; const originalPath = locationService.getLocation().pathname;
const original = dashboard.getSaveModelClone(); const original = dashboard.getSaveModelClone();
setState({ originalPath, original });
setState({ originalPath, original, modal: null });
}, 1000); }, 1000);
const savedEventUnsub = appEvents.subscribe(DashboardSavedEvent, () => {
const original = dashboard.getSaveModelClone();
setState({ originalPath, original });
});
return () => { return () => {
clearTimeout(timeoutId); clearTimeout(timeoutId);
savedEventUnsub.unsubscribe();
}; };
}, [dashboard]); }, [dashboard, originalPath]);
useEffect(() => { useEffect(() => {
const handleUnload = (event: BeforeUnloadEvent) => { const handleUnload = (event: BeforeUnloadEvent) => {
@ -65,28 +65,27 @@ export const DashboardPrompt = React.memo(({ dashboard }: Props) => {
return () => window.removeEventListener('beforeunload', handleUnload); return () => window.removeEventListener('beforeunload', handleUnload);
}, [dashboard, original]); }, [dashboard, original]);
// Handle saved events
useEffect(() => {
const savedEventUnsub = appEvents.subscribe(DashboardSavedEvent, () => {
const original = dashboard.getSaveModelClone();
const originalPath = locationService.getLocation().pathname;
setState({ originalPath, original, modal: null });
if (blockedLocation) {
moveToBlockedLocationAfterReactStateUpdate(blockedLocation);
}
});
return () => savedEventUnsub.unsubscribe();
}, [dashboard, blockedLocation]);
const onHistoryBlock = (location: H.Location) => { const onHistoryBlock = (location: H.Location) => {
const panelInEdit = dashboard.panelInEdit; const panelInEdit = dashboard.panelInEdit;
const search = new URLSearchParams(location.search); const search = new URLSearchParams(location.search);
// Are we leaving panel edit & library panel? // Are we leaving panel edit & library panel?
if (panelInEdit && panelInEdit.libraryPanel && panelInEdit.hasChanged && !search.has('editPanel')) { if (panelInEdit && panelInEdit.libraryPanel && panelInEdit.hasChanged && !search.has('editPanel')) {
setState({ ...state, modal: PromptModal.SaveLibraryPanelModal, blockedLocation: location }); showModal(SaveLibraryPanelModal, {
isUnsavedPrompt: true,
panel: dashboard.panelInEdit as PanelModelWithLibraryPanel,
folderId: dashboard.meta.folderId as number,
onConfirm: () => {
hideModal();
moveToBlockedLocationAfterReactStateUpdate(location);
},
onDiscard: () => {
dispatch(discardPanelChanges());
moveToBlockedLocationAfterReactStateUpdate(location);
hideModal();
},
onDismiss: hideModal,
});
return false; return false;
} }
@ -108,57 +107,31 @@ export const DashboardPrompt = React.memo(({ dashboard }: Props) => {
return true; return true;
} }
setState({ ...state, modal: PromptModal.UnsavedChangesModal, blockedLocation: location }); showModal(UnsavedChangesModal, {
dashboard: dashboard,
onSaveSuccess: () => {
hideModal();
moveToBlockedLocationAfterReactStateUpdate(location);
},
onDiscard: () => {
setState({ ...state, original: null });
hideModal();
moveToBlockedLocationAfterReactStateUpdate(location);
},
onDismiss: hideModal,
});
return false; return false;
}; };
const onHideModalAndMoveToBlockedLocation = () => { return <Prompt when={true} message={onHistoryBlock} />;
setState({ ...state, modal: null });
moveToBlockedLocationAfterReactStateUpdate(blockedLocation);
};
return (
<>
<Prompt when={true} message={onHistoryBlock} />
{modal === PromptModal.UnsavedChangesModal && (
<UnsavedChangesModal
dashboard={dashboard}
onSaveSuccess={() => {}} // Handled by DashboardSavedEvent above
onDiscard={() => {
// Clear original will allow us to leave without unsaved changes prompt
setState({ ...state, original: null, modal: null });
moveToBlockedLocationAfterReactStateUpdate(blockedLocation);
}}
onDismiss={() => {
setState({ ...state, modal: null, blockedLocation: null });
}}
/>
)}
{modal === PromptModal.SaveLibraryPanelModal && (
<SaveLibraryPanelModal
isUnsavedPrompt
panel={dashboard.panelInEdit as PanelModelWithLibraryPanel}
folderId={dashboard.meta.folderId as number}
onConfirm={onHideModalAndMoveToBlockedLocation}
onDiscard={() => {
dispatch(discardPanelChanges());
setState({ ...state, modal: null });
moveToBlockedLocationAfterReactStateUpdate(blockedLocation);
}}
onDismiss={() => {
setState({ ...state, modal: null, blockedLocation: null });
}}
/>
)}
</>
);
}); });
DashboardPrompt.displayName = 'DashboardPrompt'; DashboardPrompt.displayName = 'DashboardPrompt';
function moveToBlockedLocationAfterReactStateUpdate(location?: H.Location | null) { function moveToBlockedLocationAfterReactStateUpdate(location?: H.Location | null) {
if (location) { if (location) {
setTimeout(() => locationService.push(location!), 10); setTimeout(() => locationService.push(location), 10);
} }
} }

View File

@ -4,7 +4,6 @@ import { css, cx } from '@emotion/css';
import { Button, CustomScrollbar, Icon, IconName, PageToolbar, stylesFactory, useForceUpdate } from '@grafana/ui'; import { Button, CustomScrollbar, Icon, IconName, PageToolbar, stylesFactory, useForceUpdate } from '@grafana/ui';
import config from 'app/core/config'; import config from 'app/core/config';
import { contextSrv } from 'app/core/services/context_srv'; import { contextSrv } from 'app/core/services/context_srv';
import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher';
import { DashboardModel } from '../../state/DashboardModel'; import { DashboardModel } from '../../state/DashboardModel';
import { SaveDashboardAsButton, SaveDashboardButton } from '../SaveDashboard/SaveDashboardButton'; import { SaveDashboardAsButton, SaveDashboardButton } from '../SaveDashboard/SaveDashboardButton';
import { VariableEditorContainer } from '../../../variables/editor/VariableEditorContainer'; import { VariableEditorContainer } from '../../../variables/editor/VariableEditorContainer';
@ -131,7 +130,6 @@ export function DashboardSettings({ dashboard, editview }: Props) {
const onPostSave = () => { const onPostSave = () => {
dashboard.meta.hasUnsavedFolderChange = false; dashboard.meta.hasUnsavedFolderChange = false;
dashboardWatcher.reloadPage();
}; };
const folderTitle = dashboard.meta.folderTitle; const folderTitle = dashboard.meta.folderTitle;

View File

@ -16,6 +16,7 @@ export const JsonEditorSettings: React.FC<Props> = ({ dashboard }) => {
const onBlur = (value: string) => { const onBlur = (value: string) => {
setDashboardJson(value); setDashboardJson(value);
}; };
const onClick = () => { const onClick = () => {
getDashboardSrv() getDashboardSrv()
.saveJSONDashboard(dashboardJson) .saveJSONDashboard(dashboardJson)
@ -23,6 +24,7 @@ export const JsonEditorSettings: React.FC<Props> = ({ dashboard }) => {
dashboardWatcher.reloadPage(); dashboardWatcher.reloadPage();
}); });
}; };
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
return ( return (

View File

@ -51,8 +51,9 @@ export const SaveDashboardForm = ({
dashboard.resetOriginalTime(); dashboard.resetOriginalTime();
} }
onSuccess(); onSuccess();
} else {
setSaving(false);
} }
setSaving(false);
}} }}
> >
{({ register, errors }) => ( {({ register, errors }) => (
@ -96,7 +97,7 @@ export const SaveDashboardForm = ({
icon={saving ? 'fa fa-spinner' : undefined} icon={saving ? 'fa fa-spinner' : undefined}
aria-label={selectors.pages.SaveDashboardModal.save} aria-label={selectors.pages.SaveDashboardModal.save}
> >
{saving ? '' : 'Save'} Save
</Button> </Button>
{!saveModel.hasChanges && <div>No changes to save</div>} {!saveModel.hasChanges && <div>No changes to save</div>}
</Stack> </Stack>