grafana/public/app/features/dashboard-scene/saving/DashboardPrompt.tsx

177 lines
4.8 KiB
TypeScript

import { css } from '@emotion/css';
import * as H from 'history';
import React, { useContext, useEffect, useMemo } from 'react';
import { Prompt } from 'react-router';
import { locationService } from '@grafana/runtime';
import { Dashboard } from '@grafana/schema/dist/esm/index.gen';
import { ModalsContext, Modal, Button, useStyles2 } from '@grafana/ui';
import { contextSrv } from 'app/core/services/context_srv';
import { DashboardScene } from '../scene/DashboardScene';
interface DashboardPromptProps {
dashboard: DashboardScene;
}
export const DashboardPrompt = React.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, dashboard.getInitialSaveModel())) {
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 panelInEdit = dashboard.state.editPanel;
// const search = new URLSearchParams(location.search);
// TODO: Are we leaving panel edit & library panel?
// if (panelInEdit && panelInEdit.libraryPanel && panelInEdit.hasChanged && !search.has('editPanel')) {
// showModal(SaveLibraryPanelModal, {
// isUnsavedPrompt: true,
// panel: dashboard.panelInEdit as PanelModelWithLibraryPanel,
// folderUid: dashboard.meta.folderUid ?? '',
// onConfirm: () => {
// hideModal();
// moveToBlockedLocationAfterReactStateUpdate(location);
// },
// onDiscard: () => {
// dispatch(discardPanelChanges());
// moveToBlockedLocationAfterReactStateUpdate(location);
// hideModal();
// },
// onDismiss: hideModal,
// });
// return false;
// }
// Are we still on the same dashboard?
if (originalPath === location.pathname) {
return true;
}
if (ignoreChanges(dashboard, dashboard.getInitialSaveModel())) {
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 <Prompt when={true} message={onHistoryBlock} />;
});
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 (
<Modal
isOpen={true}
title="Unsaved changes"
onDismiss={onDismiss}
icon="exclamation-triangle"
className={styles.modal}
>
<h5>Do you want to save your changes?</h5>
<Modal.ButtonRow>
<Button variant="secondary" onClick={onDismiss} fill="outline">
Cancel
</Button>
<Button variant="destructive" onClick={onDiscard}>
Discard
</Button>
<Button onClick={onSaveDashboardClick}>Save dashboard</Button>
</Modal.ButtonRow>
</Modal>
);
};
const getStyles = () => ({
modal: css({
width: '500px',
}),
});
/**
* For some dashboards and users changes should be ignored *
*/
export function ignoreChanges(current: DashboardScene | null, original?: Dashboard) {
if (!original) {
return true;
}
// Ignore changes if original is unsaved
if (original.version === 0) {
return true;
}
// Ignore changes if the user has been signed out
if (!contextSrv.isSignedIn) {
return true;
}
if (!current) {
return true;
}
const { canSave, fromScript, fromFile } = current.state.meta;
if (!contextSrv.isEditor && !canSave) {
return true;
}
return !canSave || fromScript || fromFile;
}