diff --git a/packages/grafana-ui/src/components/ConfirmModal/ConfirmModal.story.tsx b/packages/grafana-ui/src/components/ConfirmModal/ConfirmModal.story.tsx new file mode 100644 index 00000000000..6c601387b3f --- /dev/null +++ b/packages/grafana-ui/src/components/ConfirmModal/ConfirmModal.story.tsx @@ -0,0 +1,45 @@ +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 ( + + ); +}); diff --git a/packages/grafana-ui/src/components/ConfirmModal/ConfirmModal.test.tsx b/packages/grafana-ui/src/components/ConfirmModal/ConfirmModal.test.tsx new file mode 100644 index 00000000000..eb0b017cd4b --- /dev/null +++ b/packages/grafana-ui/src/components/ConfirmModal/ConfirmModal.test.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import { ConfirmModal } from './ConfirmModal'; + +describe('ConfirmModal', () => { + it('renders without error', () => { + mount( + {}} + onDismiss={() => {}} + /> + ); + }); + + it('renders nothing by default or when isOpen is false', () => { + const wrapper = mount( + {}} + onDismiss={() => {}} + /> + ); + expect(wrapper.html()).toBe(null); + + wrapper.setProps({ ...wrapper.props(), isOpen: false }); + expect(wrapper.html()).toBe(null); + }); + + it('renders correct contents', () => { + const wrapper = mount( + {}} + onDismiss={() => {}} + /> + ); + + expect(wrapper.contains('Some Title')).toBeTruthy(); + expect(wrapper.contains('Content')).toBeTruthy(); + expect(wrapper.contains('Confirm')).toBeTruthy(); + }); +}); diff --git a/packages/grafana-ui/src/components/ConfirmModal/ConfirmModal.tsx b/packages/grafana-ui/src/components/ConfirmModal/ConfirmModal.tsx new file mode 100644 index 00000000000..8bddaf35677 --- /dev/null +++ b/packages/grafana-ui/src/components/ConfirmModal/ConfirmModal.tsx @@ -0,0 +1,63 @@ +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 = ({ isOpen, title, body, confirmText, icon, onConfirm, onDismiss }) => { + const theme = useContext(ThemeContext); + const styles = getStyles(theme); + + return ( + + + {body} + + + {confirmText} + + + Cancel + + + + + ); +}; diff --git a/packages/grafana-ui/src/components/Modal/Modal.tsx b/packages/grafana-ui/src/components/Modal/Modal.tsx index bc99a92e8fe..8a608dc479a 100644 --- a/packages/grafana-ui/src/components/Modal/Modal.tsx +++ b/packages/grafana-ui/src/components/Modal/Modal.tsx @@ -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 { return ( - {icon && } + {icon && } {title} ); } 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 { return ( - + {typeof title === 'string' ? this.renderDefaultHeader() : title} diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index d4af4dba87d..f85af5c28f2 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -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 diff --git a/public/app/core/components/OrgSwitcher.tsx b/public/app/core/components/OrgSwitcher.tsx index ef08336848f..c4dd88d38ec 100644 --- a/public/app/core/components/OrgSwitcher.tsx +++ b/public/app/core/components/OrgSwitcher.tsx @@ -48,7 +48,7 @@ export class OrgSwitcher extends React.PureComponent { const currentOrgId = contextSrv.user.orgId; return ( - + diff --git a/public/app/features/dashboard/components/Inspector/PanelInspector.tsx b/public/app/features/dashboard/components/Inspector/PanelInspector.tsx index 980db44cfc7..0fdfb481737 100644 --- a/public/app/features/dashboard/components/Inspector/PanelInspector.tsx +++ b/public/app/features/dashboard/components/Inspector/PanelInspector.tsx @@ -39,7 +39,7 @@ export class PanelInspector extends PureComponent { // TODO? should we get the result with an observable once? const data = (panel.getQueryRunner() as any).lastResult; return ( - +