diff --git a/public/app/core/components/FormPrompt/FormPrompt.tsx b/public/app/core/components/FormPrompt/FormPrompt.tsx
index 7e5edbc4cb7..4ccf58148b2 100644
--- a/public/app/core/components/FormPrompt/FormPrompt.tsx
+++ b/public/app/core/components/FormPrompt/FormPrompt.tsx
@@ -1,11 +1,12 @@
import { css } from '@emotion/css';
import history from 'history';
import { useEffect, useState } from 'react';
-import { Prompt } from 'react-router-dom';
import { Navigate } from 'react-router-dom-v5-compat';
import { Button, Modal } from '@grafana/ui';
+import { Prompt } from './Prompt';
+
export interface Props {
confirmRedirect?: boolean;
onDiscard: () => void;
diff --git a/public/app/core/components/FormPrompt/Prompt.test.tsx b/public/app/core/components/FormPrompt/Prompt.test.tsx
new file mode 100644
index 00000000000..c6a7a6b214a
--- /dev/null
+++ b/public/app/core/components/FormPrompt/Prompt.test.tsx
@@ -0,0 +1,57 @@
+import { History, Location, createMemoryHistory } from 'history';
+import { render } from 'test/test-utils';
+
+import { locationService } from '@grafana/runtime';
+
+import { Prompt } from './Prompt';
+
+jest.mock('@grafana/runtime', () => ({
+ ...jest.requireActual('@grafana/runtime'),
+ locationService: {
+ getLocation: jest.fn(),
+ getHistory: jest.fn(),
+ },
+}));
+
+describe('Prompt component with React Router', () => {
+ let mockHistory: History & { block: jest.Mock };
+
+ beforeEach(() => {
+ const historyInstance = createMemoryHistory({ initialEntries: ['/current'] });
+ mockHistory = {
+ ...historyInstance,
+ block: jest.fn(() => jest.fn()),
+ };
+
+ (locationService.getLocation as jest.Mock).mockReturnValue({ pathname: '/current' } as Location);
+ (locationService.getHistory as jest.Mock).mockReturnValue(mockHistory);
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should call the block function when `when` is true', () => {
+ const { unmount } = render();
+
+ unmount();
+ expect(mockHistory.block).toHaveBeenCalled();
+ });
+
+ it('should not call the block function when `when` is false', () => {
+ const { unmount } = render();
+
+ unmount();
+ expect(mockHistory.block).not.toHaveBeenCalled();
+ });
+
+ it('should use the message function if provided', async () => {
+ const messageFn = jest.fn().mockReturnValue('Custom message');
+ render();
+
+ const callback = mockHistory.block.mock.calls[0][0];
+ callback({ pathname: '/new-path' } as Location);
+
+ expect(messageFn).toHaveBeenCalledWith(expect.objectContaining({ pathname: '/new-path' }));
+ });
+});
diff --git a/public/app/core/components/FormPrompt/Prompt.tsx b/public/app/core/components/FormPrompt/Prompt.tsx
new file mode 100644
index 00000000000..a1557f16f54
--- /dev/null
+++ b/public/app/core/components/FormPrompt/Prompt.tsx
@@ -0,0 +1,27 @@
+import * as H from 'history';
+import { useEffect } from 'react';
+
+import { locationService } from '@grafana/runtime';
+
+interface PromptProps {
+ when?: boolean;
+ message: string | ((location: H.Location) => string | boolean);
+}
+
+export const Prompt = ({ message, when = true }: PromptProps) => {
+ const history = locationService.getHistory();
+
+ useEffect(() => {
+ if (!when) {
+ return undefined;
+ }
+ //@ts-expect-error TODO Update the history package to fix types
+ const unblock = history.block(message);
+
+ return () => {
+ unblock();
+ };
+ }, [when, message, history]);
+
+ return null;
+};
diff --git a/public/app/features/dashboard-scene/saving/DashboardPrompt.tsx b/public/app/features/dashboard-scene/saving/DashboardPrompt.tsx
index 94d1655686c..8c8fb1c4170 100644
--- a/public/app/features/dashboard-scene/saving/DashboardPrompt.tsx
+++ b/public/app/features/dashboard-scene/saving/DashboardPrompt.tsx
@@ -1,11 +1,11 @@
import { css } from '@emotion/css';
import * as H from 'history';
import { memo, 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 { Prompt } from 'app/core/components/FormPrompt/Prompt';
import { contextSrv } from 'app/core/services/context_srv';
import { SaveLibraryVizPanelModal } from '../panel-edit/SaveLibraryVizPanelModal';
diff --git a/public/app/features/dashboard/components/DashboardPrompt/DashboardPrompt.tsx b/public/app/features/dashboard/components/DashboardPrompt/DashboardPrompt.tsx
index ef984a5d9ea..5514f11da67 100644
--- a/public/app/features/dashboard/components/DashboardPrompt/DashboardPrompt.tsx
+++ b/public/app/features/dashboard/components/DashboardPrompt/DashboardPrompt.tsx
@@ -1,12 +1,12 @@
import * as H from 'history';
import { find } from 'lodash';
import { memo, useContext, useEffect, useState } from 'react';
-import { Prompt } from 'react-router-dom';
import { locationService } from '@grafana/runtime';
import { Dashboard } from '@grafana/schema';
import { ModalsContext } from '@grafana/ui';
import { appEvents } from 'app/core/app_events';
+import { Prompt } from 'app/core/components/FormPrompt/Prompt';
import { contextSrv } from 'app/core/services/context_srv';
import { SaveLibraryPanelModal } from 'app/features/library-panels/components/SaveLibraryPanelModal/SaveLibraryPanelModal';
import { PanelModelWithLibraryPanel } from 'app/features/library-panels/types';
diff --git a/public/app/features/explore/CorrelationEditorModeBar.tsx b/public/app/features/explore/CorrelationEditorModeBar.tsx
index 401c56421ea..01edb0f5b0b 100644
--- a/public/app/features/explore/CorrelationEditorModeBar.tsx
+++ b/public/app/features/explore/CorrelationEditorModeBar.tsx
@@ -1,11 +1,11 @@
import { css } from '@emotion/css';
import { useEffect, useState } from 'react';
-import { Prompt } from 'react-router-dom';
import { useBeforeUnload, useUnmount } from 'react-use';
import { GrafanaTheme2, colorManipulator } from '@grafana/data';
import { reportInteraction } from '@grafana/runtime';
import { Button, Icon, Stack, Tooltip, useStyles2 } from '@grafana/ui';
+import { Prompt } from 'app/core/components/FormPrompt/Prompt';
import { CORRELATION_EDITOR_POST_CONFIRM_ACTION, ExploreItemState, useDispatch, useSelector } from 'app/types';
import { CorrelationUnsavedChangesModal } from './CorrelationUnsavedChangesModal';
@@ -175,13 +175,13 @@ export const CorrelationEditorModeBar = ({ panes }: { panes: Array<[string, Expl
return (
<>
- {/* Handle navigating outside of Explore */}
+ {/* Handle navigating outside Explore */}
{
if (
location.pathname !== '/explore' &&
- (correlationDetails?.editorMode || false) &&
- (correlationDetails?.correlationDirty || false)
+ correlationDetails?.editorMode &&
+ correlationDetails?.correlationDirty
) {
return 'You have unsaved correlation data. Continue?';
} else {