diff --git a/packages/grafana-ui/src/components/Button/types.ts b/packages/grafana-ui/src/components/Button/types.ts
index c56a9b0f5cb..a163a06e8ee 100644
--- a/packages/grafana-ui/src/components/Button/types.ts
+++ b/packages/grafana-ui/src/components/Button/types.ts
@@ -1,6 +1,6 @@
import { GrafanaTheme } from '@grafana/data';
-export type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'inverse' | 'transparent' | 'destructive';
+export type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'inverse' | 'transparent' | 'destructive' | 'link';
export type ButtonSize = 'xs' | 'sm' | 'md' | 'lg';
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..c2868af2505
--- /dev/null
+++ b/packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.story.tsx
@@ -0,0 +1,76 @@
+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'),
+ size: select('Size', ['sm', 'md', 'lg'], 'md'),
+ 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 { size, buttonText, confirmText, confirmVariant, disabled } = getKnobs();
+ return (
+ <>
+
+
+ {
+ action('Saved')('save!');
+ }}
+ >
+ {buttonText}
+
+
+
+ >
+ );
+ })
+ .add('with custom button', () => {
+ const { buttonText, confirmText, confirmVariant, disabled, size } = 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..5963d3024ee
--- /dev/null
+++ b/packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.tsx
@@ -0,0 +1,160 @@
+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 Forms from '../Forms';
+import { ButtonVariant, ButtonSize } 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;
+ size?: ButtonSize;
+ confirmText?: string;
+ disabled?: boolean;
+ confirmVariant?: ButtonVariant;
+
+ onConfirm(): void;
+ onClick?(): void;
+ onCancel?(): void;
+}
+
+interface State {
+ showConfirm: boolean;
+}
+
+class UnThemedConfirmButton extends PureComponent {
+ static defaultProps: Partial = {
+ size: 'md',
+ confirmText: 'Save',
+ disabled: false,
+ confirmVariant: '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 {
+ theme,
+ className,
+ size,
+ disabled,
+ confirmText,
+ confirmVariant: confirmButtonVariant,
+ onConfirm,
+ 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}
+
+ ) : (
+
+ {children}
+
+ )}
+
+
+
+
+
+
+
+ );
+ }
+}
+
+export const ConfirmButton = withTheme(UnThemedConfirmButton);
+ConfirmButton.displayName = 'ConfirmButton';
diff --git a/packages/grafana-ui/src/components/ConfirmButton/DeleteButton.story.tsx b/packages/grafana-ui/src/components/ConfirmButton/DeleteButton.story.tsx
new file mode 100644
index 00000000000..b4d5f62ae8b
--- /dev/null
+++ b/packages/grafana-ui/src/components/ConfirmButton/DeleteButton.story.tsx
@@ -0,0 +1,34 @@
+import React from 'react';
+import { storiesOf } from '@storybook/react';
+import { boolean, select } from '@storybook/addon-knobs';
+import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
+import { action } from '@storybook/addon-actions';
+import { DeleteButton } from './DeleteButton';
+
+const getKnobs = () => {
+ return {
+ size: select('Size', ['sm', 'md', 'lg'], 'md'),
+ disabled: boolean('Disabled', false),
+ };
+};
+
+storiesOf('UI/ConfirmButton', module)
+ .addDecorator(withCenteredStory)
+ .add('delete button', () => {
+ const { disabled, size } = getKnobs();
+ return (
+ <>
+
+
+ {
+ action('Deleted')('delete!');
+ }}
+ />
+
+
+ >
+ );
+ });
diff --git a/packages/grafana-ui/src/components/ConfirmButton/DeleteButton.tsx b/packages/grafana-ui/src/components/ConfirmButton/DeleteButton.tsx
new file mode 100644
index 00000000000..2e0fe5b02ff
--- /dev/null
+++ b/packages/grafana-ui/src/components/ConfirmButton/DeleteButton.tsx
@@ -0,0 +1,24 @@
+import React, { FC } from 'react';
+import { ConfirmButton } from './ConfirmButton';
+import { Button } from '../Button/Button';
+import { ButtonSize } from '../Button/types';
+
+interface Props {
+ size?: ButtonSize;
+ disabled?: boolean;
+ onConfirm(): void;
+}
+
+export const DeleteButton: FC = ({ size, disabled, onConfirm }) => {
+ return (
+
+
+
+ );
+};
diff --git a/packages/grafana-ui/src/components/DeleteButton/DeleteButton.story.tsx b/packages/grafana-ui/src/components/DeleteButton/DeleteButton.story.tsx
deleted file mode 100644
index 0f5e85414eb..00000000000
--- a/packages/grafana-ui/src/components/DeleteButton/DeleteButton.story.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import React from 'react';
-import { storiesOf } from '@storybook/react';
-import { DeleteButton } from './DeleteButton';
-import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
-import { action } from '@storybook/addon-actions';
-
-storiesOf('UI/DeleteButton', module)
- .addDecorator(withCenteredStory)
- .add('default', () => {
- return (
- {
- action('Delete Confirmed')('delete!');
- }}
- />
- );
- });
diff --git a/packages/grafana-ui/src/components/DeleteButton/DeleteButton.test.tsx b/packages/grafana-ui/src/components/DeleteButton/DeleteButton.test.tsx
deleted file mode 100644
index f6d5a676971..00000000000
--- a/packages/grafana-ui/src/components/DeleteButton/DeleteButton.test.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import React from 'react';
-import { DeleteButton } from './DeleteButton';
-import { shallow } from 'enzyme';
-
-describe('DeleteButton', () => {
- let wrapper: any;
- let deleted: any;
-
- beforeAll(() => {
- deleted = false;
-
- function deleteItem() {
- deleted = true;
- }
-
- wrapper = shallow( deleteItem()} />);
- });
-
- it('should show confirm delete when clicked', () => {
- expect(wrapper.state().showConfirm).toBe(false);
- wrapper.find('.delete-button').simulate('click');
- expect(wrapper.state().showConfirm).toBe(true);
- });
-
- it('should hide confirm delete when clicked', () => {
- wrapper.find('.delete-button').simulate('click');
- expect(wrapper.state().showConfirm).toBe(true);
- wrapper
- .find('.confirm-delete')
- .find('.btn')
- .at(0)
- .simulate('click');
- expect(wrapper.state().showConfirm).toBe(false);
- });
-
- it('should show confirm delete when clicked', () => {
- expect(deleted).toBe(false);
- wrapper
- .find('.confirm-delete')
- .find('.btn')
- .at(1)
- .simulate('click');
- expect(deleted).toBe(true);
- });
-});
diff --git a/packages/grafana-ui/src/components/DeleteButton/DeleteButton.tsx b/packages/grafana-ui/src/components/DeleteButton/DeleteButton.tsx
deleted file mode 100644
index d262c821968..00000000000
--- a/packages/grafana-ui/src/components/DeleteButton/DeleteButton.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-import React, { PureComponent, SyntheticEvent } from 'react';
-
-interface Props {
- onConfirm(): void;
- disabled?: boolean;
-}
-
-interface State {
- showConfirm: boolean;
-}
-
-export class DeleteButton extends PureComponent {
- state: State = {
- showConfirm: false,
- };
-
- onClickDelete = (event: SyntheticEvent) => {
- if (event) {
- event.preventDefault();
- }
-
- this.setState({
- showConfirm: true,
- });
- };
-
- onClickCancel = (event: SyntheticEvent) => {
- if (event) {
- event.preventDefault();
- }
- this.setState({
- showConfirm: false,
- });
- };
-
- render() {
- const { onConfirm, disabled } = this.props;
- const showConfirmClass = this.state.showConfirm ? 'show' : 'hide';
- const showDeleteButtonClass = this.state.showConfirm ? 'hide' : 'show';
- const disabledClass = disabled ? 'disabled btn-inverse' : '';
- const onClick = disabled ? () => {} : this.onClickDelete;
-
- return (
-
-
-
-
-
-
-
- Cancel
-
-
- Confirm Delete
-
-
-
-
- );
- }
-}
diff --git a/packages/grafana-ui/src/components/DeleteButton/_DeleteButton.scss b/packages/grafana-ui/src/components/DeleteButton/_DeleteButton.scss
deleted file mode 100644
index e56a1181a09..00000000000
--- a/packages/grafana-ui/src/components/DeleteButton/_DeleteButton.scss
+++ /dev/null
@@ -1,50 +0,0 @@
-// sets a fixed width so that the rest of the table
-// isn't affected by the animation
-.delete-button-container {
- width: 24px;
- direction: rtl;
- display: flex;
- align-items: center;
-}
-
-//this container is used to make sure confirm-delete isn't
-//shown outside of table
-.confirm-delete-container {
- overflow: hidden;
- width: 145px;
- position: absolute;
- z-index: 1;
-}
-
-.delete-button {
- position: absolute;
-
- &.show {
- opacity: 1;
- transition: opacity 0.1s ease;
- z-index: 2;
- }
-
- &.hide {
- opacity: 0;
- transition: opacity 0.1s ease;
- z-index: 0;
- }
-}
-
-.confirm-delete {
- display: flex;
- align-items: flex-start;
-
- &.show {
- opacity: 1;
- transition: opacity 0.08s ease-out, transform 0.1s ease-out;
- transform: translateX(0);
- }
-
- &.hide {
- opacity: 0;
- transition: opacity 0.12s ease-in, transform 0.14s ease-in;
- transform: translateX(100px);
- }
-}
diff --git a/packages/grafana-ui/src/components/Forms/Button.story.tsx b/packages/grafana-ui/src/components/Forms/Button.story.tsx
index f7abae1fa2e..42416d085e2 100644
--- a/packages/grafana-ui/src/components/Forms/Button.story.tsx
+++ b/packages/grafana-ui/src/components/Forms/Button.story.tsx
@@ -16,7 +16,7 @@ export default {
},
};
-const variants = ['primary', 'secondary', 'destructive'];
+const variants = ['primary', 'secondary', 'destructive', 'link'];
const sizes = ['sm', 'md', 'lg'];
diff --git a/packages/grafana-ui/src/components/Forms/Button.tsx b/packages/grafana-ui/src/components/Forms/Button.tsx
index 73fe733e39e..084c764ee72 100644
--- a/packages/grafana-ui/src/components/Forms/Button.tsx
+++ b/packages/grafana-ui/src/components/Forms/Button.tsx
@@ -52,6 +52,19 @@ const getPropertiesForVariant = (theme: GrafanaTheme, variant: ButtonVariant) =>
background: buttonVariantStyles(theme.colors.redBase, theme.colors.redShade, theme.colors.white),
};
+ case 'link':
+ return {
+ borderColor: 'transparent',
+ background: buttonVariantStyles('transparent', 'transparent', theme.colors.linkExternal),
+ variantStyles: css`
+ text-decoration: underline;
+ &:focus {
+ outline: none;
+ box-shadow: none;
+ }
+ `,
+ };
+
case 'primary':
default:
return {
@@ -65,7 +78,7 @@ const getPropertiesForVariant = (theme: GrafanaTheme, variant: ButtonVariant) =>
type StyleProps = Omit & { variant: ButtonVariant };
export const getButtonStyles = stylesFactory(({ theme, size, variant }: StyleProps) => {
const { padding, fontSize, height } = getPropertiesForButtonSize(theme, size);
- const { background, borderColor } = getPropertiesForVariant(theme, variant);
+ const { background, borderColor, variantStyles } = getPropertiesForVariant(theme, variant);
return {
button: cx(
@@ -93,7 +106,10 @@ export const getButtonStyles = stylesFactory(({ theme, size, variant }: StylePro
box-shadow: none;
}
`,
- getFocusStyle(theme)
+ getFocusStyle(theme),
+ css`
+ ${variantStyles}
+ `
),
iconWrap: css`
label: button-icon-wrap;
@@ -103,8 +119,8 @@ export const getButtonStyles = stylesFactory(({ theme, size, variant }: StylePro
};
});
-// These are different from the standard Button where there are 5 variants.
-export type ButtonVariant = 'primary' | 'secondary' | 'destructive';
+// These are different from the standard Button where there are more variants.
+export type ButtonVariant = 'primary' | 'secondary' | 'destructive' | 'link';
// These also needs to be different because the ButtonVariant is different
type CommonProps = {
diff --git a/packages/grafana-ui/src/components/Forms/index.ts b/packages/grafana-ui/src/components/Forms/index.ts
index 0e578623beb..4670dda93ee 100644
--- a/packages/grafana-ui/src/components/Forms/index.ts
+++ b/packages/grafana-ui/src/components/Forms/index.ts
@@ -1,11 +1,13 @@
import { getFormStyles } from './getFormStyles';
import { Label } from './Label';
import { Input } from './Input/Input';
+import { Button } from './Button';
const Forms = {
getFormStyles,
Label: Label,
Input: Input,
+ Button: Button,
};
export default Forms;
diff --git a/packages/grafana-ui/src/components/index.scss b/packages/grafana-ui/src/components/index.scss
index 04d7ece8a3c..9b32d59e278 100644
--- a/packages/grafana-ui/src/components/index.scss
+++ b/packages/grafana-ui/src/components/index.scss
@@ -2,7 +2,6 @@
@import 'Cascader/Cascader';
@import 'ColorPicker/ColorPicker';
@import 'CustomScrollbar/CustomScrollbar';
-@import 'DeleteButton/DeleteButton';
@import 'Drawer/Drawer';
@import 'EmptySearchResult/EmptySearchResult';
@import 'FormField/FormField';
diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts
index f85af5c28f2..6b9ada39e07 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 { DeleteButton } from './ConfirmButton/DeleteButton';
export { Tooltip, PopoverContent } from './Tooltip/Tooltip';
export { PopoverController } from './Tooltip/PopoverController';
export { Popover } from './Tooltip/Popover';
diff --git a/public/app/features/api-keys/ApiKeysPage.tsx b/public/app/features/api-keys/ApiKeysPage.tsx
index b374b459d2e..ebfe0dd6a81 100644
--- a/public/app/features/api-keys/ApiKeysPage.tsx
+++ b/public/app/features/api-keys/ApiKeysPage.tsx
@@ -12,7 +12,7 @@ import ApiKeysAddedModal from './ApiKeysAddedModal';
import config from 'app/core/config';
import appEvents from 'app/core/app_events';
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
-import { DeleteButton, EventsWithValidation, FormLabel, Input, Switch, ValidationEvents } from '@grafana/ui';
+import { EventsWithValidation, FormLabel, Input, Switch, ValidationEvents, DeleteButton } from '@grafana/ui';
import { NavModel, dateTime, isDateTime } from '@grafana/data';
import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
import { store } from 'app/store/store';
@@ -287,7 +287,7 @@ export class ApiKeysPage extends PureComponent {
{key.role} |
{this.formatDate(key.expiration)} |
- this.onDeleteApiKey(key)} />
+ this.onDeleteApiKey(key)} />
|
);
diff --git a/public/app/features/teams/TeamList.tsx b/public/app/features/teams/TeamList.tsx
index fde2d9028f0..75ba2f3642b 100644
--- a/public/app/features/teams/TeamList.tsx
+++ b/public/app/features/teams/TeamList.tsx
@@ -66,7 +66,7 @@ export class TeamList extends PureComponent {
{team.memberCount}
- this.deleteTeam(team)} disabled={!canDelete} />
+ this.deleteTeam(team)} />
|
);
diff --git a/public/app/features/teams/TeamMemberRow.tsx b/public/app/features/teams/TeamMemberRow.tsx
index a22b0cac531..4b866ed2e41 100644
--- a/public/app/features/teams/TeamMemberRow.tsx
+++ b/public/app/features/teams/TeamMemberRow.tsx
@@ -1,6 +1,6 @@
import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
-import { DeleteButton, Select } from '@grafana/ui';
+import { Select, DeleteButton } from '@grafana/ui';
import { SelectableValue } from '@grafana/data';
import { TeamMember, teamsPermissionLevels, TeamPermissionLevel } from 'app/types';
@@ -86,7 +86,7 @@ export class TeamMemberRow extends PureComponent {
{this.renderPermissions(member)}
{syncEnabled && this.renderLabels(member.labels)}
- this.onRemoveMember(member)} disabled={!signedInUserIsTeamAdmin} />
+ this.onRemoveMember(member)} />
|
);
diff --git a/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap
index 1017f0d9460..e2fa7718142 100644
--- a/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap
+++ b/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap
@@ -132,9 +132,10 @@ exports[`Render should render teams table 1`] = `
-
|
@@ -183,9 +184,10 @@ exports[`Render should render teams table 1`] = `
-
|
@@ -234,9 +236,10 @@ exports[`Render should render teams table 1`] = `
-
|
@@ -285,9 +288,10 @@ exports[`Render should render teams table 1`] = `
-
|
@@ -336,9 +340,10 @@ exports[`Render should render teams table 1`] = `
-
|
@@ -462,9 +467,10 @@ exports[`Render when feature toggle editorsCanAdmin is turned on and signedin us
-
|
@@ -588,9 +594,10 @@ exports[`Render when feature toggle editorsCanAdmin is turned on and signedin us
-
|
diff --git a/public/app/features/teams/__snapshots__/TeamMemberRow.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamMemberRow.test.tsx.snap
index 630fd712da4..1c70ef18bf4 100644
--- a/public/app/features/teams/__snapshots__/TeamMemberRow.test.tsx.snap
+++ b/public/app/features/teams/__snapshots__/TeamMemberRow.test.tsx.snap
@@ -48,9 +48,10 @@ exports[`Render should render team members when sync enabled 1`] = `
-
|
@@ -137,9 +138,10 @@ exports[`Render when feature toggle editorsCanAdmin is turned off should not ren
-
|
@@ -226,9 +228,10 @@ exports[`Render when feature toggle editorsCanAdmin is turned on should render p
-
|
@@ -273,9 +276,10 @@ exports[`Render when feature toggle editorsCanAdmin is turned on should render s
-
|