Commit f24b84fa by Alexander Zobnin Committed by GitHub

UI: ConfirmModal component (#20965)

* UI: ConfirmModal component based on Modal

* UI: refactor ConfirmModal after Modal changes

* UI: use Icon component for Modal

* UI: ConfirmModal tests

* UI: ConfirmModal story
parent 1774b8f7
import React from 'react';
import { storiesOf } from '@storybook/react';
import { text, boolean, select } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { ConfirmModal } from './ConfirmModal';
const getKnobs = () => {
return {
title: text('Title', 'Delete user'),
body: text('Body', 'Are you sure you want to delete this user?'),
confirm: text('Confirm', 'Delete'),
visible: boolean('Visible', true),
icon: select('Icon', ['exclamation-triangle', 'power-off', 'cog', 'lock'], 'exclamation-triangle'),
};
};
const defaultActions = {
onConfirm: () => {
action('Confirmed')('delete');
},
onDismiss: () => {
action('Dismiss')('close');
},
};
const ConfirmModalStories = storiesOf('UI/ConfirmModal', module);
ConfirmModalStories.addDecorator(withCenteredStory);
ConfirmModalStories.add('default', () => {
const { title, body, confirm, icon, visible } = getKnobs();
const { onConfirm, onDismiss } = defaultActions;
return (
<ConfirmModal
isOpen={visible}
title={title}
body={body}
confirmText={confirm}
icon={icon}
onConfirm={onConfirm}
onDismiss={onDismiss}
/>
);
});
import React from 'react';
import { mount } from 'enzyme';
import { ConfirmModal } from './ConfirmModal';
describe('ConfirmModal', () => {
it('renders without error', () => {
mount(
<ConfirmModal
title="Some Title"
body="Some Body"
confirmText="Confirm"
isOpen={true}
onConfirm={() => {}}
onDismiss={() => {}}
/>
);
});
it('renders nothing by default or when isOpen is false', () => {
const wrapper = mount(
<ConfirmModal
title="Some Title"
body="Some Body"
confirmText="Confirm"
isOpen={false}
onConfirm={() => {}}
onDismiss={() => {}}
/>
);
expect(wrapper.html()).toBe(null);
wrapper.setProps({ ...wrapper.props(), isOpen: false });
expect(wrapper.html()).toBe(null);
});
it('renders correct contents', () => {
const wrapper = mount(
<ConfirmModal
title="Some Title"
body="Content"
confirmText="Confirm"
isOpen={true}
onConfirm={() => {}}
onDismiss={() => {}}
/>
);
expect(wrapper.contains('Some Title')).toBeTruthy();
expect(wrapper.contains('Content')).toBeTruthy();
expect(wrapper.contains('Confirm')).toBeTruthy();
});
});
import React, { FC, useContext } from 'react';
import { css } from 'emotion';
import { Modal } from '../Modal/Modal';
import { IconType } from '../Icon/types';
import { Button } from '../Button/Button';
import { stylesFactory, ThemeContext } from '../../themes';
import { GrafanaTheme } from '@grafana/data';
const getStyles = stylesFactory((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};
`,
modalButtonRow: css`
margin-bottom: 14px;
a,
button {
margin-right: ${theme.spacing.d};
}
`,
}));
const defaultIcon: IconType = 'exclamation-triangle';
interface Props {
isOpen: boolean;
title: string;
body: string;
confirmText: string;
icon?: IconType;
onConfirm(): void;
onDismiss(): void;
}
export const ConfirmModal: FC<Props> = ({ isOpen, title, body, confirmText, icon, onConfirm, onDismiss }) => {
const theme = useContext(ThemeContext);
const styles = getStyles(theme);
return (
<Modal className={styles.modal} title={title} icon={icon || defaultIcon} isOpen={isOpen} onDismiss={onDismiss}>
<div className={styles.modalContent}>
<div className={styles.modalText}>{body}</div>
<div className={styles.modalButtonRow}>
<Button variant="danger" onClick={onConfirm}>
{confirmText}
</Button>
<Button variant="inverse" onClick={onDismiss}>
Cancel
</Button>
</div>
</div>
</Modal>
);
};
......@@ -3,6 +3,8 @@ import { Portal } from '../Portal/Portal';
import { css, cx } from 'emotion';
import { stylesFactory, withTheme } from '../../themes';
import { GrafanaTheme } from '@grafana/data';
import { Icon } from '../Icon/Icon';
import { IconType } from '../Icon/types';
const getStyles = stylesFactory((theme: GrafanaTheme) => ({
modal: css`
......@@ -43,9 +45,11 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => ({
margin: 0 ${theme.spacing.md};
`,
modalHeaderIcon: css`
position: relative;
top: 2px;
padding-right: ${theme.spacing.md};
margin-right: ${theme.spacing.md};
font-size: inherit;
&:before {
vertical-align: baseline;
}
`,
modalHeaderClose: css`
margin-left: auto;
......@@ -60,9 +64,10 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => ({
}));
interface Props {
icon?: string;
icon?: IconType;
title: string | JSX.Element;
theme: GrafanaTheme;
className?: string;
isOpen?: boolean;
onDismiss?: () => void;
......@@ -88,14 +93,14 @@ export class UnthemedModal extends React.PureComponent<Props> {
return (
<h2 className={styles.modalHeaderTitle}>
{icon && <i className={cx(icon, styles.modalHeaderIcon)} />}
{icon && <Icon name={icon} className={styles.modalHeaderIcon} />}
{title}
</h2>
);
}
render() {
const { title, isOpen = false, theme } = this.props;
const { title, isOpen = false, theme, className } = this.props;
const styles = getStyles(theme);
if (!isOpen) {
......@@ -104,7 +109,7 @@ export class UnthemedModal extends React.PureComponent<Props> {
return (
<Portal>
<div className={styles.modal}>
<div className={cx(styles.modal, className)}>
<div className={styles.modalHeader}>
{typeof title === 'string' ? this.renderDefaultHeader() : title}
<a className={styles.modalHeaderClose} onClick={this.onDismiss}>
......
......@@ -39,6 +39,7 @@ export { TimeOfDayPicker } from './TimePicker/TimeOfDayPicker';
export { List } from './List/List';
export { TagsInput } from './TagsInput/TagsInput';
export { Modal } from './Modal/Modal';
export { ConfirmModal } from './ConfirmModal/ConfirmModal';
export { QueryField } from './QueryField/QueryField';
// Renderless
......
......@@ -48,7 +48,7 @@ export class OrgSwitcher extends React.PureComponent<Props, State> {
const currentOrgId = contextSrv.user.orgId;
return (
<Modal title="Switch Organization" icon="fa fa-random" onDismiss={onDismiss} isOpen={isOpen}>
<Modal title="Switch Organization" icon="random" onDismiss={onDismiss} isOpen={isOpen}>
<table className="filter-table form-inline">
<thead>
<tr>
......
......@@ -39,7 +39,7 @@ export class PanelInspector extends PureComponent<Props, State> {
// TODO? should we get the result with an observable once?
const data = (panel.getQueryRunner() as any).lastResult;
return (
<Modal title={panel.title} icon="fa fa-info-circle" onDismiss={this.onDismiss} isOpen={true}>
<Modal title={panel.title} icon="info-circle" onDismiss={this.onDismiss} isOpen={true}>
<div className={bodyStyle}>
<JSONFormatter json={data} open={2} />
</div>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment