Skip to content

Commit f8dc333

Browse files
Preferences: Add confirmation modal when saving org preferences (grafana#59119)
1 parent 5b1ff83 commit f8dc333

File tree

7 files changed

+79
-13
lines changed

7 files changed

+79
-13
lines changed

packages/grafana-ui/src/components/ConfirmModal/ConfirmModal.story.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export const Basic: ComponentStory<typeof ConfirmModal> = ({
4646
body,
4747
description,
4848
confirmText,
49+
confirmButtonVariant,
4950
dismissText,
5051
icon,
5152
isOpen,
@@ -58,6 +59,7 @@ export const Basic: ComponentStory<typeof ConfirmModal> = ({
5859
body={body}
5960
description={description}
6061
confirmText={confirmText}
62+
confirmButtonVariant={confirmButtonVariant}
6163
dismissText={dismissText}
6264
icon={icon}
6365
onConfirm={onConfirm}
@@ -77,6 +79,7 @@ Basic.args = {
7779
body: 'Are you sure you want to delete this user?',
7880
description: 'Removing the user will not remove any dashboards the user has created',
7981
confirmText: 'Delete',
82+
confirmButtonVariant: 'destructive',
8083
dismissText: 'Cancel',
8184
icon: 'exclamation-triangle',
8285
isOpen: true,
@@ -112,7 +115,7 @@ export const AlternativeAction: ComponentStory<typeof ConfirmModal> = ({
112115

113116
AlternativeAction.parameters = {
114117
controls: {
115-
exclude: [...defaultExcludes, 'confirmationText'],
118+
exclude: [...defaultExcludes, 'confirmationText', 'confirmButtonVariant'],
116119
},
117120
};
118121

@@ -155,7 +158,7 @@ export const WithConfirmation: ComponentStory<typeof ConfirmModal> = ({
155158

156159
WithConfirmation.parameters = {
157160
controls: {
158-
exclude: [...defaultExcludes, 'alternativeText'],
161+
exclude: [...defaultExcludes, 'alternativeText', 'confirmButtonVariant'],
159162
},
160163
};
161164

packages/grafana-ui/src/components/ConfirmModal/ConfirmModal.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { selectors } from '@grafana/e2e-selectors';
77
import { HorizontalGroup, Input } from '..';
88
import { useStyles2 } from '../../themes';
99
import { IconName } from '../../types/icon';
10-
import { Button } from '../Button';
10+
import { Button, ButtonVariant } from '../Button';
1111
import { Modal } from '../Modal/Modal';
1212

1313
export interface ConfirmModalProps {
@@ -31,6 +31,8 @@ export interface ConfirmModalProps {
3131
confirmationText?: string;
3232
/** Text for alternative button */
3333
alternativeText?: string;
34+
/** Confirm button variant */
35+
confirmButtonVariant?: ButtonVariant;
3436
/** Confirm action callback */
3537
onConfirm(): void;
3638
/** Dismiss action callback */
@@ -53,6 +55,7 @@ export const ConfirmModal = ({
5355
onConfirm,
5456
onDismiss,
5557
onAlternative,
58+
confirmButtonVariant = 'destructive',
5659
}: ConfirmModalProps): JSX.Element => {
5760
const [disabled, setDisabled] = useState(Boolean(confirmationText));
5861
const styles = useStyles2(getStyles);
@@ -86,7 +89,7 @@ export const ConfirmModal = ({
8689
{dismissText}
8790
</Button>
8891
<Button
89-
variant="destructive"
92+
variant={confirmButtonVariant}
9093
onClick={onConfirm}
9194
disabled={disabled}
9295
ref={buttonRef}

public/app/core/components/SharedPreferences/SharedPreferences.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { UserPreferencesDTO } from 'app/types';
2626
export interface Props {
2727
resourceUri: string;
2828
disabled?: boolean;
29+
onConfirm?: () => Promise<boolean>;
2930
}
3031

3132
export type State = UserPreferencesDTO;
@@ -85,9 +86,13 @@ export class SharedPreferences extends PureComponent<Props, State> {
8586
}
8687

8788
onSubmitForm = async () => {
88-
const { homeDashboardUID, theme, timezone, weekStart, language, queryHistory } = this.state;
89-
await this.service.update({ homeDashboardUID, theme, timezone, weekStart, language, queryHistory });
90-
window.location.reload();
89+
const confirmationResult = this.props.onConfirm ? await this.props.onConfirm() : true;
90+
91+
if (confirmationResult) {
92+
const { homeDashboardUID, theme, timezone, weekStart, language, queryHistory } = this.state;
93+
await this.service.update({ homeDashboardUID, theme, timezone, weekStart, language, queryHistory });
94+
window.location.reload();
95+
}
9196
};
9297

9398
onThemeChanged = (value: string) => {

public/app/core/services/ModalManager.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export class ModalManager {
5151
const {
5252
confirmText,
5353
onConfirm = () => undefined,
54+
onDismiss,
5455
text2,
5556
altActionText,
5657
onAltAction,
@@ -60,9 +61,11 @@ export class ModalManager {
6061
yesText = 'Yes',
6162
icon,
6263
title = 'Confirm',
64+
yesButtonVariant,
6365
} = payload;
6466
const props: ConfirmModalProps = {
6567
confirmText: yesText,
68+
confirmButtonVariant: yesButtonVariant,
6669
confirmationText: confirmText,
6770
icon,
6871
title,
@@ -74,7 +77,10 @@ export class ModalManager {
7477
onConfirm();
7578
this.onReactModalDismiss();
7679
},
77-
onDismiss: this.onReactModalDismiss,
80+
onDismiss: () => {
81+
onDismiss?.();
82+
this.onReactModalDismiss();
83+
},
7884
onAlternative: onAltAction
7985
? () => {
8086
onAltAction();

public/app/features/org/OrgDetailsPage.test.tsx

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1-
import { render } from '@testing-library/react';
1+
import { render, screen } from '@testing-library/react';
2+
import userEvent from '@testing-library/user-event';
23
import React from 'react';
4+
import { Provider } from 'react-redux';
35
import { mockToolkitActionCreator } from 'test/core/redux/mocks';
46

57
import { NavModel } from '@grafana/data';
8+
import { ModalManager } from 'app/core/services/ModalManager';
9+
import { configureStore } from 'app/store/configureStore';
610

711
import { backendSrv } from '../../core/services/backend_srv';
812
import { Organization } from '../../types';
@@ -12,6 +16,7 @@ import { setOrganizationName } from './state/reducers';
1216

1317
jest.mock('app/core/core', () => {
1418
return {
19+
...jest.requireActual('app/core/core'),
1520
contextSrv: {
1621
hasPermission: () => true,
1722
},
@@ -56,7 +61,11 @@ const setup = (propOverrides?: object) => {
5661
};
5762
Object.assign(props, propOverrides);
5863

59-
render(<OrgDetailsPage {...props} />);
64+
render(
65+
<Provider store={configureStore()}>
66+
<OrgDetailsPage {...props} />
67+
</Provider>
68+
);
6069
};
6170

6271
describe('Render', () => {
@@ -84,4 +93,24 @@ describe('Render', () => {
8493
})
8594
).not.toThrow();
8695
});
96+
97+
it('should show a modal when submitting', async () => {
98+
new ModalManager().init();
99+
setup({
100+
organization: {
101+
name: 'Cool org',
102+
id: 1,
103+
},
104+
preferences: {
105+
homeDashboardUID: 'home-dashboard',
106+
theme: 'Default',
107+
timezone: 'Default',
108+
locale: '',
109+
},
110+
});
111+
112+
await userEvent.click(screen.getByRole('button', { name: 'Save' }));
113+
114+
expect(screen.getByText('Confirm preferences update')).toBeInTheDocument();
115+
});
87116
});

public/app/features/org/OrgDetailsPage.tsx

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ import { NavModel } from '@grafana/data';
55
import { VerticalGroup } from '@grafana/ui';
66
import { Page } from 'app/core/components/Page/Page';
77
import SharedPreferences from 'app/core/components/SharedPreferences/SharedPreferences';
8-
import { contextSrv } from 'app/core/core';
8+
import { appEvents, contextSrv } from 'app/core/core';
99
import { getNavModel } from 'app/core/selectors/navModel';
1010
import { AccessControlAction, Organization, StoreState } from 'app/types';
11+
import { ShowConfirmModalEvent } from 'app/types/events';
1112

1213
import OrgProfile from './OrgProfile';
1314
import { loadOrganization, updateOrganization } from './state/actions';
@@ -31,6 +32,21 @@ export class OrgDetailsPage extends PureComponent<Props> {
3132
this.props.updateOrganization();
3233
};
3334

35+
handleConfirm = () => {
36+
return new Promise<boolean>((resolve) => {
37+
appEvents.publish(
38+
new ShowConfirmModalEvent({
39+
title: 'Confirm preferences update',
40+
text: 'This will update the preferences for the whole organization. Are you sure you want to update the preferences?',
41+
yesText: 'Save',
42+
yesButtonVariant: 'primary',
43+
onConfirm: async () => resolve(true),
44+
onDismiss: async () => resolve(false),
45+
})
46+
);
47+
});
48+
};
49+
3450
render() {
3551
const { navModel, organization } = this.props;
3652
const isLoading = Object.keys(organization).length === 0;
@@ -44,7 +60,9 @@ export class OrgDetailsPage extends PureComponent<Props> {
4460
{!isLoading && (
4561
<VerticalGroup spacing="lg">
4662
{canReadOrg && <OrgProfile onSubmit={this.onUpdateOrganization} orgName={organization.name} />}
47-
{canReadPreferences && <SharedPreferences resourceUri="org" disabled={!canWritePreferences} />}
63+
{canReadPreferences && (
64+
<SharedPreferences resourceUri="org" disabled={!canWritePreferences} onConfirm={this.handleConfirm} />
65+
)}
4866
</VerticalGroup>
4967
)}
5068
</Page.Contents>

public/app/types/events.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { AnnotationQuery, BusEventBase, BusEventWithPayload, eventFactory } from '@grafana/data';
2-
import { IconName } from '@grafana/ui';
2+
import { IconName, ButtonVariant } from '@grafana/ui';
33

44
/**
55
* Event Payloads
@@ -37,7 +37,9 @@ export interface ShowConfirmModalPayload {
3737
yesText?: string;
3838
noText?: string;
3939
icon?: IconName;
40+
yesButtonVariant?: ButtonVariant;
4041

42+
onDismiss?: () => void;
4143
onConfirm?: () => void;
4244
onAltAction?: () => void;
4345
}

0 commit comments

Comments
 (0)