diff --git a/packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.story.tsx b/packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.story.tsx
new file mode 100644
index 00000000000..9aa49d367b2
--- /dev/null
+++ b/packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.story.tsx
@@ -0,0 +1,73 @@
+import React from 'react';
+import { storiesOf } from '@storybook/react';
+import { text, boolean, select } from '@storybook/addon-knobs';
+import { ConfirmButton } from './ConfirmButton';
+import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
+import { action } from '@storybook/addon-actions';
+import { Button } from '../Button/Button';
+
+const getKnobs = () => {
+ return {
+ buttonText: text('Button text', 'Edit'),
+ confirmText: text('Confirm text', 'Save'),
+ confirmVariant: select(
+ 'Confirm variant',
+ {
+ primary: 'primary',
+ secondary: 'secondary',
+ danger: 'danger',
+ inverse: 'inverse',
+ transparent: 'transparent',
+ },
+ 'primary'
+ ),
+ disabled: boolean('Disabled', false),
+ };
+};
+
+storiesOf('UI/ConfirmButton', module)
+ .addDecorator(withCenteredStory)
+ .add('default', () => {
+ const { buttonText, confirmText, confirmVariant, disabled } = getKnobs();
+ return (
+ <>
+
+
+ {
+ action('Saved')('save!');
+ }}
+ >
+ {buttonText}
+
+
+
+ >
+ );
+ })
+ .add('with custom button', () => {
+ const { buttonText, confirmText, confirmVariant, disabled } = getKnobs();
+ return (
+ <>
+
+
+ {
+ action('Saved')('save!');
+ }}
+ >
+
+
+
+
+ >
+ );
+ });
diff --git a/packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.test.tsx b/packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.test.tsx
new file mode 100644
index 00000000000..b708e3cf226
--- /dev/null
+++ b/packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.test.tsx
@@ -0,0 +1,34 @@
+import React from 'react';
+import { ConfirmButton } from './ConfirmButton';
+import { mount, ShallowWrapper } from 'enzyme';
+import { Button } from '../Button/Button';
+
+describe('ConfirmButton', () => {
+ let wrapper: any;
+ let deleted: any;
+
+ beforeAll(() => {
+ deleted = false;
+
+ function deleteItem() {
+ deleted = true;
+ }
+
+ wrapper = mount(
+ deleteItem()}>
+ Delete
+
+ );
+ });
+
+ it('should show confirm delete when clicked', () => {
+ expect(deleted).toBe(false);
+ wrapper
+ .find(Button)
+ .findWhere((n: ShallowWrapper) => {
+ return n.text() === 'Confirm delete' && n.type() === Button;
+ })
+ .simulate('click');
+ expect(deleted).toBe(true);
+ });
+});
diff --git a/packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.tsx b/packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.tsx
new file mode 100644
index 00000000000..9882bff3526
--- /dev/null
+++ b/packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.tsx
@@ -0,0 +1,148 @@
+import React, { PureComponent, SyntheticEvent } from 'react';
+import { cx, css } from 'emotion';
+import { stylesFactory, withTheme } from '../../themes';
+import { GrafanaTheme } from '@grafana/data';
+import { Themeable } from '../../types';
+import { Button } from '../Button/Button';
+import { ButtonVariant } from '../Button/types';
+
+const getStyles = stylesFactory((theme: GrafanaTheme) => {
+ return {
+ buttonContainer: css`
+ direction: rtl;
+ display: flex;
+ align-items: center;
+ `,
+ buttonDisabled: css`
+ text-decoration: none;
+ color: ${theme.colors.text};
+ opacity: 0.65;
+ cursor: not-allowed;
+ pointer-events: none;
+ `,
+ buttonShow: css`
+ opacity: 1;
+ transition: opacity 0.1s ease;
+ z-index: 2;
+ `,
+ buttonHide: css`
+ opacity: 0;
+ transition: opacity 0.1s ease;
+ z-index: 0;
+ `,
+ confirmButtonContainer: css`
+ overflow: hidden;
+ position: absolute;
+ z-index: 1;
+ `,
+ confirmButton: css`
+ display: flex;
+ align-items: flex-start;
+ `,
+ confirmButtonShow: css`
+ opacity: 1;
+ transition: opacity 0.08s ease-out, transform 0.1s ease-out;
+ transform: translateX(0);
+ `,
+ confirmButtonHide: css`
+ opacity: 0;
+ transition: opacity 0.12s ease-in, transform 0.14s ease-in;
+ transform: translateX(100px);
+ `,
+ };
+});
+
+interface Props extends Themeable {
+ className?: string;
+ confirmText?: string;
+ disabled?: boolean;
+ confirmButtonVariant?: ButtonVariant;
+
+ onConfirm(): void;
+ onClick?(): void;
+ onCancel?(): void;
+}
+
+interface State {
+ showConfirm: boolean;
+}
+
+class UnThemedConfirmButton extends PureComponent {
+ static defaultProps: Partial = {
+ confirmText: 'Save',
+ disabled: false,
+ confirmButtonVariant: 'primary',
+ };
+
+ state: State = {
+ showConfirm: false,
+ };
+
+ onClickButton = (event: SyntheticEvent) => {
+ if (event) {
+ event.preventDefault();
+ }
+
+ this.setState({
+ showConfirm: true,
+ });
+
+ if (this.props.onClick) {
+ this.props.onClick();
+ }
+ };
+
+ onClickCancel = (event: SyntheticEvent) => {
+ if (event) {
+ event.preventDefault();
+ }
+ this.setState({
+ showConfirm: false,
+ });
+ if (this.props.onCancel) {
+ this.props.onCancel();
+ }
+ };
+
+ render() {
+ const { onConfirm, disabled, confirmText, confirmButtonVariant, className, theme, children } = this.props;
+ const styles = getStyles(theme);
+ const buttonClass = cx(
+ className,
+ this.state.showConfirm ? styles.buttonHide : styles.buttonShow,
+ disabled && styles.buttonDisabled
+ );
+ const confirmButtonClass = cx(
+ styles.confirmButton,
+ this.state.showConfirm ? styles.confirmButtonShow : styles.confirmButtonHide
+ );
+ const onClick = disabled ? () => {} : this.onClickButton;
+
+ return (
+
+ {typeof children === 'string' ? (
+
+ ) : (
+
+ {children}
+
+ )}
+
+
+
+
+
+
+
+ );
+ }
+}
+
+export const ConfirmButton = withTheme(UnThemedConfirmButton);
+ConfirmButton.displayName = 'ConfirmButton';
diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts
index f85af5c28f2..7f314ee569c 100644
--- a/packages/grafana-ui/src/components/index.ts
+++ b/packages/grafana-ui/src/components/index.ts
@@ -1,4 +1,5 @@
export { DeleteButton } from './DeleteButton/DeleteButton';
+export { ConfirmButton } from './ConfirmButton/ConfirmButton';
export { Tooltip, PopoverContent } from './Tooltip/Tooltip';
export { PopoverController } from './Tooltip/PopoverController';
export { Popover } from './Tooltip/Popover';