import { css } from '@emotion/css';
import * as H from 'history';
import { memo, useContext, useEffect, useMemo } from 'react';
import { locationService } from '@grafana/runtime';
import { ModalsContext, Modal, Button, useStyles2 } from '@grafana/ui';
import { Prompt } from 'app/core/components/FormPrompt/Prompt';
import { contextSrv } from 'app/core/services/context_srv';
import { SaveLibraryVizPanelModal } from '../panel-edit/SaveLibraryVizPanelModal';
import { DashboardScene } from '../scene/DashboardScene';
import { getLibraryPanelBehavior, isLibraryPanel } from '../utils/utils';
interface DashboardPromptProps {
dashboard: DashboardScene;
}
export const DashboardPrompt = memo(({ dashboard }: DashboardPromptProps) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
const originalPath = useMemo(() => locationService.getLocation().pathname, [dashboard]);
const { showModal, hideModal } = useContext(ModalsContext);
useEffect(() => {
const handleUnload = (event: BeforeUnloadEvent) => {
if (ignoreChanges(dashboard)) {
return;
}
if (dashboard.state.isDirty) {
event.preventDefault();
// No browser actually displays this message anymore.
// But Chrome requires it to be defined else the popup won't show.
event.returnValue = '';
}
};
window.addEventListener('beforeunload', handleUnload);
return () => window.removeEventListener('beforeunload', handleUnload);
}, [dashboard]);
const onHistoryBlock = (location: H.Location) => {
const panelEditor = dashboard.state.editPanel;
const vizPanel = panelEditor?.getPanel();
const search = new URLSearchParams(location.search);
// Are we leaving panel edit & library panel?
if (panelEditor && vizPanel && isLibraryPanel(vizPanel) && panelEditor.state.isDirty && !search.has('editPanel')) {
const libPanelBehavior = getLibraryPanelBehavior(vizPanel);
showModal(SaveLibraryVizPanelModal, {
dashboard,
isUnsavedPrompt: true,
libraryPanel: libPanelBehavior!,
onConfirm: () => {
panelEditor.onConfirmSaveLibraryPanel();
hideModal();
moveToBlockedLocationAfterReactStateUpdate(location);
},
onDiscard: () => {
panelEditor.onDiscard();
hideModal();
moveToBlockedLocationAfterReactStateUpdate(location);
},
onDismiss: hideModal,
});
return false;
}
// Are we still on the same dashboard?
if (originalPath === location.pathname) {
return true;
}
if (ignoreChanges(dashboard)) {
return true;
}
if (!dashboard.state.isDirty) {
return true;
}
showModal(UnsavedChangesModal, {
dashboard,
onSaveDashboardClick: () => {
hideModal();
dashboard.openSaveDrawer({
onSaveSuccess: () => {
moveToBlockedLocationAfterReactStateUpdate(location);
},
});
},
onDiscard: () => {
dashboard.exitEditMode({ skipConfirm: true });
hideModal();
moveToBlockedLocationAfterReactStateUpdate(location);
},
onDismiss: hideModal,
});
return false;
};
return ;
});
DashboardPrompt.displayName = 'DashboardPrompt';
function moveToBlockedLocationAfterReactStateUpdate(location?: H.Location | null) {
if (location) {
setTimeout(() => locationService.push(location), 10);
}
}
interface UnsavedChangesModalProps {
onDiscard: () => void;
onDismiss: () => void;
onSaveDashboardClick?: () => void;
}
export const UnsavedChangesModal = ({ onDiscard, onDismiss, onSaveDashboardClick }: UnsavedChangesModalProps) => {
const styles = useStyles2(getStyles);
return (
Do you want to save your changes?
);
};
const getStyles = () => ({
modal: css({
width: '500px',
}),
});
/**
* For some dashboards and users changes should be ignored *
*/
export function ignoreChanges(scene: DashboardScene | null) {
const original = scene?.getInitialSaveModel();
if (!original) {
return true;
}
// Ignore changes if original is unsaved
if (scene?.state.meta.version === 0) {
return true;
}
// Ignore changes if the user has been signed out
if (!contextSrv.isSignedIn) {
return true;
}
if (!scene) {
return true;
}
const { canSave, fromScript, fromFile } = scene.state.meta;
if (!contextSrv.isEditor && !canSave) {
return true;
}
return !canSave || fromScript || fromFile;
}