Preferences: Add confirmation modal when saving org preferences (#59119)

This commit is contained in:
Joao Silva 2022-11-22 16:47:42 +01:00 committed by GitHub
parent 5b1ff83ee9
commit f8dc333ee4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 79 additions and 13 deletions

View File

@ -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'],
},
};

View File

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

View File

@ -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) => {

View File

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

View File

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

View File

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

View File

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