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
This commit is contained in:
Alex Khomenko 2024-10-18 07:49:25 +03:00 committed by GitHub
parent 3e1f5559a6
commit 22d5efba25
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 92 additions and 7 deletions

View File

@ -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;

View File

@ -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(<Prompt when={true} message="Are you sure you want to leave?" />);
unmount();
expect(mockHistory.block).toHaveBeenCalled();
});
it('should not call the block function when `when` is false', () => {
const { unmount } = render(<Prompt when={false} message="Are you sure you want to leave?" />);
unmount();
expect(mockHistory.block).not.toHaveBeenCalled();
});
it('should use the message function if provided', async () => {
const messageFn = jest.fn().mockReturnValue('Custom message');
render(<Prompt when={true} message={messageFn} />);
const callback = mockHistory.block.mock.calls[0][0];
callback({ pathname: '/new-path' } as Location);
expect(messageFn).toHaveBeenCalledWith(expect.objectContaining({ pathname: '/new-path' }));
});
});

View File

@ -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;
};

View File

@ -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';

View File

@ -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';

View File

@ -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 */}
<Prompt
message={(location) => {
if (
location.pathname !== '/explore' &&
(correlationDetails?.editorMode || false) &&
(correlationDetails?.correlationDirty || false)
correlationDetails?.editorMode &&
correlationDetails?.correlationDirty
) {
return 'You have unsaved correlation data. Continue?';
} else {