ConfirmModal: Migrates to React with new theme (#33107)

* ConfirmModal: Migrates to React

* Refactor: migrates to v2 styles

* Chore: updates after PR comments
This commit is contained in:
Hugo Häggmark 2021-04-19 14:30:18 +02:00 committed by GitHub
parent 33621e6f9b
commit 9bb1484dc5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 234 additions and 62 deletions

View File

@ -4,7 +4,9 @@ import { action } from '@storybook/addon-actions';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { ConfirmModal } from '@grafana/ui';
import mdx from './ConfirmModal.mdx';
import { Props } from './ConfirmModal';
import { ConfirmModalProps } from './ConfirmModal';
const defaultExcludes = ['onConfirm', 'onDismiss', 'onAlternative'];
export default {
title: 'Overlays/ConfirmModal',
@ -18,11 +20,13 @@ export default {
disable: true,
},
controls: {
exclude: ['isOpen', 'body'],
exclude: defaultExcludes,
},
},
argTypes: {
icon: { control: { type: 'select', options: ['exclamation-triangle', 'power', 'cog', 'lock'] } },
icon: { control: { type: 'select', options: ['exclamation-triangle', 'power', 'cog', 'lock', 'trash-alt'] } },
body: { control: { type: 'text' } },
description: { control: { type: 'text' } },
},
} as Meta;
@ -33,20 +37,27 @@ const defaultActions = {
onDismiss: () => {
action('Dismiss')('close');
},
onAlternative: () => {
action('Alternative')('alternative');
},
};
interface StoryProps extends Props {
visible: boolean;
bodyText: string;
}
export const Basic: Story<StoryProps> = ({ title, bodyText, confirmText, dismissText, icon, visible }) => {
export const Basic: Story<ConfirmModalProps> = ({
title,
body,
description,
confirmText,
dismissText,
icon,
isOpen,
}) => {
const { onConfirm, onDismiss } = defaultActions;
return (
<ConfirmModal
isOpen={visible}
isOpen={isOpen}
title={title}
body={bodyText}
body={body}
description={description}
confirmText={confirmText}
dismissText={dismissText}
icon={icon}
@ -56,11 +67,106 @@ export const Basic: Story<StoryProps> = ({ title, bodyText, confirmText, dismiss
);
};
Basic.parameters = {
controls: {
exclude: [...defaultExcludes, 'alternativeText', 'confirmationText'],
},
};
Basic.args = {
title: 'Delete user',
bodyText: 'Are you sure you want to delete this user?',
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',
dismissText: 'Cancel',
icon: 'exclamation-triangle',
visible: true,
isOpen: true,
};
export const AlternativeAction: Story<ConfirmModalProps> = ({
title,
body,
description,
confirmText,
dismissText,
icon,
alternativeText,
isOpen,
}) => {
const { onConfirm, onDismiss, onAlternative } = defaultActions;
return (
<ConfirmModal
isOpen={isOpen}
title={title}
body={body}
description={description}
confirmText={confirmText}
dismissText={dismissText}
alternativeText={alternativeText}
icon={icon}
onConfirm={onConfirm}
onDismiss={onDismiss}
onAlternative={onAlternative}
/>
);
};
AlternativeAction.parameters = {
controls: {
exclude: [...defaultExcludes, 'confirmationText'],
},
};
AlternativeAction.args = {
title: 'Delete row',
body: 'Are you sure you want to remove this row and all its panels?',
alternativeText: 'Delete row only',
confirmText: 'Yes',
dismissText: 'Cancel',
icon: 'trash-alt',
isOpen: true,
};
export const WithConfirmation: Story<ConfirmModalProps> = ({
title,
body,
description,
confirmationText,
confirmText,
dismissText,
icon,
isOpen,
}) => {
const { onConfirm, onDismiss } = defaultActions;
return (
<ConfirmModal
isOpen={isOpen}
title={title}
body={body}
confirmationText={confirmationText}
description={description}
confirmText={confirmText}
dismissText={dismissText}
icon={icon}
onConfirm={onConfirm}
onDismiss={onDismiss}
/>
);
};
WithConfirmation.parameters = {
controls: {
exclude: [...defaultExcludes, 'alternativeText'],
},
};
WithConfirmation.args = {
title: 'Delete',
body: 'Do you want to delete this notification channel?',
description: 'Deleting this notification channel will not delete from alerts any references to it',
confirmationText: 'Delete',
confirmText: 'Delete',
dismissText: 'Cancel',
icon: 'trash-alt',
isOpen: true,
};

View File

@ -1,50 +1,86 @@
import React, { FC, useContext } from 'react';
import React, { useState } from 'react';
import { css } from '@emotion/css';
import { Modal } from '../Modal/Modal';
import { IconName } from '../../types/icon';
import { Button } from '../Button';
import { stylesFactory, ThemeContext } from '../../themes';
import { useStyles } from '../../themes';
import { GrafanaTheme } from '@grafana/data';
import { HorizontalGroup } from '..';
import { HorizontalGroup, Input } from '..';
import { selectors } from '@grafana/e2e-selectors';
export interface Props {
export interface ConfirmModalProps {
/** Toggle modal's open/closed state */
isOpen: boolean;
/** Title for the modal header */
title: string;
/** Modal content */
body: React.ReactNode;
/** Modal description */
description?: React.ReactNode;
/** Text for confirm button */
confirmText: string;
/** Text for dismiss button */
dismissText?: string;
/** Icon for the modal header */
icon?: IconName;
/** Text user needs to fill in before confirming */
confirmationText?: string;
/** Text for alternative button */
alternativeText?: string;
/** Confirm action callback */
onConfirm(): void;
/** Dismiss action callback */
onDismiss(): void;
/** Alternative action callback */
onAlternative?(): void;
}
export const ConfirmModal: FC<Props> = ({
export const ConfirmModal = ({
isOpen,
title,
body,
description,
confirmText,
confirmationText,
dismissText = 'Cancel',
alternativeText,
icon = 'exclamation-triangle',
onConfirm,
onDismiss,
}) => {
const theme = useContext(ThemeContext);
const styles = getStyles(theme);
onAlternative,
}: ConfirmModalProps): JSX.Element => {
const [disabled, setDisabled] = useState(Boolean(confirmationText));
const styles = useStyles(getStyles);
const onConfirmationTextChange = (event: React.FormEvent<HTMLInputElement>) => {
setDisabled(confirmationText?.localeCompare(event.currentTarget.value) !== 0);
};
return (
<Modal className={styles.modal} title={title} icon={icon} isOpen={isOpen} onDismiss={onDismiss}>
<div className={styles.modalContent}>
<div className={styles.modalText}>{body}</div>
<div className={styles.modalText}>
{body}
{description ? <div className={styles.modalDescription}>{description}</div> : null}
{confirmationText ? (
<div className={styles.modalConfirmationInput}>
<HorizontalGroup justify="center">
<Input placeholder={`Type ${confirmationText} to confirm`} onChange={onConfirmationTextChange} />
</HorizontalGroup>
</div>
) : null}
</div>
<HorizontalGroup justify="center">
<Button variant="destructive" onClick={onConfirm}>
{onAlternative ? (
<Button variant="primary" onClick={onAlternative}>
{alternativeText}
</Button>
) : null}
<Button
variant="destructive"
onClick={onConfirm}
disabled={disabled}
aria-label={selectors.pages.ConfirmModal.delete}
>
{confirmText}
</Button>
<Button variant="secondary" onClick={onDismiss}>
@ -56,17 +92,24 @@ export const ConfirmModal: FC<Props> = ({
);
};
const getStyles = stylesFactory((theme: GrafanaTheme) => ({
const getStyles = (theme: GrafanaTheme) => ({
modal: css`
width: 500px;
`,
modalContent: css`
text-align: center;
`,
modalText: css`
font-size: ${theme.typography.heading.h4};
color: ${theme.colors.link};
margin-bottom: calc(${theme.spacing.d} * 2);
padding-top: ${theme.spacing.d};
`,
}));
modalText: css({
fontSize: theme.v2.typography.h4.fontSize,
color: theme.v2.palette.text.primary,
marginBottom: `calc(${theme.v2.spacing(2)}*2)`,
paddingTop: theme.v2.spacing(2),
}),
modalDescription: css({
fontSize: theme.v2.typography.h6.fontSize,
paddingTop: theme.v2.spacing(2),
}),
modalConfirmationInput: css({
paddingTop: theme.v2.spacing(2),
}),
});

View File

@ -33,7 +33,7 @@ export { Tag, OnTagClick } from './Tags/Tag';
export { TagList } from './Tags/TagList';
export { FilterPill } from './FilterPill/FilterPill';
export { ConfirmModal } from './ConfirmModal/ConfirmModal';
export { ConfirmModal, ConfirmModalProps } from './ConfirmModal/ConfirmModal';
export { QueryField } from './QueryField/QueryField';
// Code editor

View File

@ -1,6 +1,5 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { selectors } from '@grafana/e2e-selectors';
import coreModule from 'app/core/core_module';
import appEvents from 'app/core/app_events';
@ -8,7 +7,15 @@ import appEvents from 'app/core/app_events';
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
import { AngularModalProxy } from '../components/modals/AngularModalProxy';
import { provideTheme } from '../utils/ConfigProvider';
import { HideModalEvent, ShowConfirmModalEvent, ShowModalEvent, ShowModalReactEvent } from '../../types/events';
import {
HideModalEvent,
ShowConfirmModalEvent,
ShowConfirmModalPayload,
ShowModalEvent,
ShowModalReactEvent,
} from '../../types/events';
import { ConfirmModal, ConfirmModalProps } from '@grafana/ui';
import { textUtil } from '@grafana/data';
export class UtilSrv {
modalScope: any;
@ -84,35 +91,50 @@ export class UtilSrv {
});
}
showConfirmModal(payload: any) {
const scope: any = this.$rootScope.$new();
scope.updateConfirmText = (value: any) => {
scope.confirmTextValid = payload.confirmText.toLowerCase() === value.toLowerCase();
showConfirmModal(payload: ShowConfirmModalPayload) {
const {
confirmText,
onConfirm = () => undefined,
text2,
altActionText,
onAltAction,
noText,
text,
text2htmlBind,
yesText = 'Yes',
icon,
title = 'Confirm',
} = payload;
const props: ConfirmModalProps = {
confirmText: yesText,
confirmationText: confirmText,
icon,
title,
body: text,
description: text2 && text2htmlBind ? textUtil.sanitize(text2) : text2,
isOpen: true,
dismissText: noText,
onConfirm: () => {
onConfirm();
this.onReactModalDismiss();
},
onDismiss: this.onReactModalDismiss,
onAlternative: onAltAction
? () => {
onAltAction();
this.onReactModalDismiss();
}
: undefined,
alternativeText: altActionText,
};
const modalProps = {
component: ConfirmModal,
props,
};
scope.title = payload.title;
scope.text = payload.text;
scope.text2 = payload.text2;
scope.text2htmlBind = payload.text2htmlBind;
scope.confirmText = payload.confirmText;
scope.onConfirm = payload.onConfirm;
scope.onAltAction = payload.onAltAction;
scope.altActionText = payload.altActionText;
scope.icon = payload.icon || 'check';
scope.yesText = payload.yesText || 'Yes';
scope.noText = payload.noText || 'Cancel';
scope.confirmTextValid = scope.confirmText ? false : true;
scope.selectors = selectors.pages.ConfirmModal;
appEvents.publish(
new ShowModalEvent({
src: 'public/app/partials/confirm_modal.html',
scope: scope,
modalClass: 'confirm-modal',
})
);
const elem = React.createElement(provideTheme(AngularModalProxy), modalProps);
this.reactModalRoot.appendChild(this.reactModalNode);
ReactDOM.render(elem, this.reactModalNode);
}
}

View File

@ -1,4 +1,5 @@
import { BusEventBase, BusEventWithPayload, eventFactory, GrafanaTheme, TimeRange } from '@grafana/data';
import { IconName } from '@grafana/ui';
/**
* Event Payloads
@ -35,7 +36,7 @@ export interface ShowConfirmModalPayload {
altActionText?: string;
yesText?: string;
noText?: string;
icon?: string;
icon?: IconName;
onConfirm?: () => void;
onAltAction?: () => void;