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:
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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',
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user