mirror of
https://github.com/grafana/grafana.git
synced 2025-07-31 17:52:20 +08:00
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:
@ -1,11 +1,12 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import history from 'history';
|
import history from 'history';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Prompt } from 'react-router-dom';
|
|
||||||
import { Navigate } from 'react-router-dom-v5-compat';
|
import { Navigate } from 'react-router-dom-v5-compat';
|
||||||
|
|
||||||
import { Button, Modal } from '@grafana/ui';
|
import { Button, Modal } from '@grafana/ui';
|
||||||
|
|
||||||
|
import { Prompt } from './Prompt';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
confirmRedirect?: boolean;
|
confirmRedirect?: boolean;
|
||||||
onDiscard: () => void;
|
onDiscard: () => void;
|
||||||
|
57
public/app/core/components/FormPrompt/Prompt.test.tsx
Normal file
57
public/app/core/components/FormPrompt/Prompt.test.tsx
Normal 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' }));
|
||||||
|
});
|
||||||
|
});
|
27
public/app/core/components/FormPrompt/Prompt.tsx
Normal file
27
public/app/core/components/FormPrompt/Prompt.tsx
Normal 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;
|
||||||
|
};
|
@ -1,11 +1,11 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import * as H from 'history';
|
import * as H from 'history';
|
||||||
import { memo, useContext, useEffect, useMemo } from 'react';
|
import { memo, useContext, useEffect, useMemo } from 'react';
|
||||||
import { Prompt } from 'react-router';
|
|
||||||
|
|
||||||
import { locationService } from '@grafana/runtime';
|
import { locationService } from '@grafana/runtime';
|
||||||
import { Dashboard } from '@grafana/schema/dist/esm/index.gen';
|
import { Dashboard } from '@grafana/schema/dist/esm/index.gen';
|
||||||
import { ModalsContext, Modal, Button, useStyles2 } from '@grafana/ui';
|
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 { contextSrv } from 'app/core/services/context_srv';
|
||||||
|
|
||||||
import { SaveLibraryVizPanelModal } from '../panel-edit/SaveLibraryVizPanelModal';
|
import { SaveLibraryVizPanelModal } from '../panel-edit/SaveLibraryVizPanelModal';
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import * as H from 'history';
|
import * as H from 'history';
|
||||||
import { find } from 'lodash';
|
import { find } from 'lodash';
|
||||||
import { memo, useContext, useEffect, useState } from 'react';
|
import { memo, useContext, useEffect, useState } from 'react';
|
||||||
import { Prompt } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { locationService } from '@grafana/runtime';
|
import { locationService } from '@grafana/runtime';
|
||||||
import { Dashboard } from '@grafana/schema';
|
import { Dashboard } from '@grafana/schema';
|
||||||
import { ModalsContext } from '@grafana/ui';
|
import { ModalsContext } from '@grafana/ui';
|
||||||
import { appEvents } from 'app/core/app_events';
|
import { appEvents } from 'app/core/app_events';
|
||||||
|
import { Prompt } from 'app/core/components/FormPrompt/Prompt';
|
||||||
import { contextSrv } from 'app/core/services/context_srv';
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
import { SaveLibraryPanelModal } from 'app/features/library-panels/components/SaveLibraryPanelModal/SaveLibraryPanelModal';
|
import { SaveLibraryPanelModal } from 'app/features/library-panels/components/SaveLibraryPanelModal/SaveLibraryPanelModal';
|
||||||
import { PanelModelWithLibraryPanel } from 'app/features/library-panels/types';
|
import { PanelModelWithLibraryPanel } from 'app/features/library-panels/types';
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Prompt } from 'react-router-dom';
|
|
||||||
import { useBeforeUnload, useUnmount } from 'react-use';
|
import { useBeforeUnload, useUnmount } from 'react-use';
|
||||||
|
|
||||||
import { GrafanaTheme2, colorManipulator } from '@grafana/data';
|
import { GrafanaTheme2, colorManipulator } from '@grafana/data';
|
||||||
import { reportInteraction } from '@grafana/runtime';
|
import { reportInteraction } from '@grafana/runtime';
|
||||||
import { Button, Icon, Stack, Tooltip, useStyles2 } from '@grafana/ui';
|
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 { CORRELATION_EDITOR_POST_CONFIRM_ACTION, ExploreItemState, useDispatch, useSelector } from 'app/types';
|
||||||
|
|
||||||
import { CorrelationUnsavedChangesModal } from './CorrelationUnsavedChangesModal';
|
import { CorrelationUnsavedChangesModal } from './CorrelationUnsavedChangesModal';
|
||||||
@ -175,13 +175,13 @@ export const CorrelationEditorModeBar = ({ panes }: { panes: Array<[string, Expl
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Handle navigating outside of Explore */}
|
{/* Handle navigating outside Explore */}
|
||||||
<Prompt
|
<Prompt
|
||||||
message={(location) => {
|
message={(location) => {
|
||||||
if (
|
if (
|
||||||
location.pathname !== '/explore' &&
|
location.pathname !== '/explore' &&
|
||||||
(correlationDetails?.editorMode || false) &&
|
correlationDetails?.editorMode &&
|
||||||
(correlationDetails?.correlationDirty || false)
|
correlationDetails?.correlationDirty
|
||||||
) {
|
) {
|
||||||
return 'You have unsaved correlation data. Continue?';
|
return 'You have unsaved correlation data. Continue?';
|
||||||
} else {
|
} else {
|
||||||
|
Reference in New Issue
Block a user