From 22d5efba25f19b77a8c0b7e5aedb44a1135c865b Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Fri, 18 Oct 2024 07:49:25 +0300 Subject: [PATCH] Routing: Replace Prompt component (#94675) * Add custom Prompt component * Add test * Remove beforeunload handling * Updates * Use custom Prompt in CorrelationEditorModeBar.tsx * Simplify component * Update DashboardPrompt * Simplify Prompt * Update * Update DashboardPrompt.tsx * Update type * Update tests --- .../core/components/FormPrompt/FormPrompt.tsx | 3 +- .../components/FormPrompt/Prompt.test.tsx | 57 +++++++++++++++++++ .../app/core/components/FormPrompt/Prompt.tsx | 27 +++++++++ .../saving/DashboardPrompt.tsx | 2 +- .../DashboardPrompt/DashboardPrompt.tsx | 2 +- .../explore/CorrelationEditorModeBar.tsx | 8 +-- 6 files changed, 92 insertions(+), 7 deletions(-) create mode 100644 public/app/core/components/FormPrompt/Prompt.test.tsx create mode 100644 public/app/core/components/FormPrompt/Prompt.tsx 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 {