diff --git a/packages/grafana-ui/src/components/Modal/ModalsContext.tsx b/packages/grafana-ui/src/components/Modal/ModalsContext.tsx index 7445a8ecdba..1af225f0487 100644 --- a/packages/grafana-ui/src/components/Modal/ModalsContext.tsx +++ b/packages/grafana-ui/src/components/Modal/ModalsContext.tsx @@ -7,7 +7,7 @@ interface ModalsContextState { hideModal: () => void; } -const ModalsContext = React.createContext({ +export const ModalsContext = React.createContext({ component: null, props: {}, showModal: () => {}, diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index afd45c7cc6b..3deb48b17f1 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -62,7 +62,7 @@ export { Modal } from './Modal/Modal'; export { ModalHeader } from './Modal/ModalHeader'; export { ModalTabsHeader } from './Modal/ModalTabsHeader'; 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'; // Renderless diff --git a/public/app/features/dashboard/components/DashboardPrompt/DashboardPrompt.tsx b/public/app/features/dashboard/components/DashboardPrompt/DashboardPrompt.tsx index 1a273bab2e4..b6823e765e3 100644 --- a/public/app/features/dashboard/components/DashboardPrompt/DashboardPrompt.tsx +++ b/public/app/features/dashboard/components/DashboardPrompt/DashboardPrompt.tsx @@ -1,7 +1,6 @@ import { locationService } from '@grafana/runtime'; -import { appEvents } from 'app/core/core'; 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 { DashboardModel } from '../../state/DashboardModel'; 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 { useDispatch } from 'react-redux'; 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'; export interface Props { @@ -20,19 +21,13 @@ export interface Props { interface State { original: object | null; originalPath?: string; - modal: PromptModal | null; - blockedLocation?: H.Location | null; -} - -enum PromptModal { - UnsavedChangesModal, - SaveLibraryPanelModal, } export const DashboardPrompt = React.memo(({ dashboard }: Props) => { - const [state, setState] = useState({ original: null, modal: null }); + const [state, setState] = useState({ original: null }); const dispatch = useDispatch(); - const { original, originalPath, blockedLocation, modal } = state; + const { original, originalPath } = state; + const { showModal, hideModal } = useContext(ModalsContext); useEffect(() => { // 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 originalPath = locationService.getLocation().pathname; const original = dashboard.getSaveModelClone(); - - setState({ originalPath, original, modal: null }); + setState({ originalPath, original }); }, 1000); + const savedEventUnsub = appEvents.subscribe(DashboardSavedEvent, () => { + const original = dashboard.getSaveModelClone(); + setState({ originalPath, original }); + }); + return () => { clearTimeout(timeoutId); + savedEventUnsub.unsubscribe(); }; - }, [dashboard]); + }, [dashboard, originalPath]); useEffect(() => { const handleUnload = (event: BeforeUnloadEvent) => { @@ -65,28 +65,27 @@ export const DashboardPrompt = React.memo(({ dashboard }: Props) => { return () => window.removeEventListener('beforeunload', handleUnload); }, [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 panelInEdit = dashboard.panelInEdit; const search = new URLSearchParams(location.search); // Are we leaving panel edit & library panel? 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; } @@ -108,57 +107,31 @@ export const DashboardPrompt = React.memo(({ dashboard }: Props) => { 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; }; - const onHideModalAndMoveToBlockedLocation = () => { - setState({ ...state, modal: null }); - moveToBlockedLocationAfterReactStateUpdate(blockedLocation); - }; - - return ( - <> - - {modal === PromptModal.UnsavedChangesModal && ( - {}} // 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 && ( - { - dispatch(discardPanelChanges()); - setState({ ...state, modal: null }); - moveToBlockedLocationAfterReactStateUpdate(blockedLocation); - }} - onDismiss={() => { - setState({ ...state, modal: null, blockedLocation: null }); - }} - /> - )} - - ); + return ; }); DashboardPrompt.displayName = 'DashboardPrompt'; function moveToBlockedLocationAfterReactStateUpdate(location?: H.Location | null) { if (location) { - setTimeout(() => locationService.push(location!), 10); + setTimeout(() => locationService.push(location), 10); } } diff --git a/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx b/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx index 6d6aa3b3ab8..2e31a65ed96 100644 --- a/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx +++ b/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx @@ -4,7 +4,6 @@ import { css, cx } from '@emotion/css'; import { Button, CustomScrollbar, Icon, IconName, PageToolbar, stylesFactory, useForceUpdate } from '@grafana/ui'; import config from 'app/core/config'; import { contextSrv } from 'app/core/services/context_srv'; -import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher'; import { DashboardModel } from '../../state/DashboardModel'; import { SaveDashboardAsButton, SaveDashboardButton } from '../SaveDashboard/SaveDashboardButton'; import { VariableEditorContainer } from '../../../variables/editor/VariableEditorContainer'; @@ -131,7 +130,6 @@ export function DashboardSettings({ dashboard, editview }: Props) { const onPostSave = () => { dashboard.meta.hasUnsavedFolderChange = false; - dashboardWatcher.reloadPage(); }; const folderTitle = dashboard.meta.folderTitle; diff --git a/public/app/features/dashboard/components/DashboardSettings/JsonEditorSettings.tsx b/public/app/features/dashboard/components/DashboardSettings/JsonEditorSettings.tsx index fd1219759a3..e5da865a634 100644 --- a/public/app/features/dashboard/components/DashboardSettings/JsonEditorSettings.tsx +++ b/public/app/features/dashboard/components/DashboardSettings/JsonEditorSettings.tsx @@ -16,6 +16,7 @@ export const JsonEditorSettings: React.FC = ({ dashboard }) => { const onBlur = (value: string) => { setDashboardJson(value); }; + const onClick = () => { getDashboardSrv() .saveJSONDashboard(dashboardJson) @@ -23,6 +24,7 @@ export const JsonEditorSettings: React.FC = ({ dashboard }) => { dashboardWatcher.reloadPage(); }); }; + const styles = useStyles2(getStyles); return ( diff --git a/public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardForm.tsx b/public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardForm.tsx index 3807609231c..2e9a66d5702 100644 --- a/public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardForm.tsx +++ b/public/app/features/dashboard/components/SaveDashboard/forms/SaveDashboardForm.tsx @@ -51,8 +51,9 @@ export const SaveDashboardForm = ({ dashboard.resetOriginalTime(); } onSuccess(); + } else { + setSaving(false); } - setSaving(false); }} > {({ register, errors }) => ( @@ -96,7 +97,7 @@ export const SaveDashboardForm = ({ icon={saving ? 'fa fa-spinner' : undefined} aria-label={selectors.pages.SaveDashboardModal.save} > - {saving ? '' : 'Save'} + Save {!saveModel.hasChanges &&
No changes to save
}