Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Preferences: Add confirmation modal when saving org preferences #59119

Merged
merged 2 commits into from Nov 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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, 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) => {
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