Skip to content

Commit

Permalink
[v9.3.x] Preferences: Add confirmation modal when saving org preferen…
Browse files Browse the repository at this point in the history
…ces (#59119) (#59141)
  • Loading branch information
JoaoSilvaGrafana committed Nov 22, 2022
1 parent a2263b9 commit 4c3adef
Show file tree
Hide file tree
Showing 7 changed files with 79 additions and 13 deletions.
Expand Up @@ -46,6 +46,7 @@ export const Basic: ComponentStory<typeof ConfirmModal> = ({
body,
description,
confirmText,
confirmButtonVariant,
dismissText,
icon,
isOpen,
Expand All @@ -58,6 +59,7 @@ export const Basic: ComponentStory<typeof ConfirmModal> = ({
body={body}
description={description}
confirmText={confirmText}
confirmButtonVariant={confirmButtonVariant}
dismissText={dismissText}
icon={icon}
onConfirm={onConfirm}
Expand All @@ -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,
Expand Down Expand Up @@ -112,7 +115,7 @@ export const AlternativeAction: ComponentStory<typeof ConfirmModal> = ({

AlternativeAction.parameters = {
controls: {
exclude: [...defaultExcludes, 'confirmationText'],
exclude: [...defaultExcludes, 'confirmationText', 'confirmButtonVariant'],
},
};

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

WithConfirmation.parameters = {
controls: {
exclude: [...defaultExcludes, 'alternativeText'],
exclude: [...defaultExcludes, 'alternativeText', 'confirmButtonVariant'],
},
};

Expand Down
Expand Up @@ -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 {
Expand All @@ -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 */
Expand All @@ -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);
Expand Down Expand Up @@ -86,7 +89,7 @@ export const ConfirmModal = ({
{dismissText}
</Button>
<Button
variant="destructive"
variant={confirmButtonVariant}
onClick={onConfirm}
disabled={disabled}
ref={buttonRef}
Expand Down
Expand Up @@ -26,6 +26,7 @@ import { UserPreferencesDTO } from 'app/types';
export interface Props {
resourceUri: string;
disabled?: boolean;
onConfirm?: () => Promise<boolean>;
}

export type State = UserPreferencesDTO;
Expand Down Expand Up @@ -85,9 +86,13 @@ export class SharedPreferences extends PureComponent<Props, State> {
}

onSubmitForm = async () => {
const { homeDashboardUID, theme, timezone, weekStart, locale, queryHistory } = this.state;
await this.service.update({ homeDashboardUID, theme, timezone, weekStart, locale, queryHistory });
window.location.reload();
const confirmationResult = this.props.onConfirm ? await this.props.onConfirm() : true;

if (confirmationResult) {
const { homeDashboardUID, theme, timezone, weekStart, locale, queryHistory } = this.state;
await this.service.update({ homeDashboardUID, theme, timezone, weekStart, locale, queryHistory });
window.location.reload();
}
};

onThemeChanged = (value: string) => {
Expand Down
8 changes: 7 additions & 1 deletion public/app/core/services/ModalManager.ts
Expand Up @@ -51,6 +51,7 @@ export class ModalManager {
const {
confirmText,
onConfirm = () => undefined,
onDismiss,
text2,
altActionText,
onAltAction,
Expand All @@ -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,
Expand All @@ -74,7 +77,10 @@ export class ModalManager {
onConfirm();
this.onReactModalDismiss();
},
onDismiss: this.onReactModalDismiss,
onDismiss: () => {
onDismiss?.();
this.onReactModalDismiss();
},
onAlternative: onAltAction
? () => {
onAltAction();
Expand Down
33 changes: 31 additions & 2 deletions public/app/features/org/OrgDetailsPage.test.tsx
@@ -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';
Expand All @@ -12,6 +16,7 @@ import { setOrganizationName } from './state/reducers';

jest.mock('app/core/core', () => {
return {
...jest.requireActual('app/core/core'),
contextSrv: {
hasPermission: () => true,
},
Expand Down Expand Up @@ -56,7 +61,11 @@ const setup = (propOverrides?: object) => {
};
Object.assign(props, propOverrides);

render(<OrgDetailsPage {...props} />);
render(
<Provider store={configureStore()}>
<OrgDetailsPage {...props} />
</Provider>
);
};

describe('Render', () => {
Expand Down Expand Up @@ -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();
});
});
22 changes: 20 additions & 2 deletions public/app/features/org/OrgDetailsPage.tsx
Expand Up @@ -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';
Expand All @@ -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;
Expand All @@ -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>
Expand Down
4 changes: 3 additions & 1 deletion public/app/types/events.ts
@@ -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
Expand Down Expand Up @@ -37,7 +37,9 @@ export interface ShowConfirmModalPayload {
yesText?: string;
noText?: string;
icon?: IconName;
yesButtonVariant?: ButtonVariant;

onDismiss?: () => void;
onConfirm?: () => void;
onAltAction?: () => void;
}
Expand Down

0 comments on commit 4c3adef

Please sign in to comment.