mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Preferences: Add confirmation modal when saving org preferences (#59119)
This commit is contained in:
parent
5b1ff83ee9
commit
f8dc333ee4
@ -46,6 +46,7 @@ export const Basic: ComponentStory<typeof ConfirmModal> = ({
|
||||
body,
|
||||
description,
|
||||
confirmText,
|
||||
confirmButtonVariant,
|
||||
dismissText,
|
||||
icon,
|
||||
isOpen,
|
||||
@ -58,6 +59,7 @@ export const Basic: ComponentStory<typeof ConfirmModal> = ({
|
||||
body={body}
|
||||
description={description}
|
||||
confirmText={confirmText}
|
||||
confirmButtonVariant={confirmButtonVariant}
|
||||
dismissText={dismissText}
|
||||
icon={icon}
|
||||
onConfirm={onConfirm}
|
||||
@ -77,6 +79,7 @@ Basic.args = {
|
||||
body: 'Are you sure you want to delete this user?',
|
||||
description: 'Removing the user will not remove any dashboards the user has created',
|
||||
confirmText: 'Delete',
|
||||
confirmButtonVariant: 'destructive',
|
||||
dismissText: 'Cancel',
|
||||
icon: 'exclamation-triangle',
|
||||
isOpen: true,
|
||||
@ -112,7 +115,7 @@ export const AlternativeAction: ComponentStory<typeof ConfirmModal> = ({
|
||||
|
||||
AlternativeAction.parameters = {
|
||||
controls: {
|
||||
exclude: [...defaultExcludes, 'confirmationText'],
|
||||
exclude: [...defaultExcludes, 'confirmationText', 'confirmButtonVariant'],
|
||||
},
|
||||
};
|
||||
|
||||
@ -155,7 +158,7 @@ export const WithConfirmation: ComponentStory<typeof ConfirmModal> = ({
|
||||
|
||||
WithConfirmation.parameters = {
|
||||
controls: {
|
||||
exclude: [...defaultExcludes, 'alternativeText'],
|
||||
exclude: [...defaultExcludes, 'alternativeText', 'confirmButtonVariant'],
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -7,7 +7,7 @@ import { selectors } from '@grafana/e2e-selectors';
|
||||
import { HorizontalGroup, Input } from '..';
|
||||
import { useStyles2 } from '../../themes';
|
||||
import { IconName } from '../../types/icon';
|
||||
import { Button } from '../Button';
|
||||
import { Button, ButtonVariant } from '../Button';
|
||||
import { Modal } from '../Modal/Modal';
|
||||
|
||||
export interface ConfirmModalProps {
|
||||
@ -31,6 +31,8 @@ export interface ConfirmModalProps {
|
||||
confirmationText?: string;
|
||||
/** Text for alternative button */
|
||||
alternativeText?: string;
|
||||
/** Confirm button variant */
|
||||
confirmButtonVariant?: ButtonVariant;
|
||||
/** Confirm action callback */
|
||||
onConfirm(): void;
|
||||
/** Dismiss action callback */
|
||||
@ -53,6 +55,7 @@ export const ConfirmModal = ({
|
||||
onConfirm,
|
||||
onDismiss,
|
||||
onAlternative,
|
||||
confirmButtonVariant = 'destructive',
|
||||
}: ConfirmModalProps): JSX.Element => {
|
||||
const [disabled, setDisabled] = useState(Boolean(confirmationText));
|
||||
const styles = useStyles2(getStyles);
|
||||
@ -86,7 +89,7 @@ export const ConfirmModal = ({
|
||||
{dismissText}
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
variant={confirmButtonVariant}
|
||||
onClick={onConfirm}
|
||||
disabled={disabled}
|
||||
ref={buttonRef}
|
||||
|
@ -26,6 +26,7 @@ import { UserPreferencesDTO } from 'app/types';
|
||||
export interface Props {
|
||||
resourceUri: string;
|
||||
disabled?: boolean;
|
||||
onConfirm?: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
export type State = UserPreferencesDTO;
|
||||
@ -85,9 +86,13 @@ export class SharedPreferences extends PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
onSubmitForm = async () => {
|
||||
const { homeDashboardUID, theme, timezone, weekStart, language, queryHistory } = this.state;
|
||||
await this.service.update({ homeDashboardUID, theme, timezone, weekStart, language, queryHistory });
|
||||
window.location.reload();
|
||||
const confirmationResult = this.props.onConfirm ? await this.props.onConfirm() : true;
|
||||
|
||||
if (confirmationResult) {
|
||||
const { homeDashboardUID, theme, timezone, weekStart, language, queryHistory } = this.state;
|
||||
await this.service.update({ homeDashboardUID, theme, timezone, weekStart, language, queryHistory });
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
|
||||
onThemeChanged = (value: string) => {
|
||||
|
@ -51,6 +51,7 @@ export class ModalManager {
|
||||
const {
|
||||
confirmText,
|
||||
onConfirm = () => undefined,
|
||||
onDismiss,
|
||||
text2,
|
||||
altActionText,
|
||||
onAltAction,
|
||||
@ -60,9 +61,11 @@ export class ModalManager {
|
||||
yesText = 'Yes',
|
||||
icon,
|
||||
title = 'Confirm',
|
||||
yesButtonVariant,
|
||||
} = payload;
|
||||
const props: ConfirmModalProps = {
|
||||
confirmText: yesText,
|
||||
confirmButtonVariant: yesButtonVariant,
|
||||
confirmationText: confirmText,
|
||||
icon,
|
||||
title,
|
||||
@ -74,7 +77,10 @@ export class ModalManager {
|
||||
onConfirm();
|
||||
this.onReactModalDismiss();
|
||||
},
|
||||
onDismiss: this.onReactModalDismiss,
|
||||
onDismiss: () => {
|
||||
onDismiss?.();
|
||||
this.onReactModalDismiss();
|
||||
},
|
||||
onAlternative: onAltAction
|
||||
? () => {
|
||||
onAltAction();
|
||||
|
@ -1,8 +1,12 @@
|
||||
import { render } from '@testing-library/react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { mockToolkitActionCreator } from 'test/core/redux/mocks';
|
||||
|
||||
import { NavModel } from '@grafana/data';
|
||||
import { ModalManager } from 'app/core/services/ModalManager';
|
||||
import { configureStore } from 'app/store/configureStore';
|
||||
|
||||
import { backendSrv } from '../../core/services/backend_srv';
|
||||
import { Organization } from '../../types';
|
||||
@ -12,6 +16,7 @@ import { setOrganizationName } from './state/reducers';
|
||||
|
||||
jest.mock('app/core/core', () => {
|
||||
return {
|
||||
...jest.requireActual('app/core/core'),
|
||||
contextSrv: {
|
||||
hasPermission: () => true,
|
||||
},
|
||||
@ -56,7 +61,11 @@ const setup = (propOverrides?: object) => {
|
||||
};
|
||||
Object.assign(props, propOverrides);
|
||||
|
||||
render(<OrgDetailsPage {...props} />);
|
||||
render(
|
||||
<Provider store={configureStore()}>
|
||||
<OrgDetailsPage {...props} />
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
describe('Render', () => {
|
||||
@ -84,4 +93,24 @@ describe('Render', () => {
|
||||
})
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('should show a modal when submitting', async () => {
|
||||
new ModalManager().init();
|
||||
setup({
|
||||
organization: {
|
||||
name: 'Cool org',
|
||||
id: 1,
|
||||
},
|
||||
preferences: {
|
||||
homeDashboardUID: 'home-dashboard',
|
||||
theme: 'Default',
|
||||
timezone: 'Default',
|
||||
locale: '',
|
||||
},
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Save' }));
|
||||
|
||||
expect(screen.getByText('Confirm preferences update')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
@ -5,9 +5,10 @@ import { NavModel } from '@grafana/data';
|
||||
import { VerticalGroup } from '@grafana/ui';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import SharedPreferences from 'app/core/components/SharedPreferences/SharedPreferences';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { appEvents, contextSrv } from 'app/core/core';
|
||||
import { getNavModel } from 'app/core/selectors/navModel';
|
||||
import { AccessControlAction, Organization, StoreState } from 'app/types';
|
||||
import { ShowConfirmModalEvent } from 'app/types/events';
|
||||
|
||||
import OrgProfile from './OrgProfile';
|
||||
import { loadOrganization, updateOrganization } from './state/actions';
|
||||
@ -31,6 +32,21 @@ export class OrgDetailsPage extends PureComponent<Props> {
|
||||
this.props.updateOrganization();
|
||||
};
|
||||
|
||||
handleConfirm = () => {
|
||||
return new Promise<boolean>((resolve) => {
|
||||
appEvents.publish(
|
||||
new ShowConfirmModalEvent({
|
||||
title: 'Confirm preferences update',
|
||||
text: 'This will update the preferences for the whole organization. Are you sure you want to update the preferences?',
|
||||
yesText: 'Save',
|
||||
yesButtonVariant: 'primary',
|
||||
onConfirm: async () => resolve(true),
|
||||
onDismiss: async () => resolve(false),
|
||||
})
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { navModel, organization } = this.props;
|
||||
const isLoading = Object.keys(organization).length === 0;
|
||||
@ -44,7 +60,9 @@ export class OrgDetailsPage extends PureComponent<Props> {
|
||||
{!isLoading && (
|
||||
<VerticalGroup spacing="lg">
|
||||
{canReadOrg && <OrgProfile onSubmit={this.onUpdateOrganization} orgName={organization.name} />}
|
||||
{canReadPreferences && <SharedPreferences resourceUri="org" disabled={!canWritePreferences} />}
|
||||
{canReadPreferences && (
|
||||
<SharedPreferences resourceUri="org" disabled={!canWritePreferences} onConfirm={this.handleConfirm} />
|
||||
)}
|
||||
</VerticalGroup>
|
||||
)}
|
||||
</Page.Contents>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { AnnotationQuery, BusEventBase, BusEventWithPayload, eventFactory } from '@grafana/data';
|
||||
import { IconName } from '@grafana/ui';
|
||||
import { IconName, ButtonVariant } from '@grafana/ui';
|
||||
|
||||
/**
|
||||
* Event Payloads
|
||||
@ -37,7 +37,9 @@ export interface ShowConfirmModalPayload {
|
||||
yesText?: string;
|
||||
noText?: string;
|
||||
icon?: IconName;
|
||||
yesButtonVariant?: ButtonVariant;
|
||||
|
||||
onDismiss?: () => void;
|
||||
onConfirm?: () => void;
|
||||
onAltAction?: () => void;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user