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
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 { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { ConfirmModal } from '@grafana/ui'; import { ConfirmModal } from '@grafana/ui';
import mdx from './ConfirmModal.mdx'; import mdx from './ConfirmModal.mdx';
import { Props } from './ConfirmModal'; import { ConfirmModalProps } from './ConfirmModal';
const defaultExcludes = ['onConfirm', 'onDismiss', 'onAlternative'];
export default { export default {
title: 'Overlays/ConfirmModal', title: 'Overlays/ConfirmModal',
@@ -18,11 +20,13 @@ export default {
disable: true, disable: true,
}, },
controls: { controls: {
exclude: ['isOpen', 'body'], exclude: defaultExcludes,
}, },
}, },
argTypes: { 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; } as Meta;
@@ -33,20 +37,27 @@ const defaultActions = {
onDismiss: () => { onDismiss: () => {
action('Dismiss')('close'); action('Dismiss')('close');
}, },
onAlternative: () => {
action('Alternative')('alternative');
},
}; };
interface StoryProps extends Props { export const Basic: Story<ConfirmModalProps> = ({
visible: boolean; title,
bodyText: string; body,
} description,
confirmText,
export const Basic: Story<StoryProps> = ({ title, bodyText, confirmText, dismissText, icon, visible }) => { dismissText,
icon,
isOpen,
}) => {
const { onConfirm, onDismiss } = defaultActions; const { onConfirm, onDismiss } = defaultActions;
return ( return (
<ConfirmModal <ConfirmModal
isOpen={visible} isOpen={isOpen}
title={title} title={title}
body={bodyText} body={body}
description={description}
confirmText={confirmText} confirmText={confirmText}
dismissText={dismissText} dismissText={dismissText}
icon={icon} icon={icon}
@@ -56,11 +67,106 @@ export const Basic: Story<StoryProps> = ({ title, bodyText, confirmText, dismiss
); );
}; };
Basic.parameters = {
controls: {
exclude: [...defaultExcludes, 'alternativeText', 'confirmationText'],
},
};
Basic.args = { Basic.args = {
title: 'Delete user', 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', confirmText: 'Delete',
dismissText: 'Cancel', dismissText: 'Cancel',
icon: 'exclamation-triangle', 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 { css } from '@emotion/css';
import { Modal } from '../Modal/Modal'; import { Modal } from '../Modal/Modal';
import { IconName } from '../../types/icon'; import { IconName } from '../../types/icon';
import { Button } from '../Button'; import { Button } from '../Button';
import { stylesFactory, ThemeContext } from '../../themes'; import { useStyles } from '../../themes';
import { GrafanaTheme } from '@grafana/data'; 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 */ /** Toggle modal's open/closed state */
isOpen: boolean; isOpen: boolean;
/** Title for the modal header */ /** Title for the modal header */
title: string; title: string;
/** Modal content */ /** Modal content */
body: React.ReactNode; body: React.ReactNode;
/** Modal description */
description?: React.ReactNode;
/** Text for confirm button */ /** Text for confirm button */
confirmText: string; confirmText: string;
/** Text for dismiss button */ /** Text for dismiss button */
dismissText?: string; dismissText?: string;
/** Icon for the modal header */ /** Icon for the modal header */
icon?: IconName; icon?: IconName;
/** Text user needs to fill in before confirming */
confirmationText?: string;
/** Text for alternative button */
alternativeText?: string;
/** Confirm action callback */ /** Confirm action callback */
onConfirm(): void; onConfirm(): void;
/** Dismiss action callback */ /** Dismiss action callback */
onDismiss(): void; onDismiss(): void;
/** Alternative action callback */
onAlternative?(): void;
} }
export const ConfirmModal: FC<Props> = ({ export const ConfirmModal = ({
isOpen, isOpen,
title, title,
body, body,
description,
confirmText, confirmText,
confirmationText,
dismissText = 'Cancel', dismissText = 'Cancel',
alternativeText,
icon = 'exclamation-triangle', icon = 'exclamation-triangle',
onConfirm, onConfirm,
onDismiss, onDismiss,
}) => { onAlternative,
const theme = useContext(ThemeContext); }: ConfirmModalProps): JSX.Element => {
const styles = getStyles(theme); const [disabled, setDisabled] = useState(Boolean(confirmationText));
const styles = useStyles(getStyles);
const onConfirmationTextChange = (event: React.FormEvent<HTMLInputElement>) => {
setDisabled(confirmationText?.localeCompare(event.currentTarget.value) !== 0);
};
return ( return (
<Modal className={styles.modal} title={title} icon={icon} isOpen={isOpen} onDismiss={onDismiss}> <Modal className={styles.modal} title={title} icon={icon} isOpen={isOpen} onDismiss={onDismiss}>
<div className={styles.modalContent}> <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"> <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} {confirmText}
</Button> </Button>
<Button variant="secondary" onClick={onDismiss}> <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` modal: css`
width: 500px; width: 500px;
`, `,
modalContent: css` modalContent: css`
text-align: center; text-align: center;
`, `,
modalText: css` modalText: css({
font-size: ${theme.typography.heading.h4}; fontSize: theme.v2.typography.h4.fontSize,
color: ${theme.colors.link}; color: theme.v2.palette.text.primary,
margin-bottom: calc(${theme.spacing.d} * 2); marginBottom: `calc(${theme.v2.spacing(2)}*2)`,
padding-top: ${theme.spacing.d}; 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 { TagList } from './Tags/TagList';
export { FilterPill } from './FilterPill/FilterPill'; export { FilterPill } from './FilterPill/FilterPill';
export { ConfirmModal } from './ConfirmModal/ConfirmModal'; export { ConfirmModal, ConfirmModalProps } from './ConfirmModal/ConfirmModal';
export { QueryField } from './QueryField/QueryField'; export { QueryField } from './QueryField/QueryField';
// Code editor // Code editor

View File

@@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { selectors } from '@grafana/e2e-selectors';
import coreModule from 'app/core/core_module'; import coreModule from 'app/core/core_module';
import appEvents from 'app/core/app_events'; 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 { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
import { AngularModalProxy } from '../components/modals/AngularModalProxy'; import { AngularModalProxy } from '../components/modals/AngularModalProxy';
import { provideTheme } from '../utils/ConfigProvider'; 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 { export class UtilSrv {
modalScope: any; modalScope: any;
@@ -84,35 +91,50 @@ export class UtilSrv {
}); });
} }
showConfirmModal(payload: any) { showConfirmModal(payload: ShowConfirmModalPayload) {
const scope: any = this.$rootScope.$new(); const {
confirmText,
scope.updateConfirmText = (value: any) => { onConfirm = () => undefined,
scope.confirmTextValid = payload.confirmText.toLowerCase() === value.toLowerCase(); 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; const elem = React.createElement(provideTheme(AngularModalProxy), modalProps);
scope.text = payload.text; this.reactModalRoot.appendChild(this.reactModalNode);
scope.text2 = payload.text2; ReactDOM.render(elem, this.reactModalNode);
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',
})
);
} }
} }

View File

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