mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
33621e6f9b
commit
9bb1484dc5
@ -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,
|
||||
};
|
||||
|
@ -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),
|
||||
}),
|
||||
});
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user