Commit 5cdb8f8e by Tobias Skarhed Committed by GitHub

Form Migrations: Button (#23019)

* Update legacy exports and fix Type errors

* Remove Button and LinkButton from Forms namespace

* Fix errors

* Update snapshots

* Move Legacy button

* Migrate more Buttons

* Remove legacy button dependency

* Move button up

* Remove legacy button

* Update Snapshots

* Fix ComponentSize issues

* Switch primary button

* Switch primary

* Add classNames and fix some angular directive issues

* Fix failing build and remove log

Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
parent f63877f2
......@@ -59,5 +59,22 @@ Used for removing or deleting entities.
</div>
</Preview>
## Link
Used for for hyperlinks.
<Preview>
<div>
<Button href="/" variant="link" size="sm" renderAs="button" style={{ margin: '5px' }}>
Small
</Button>
<Button href="/" variant="link" size="md" renderAs="button" style={{ margin: '5px' }}>
Medium
</Button>
<Button href="/" variant="link" size="lg" renderAs="button" style={{ margin: '5px' }}>
Large
</Button>
</div>
</Preview>
<Props of={Button} />
import { storiesOf } from '@storybook/react';
import { Button, LinkButton } from './Button';
// @ts-ignore
import withPropsCombinations from 'react-storybook-addon-props-combinations';
import { action } from '@storybook/addon-actions';
import { ThemeableCombinationsRowRenderer } from '../../utils/storybook/CombinationsRowRenderer';
import { boolean } from '@storybook/addon-knobs';
import React from 'react';
import { select, text } from '@storybook/addon-knobs';
import { Button, ButtonVariant } from './Button';
import { withCenteredStory, withHorizontallyCenteredStory } from '../../utils/storybook/withCenteredStory';
import { getIconKnob } from '../../utils/storybook/knobs';
import mdx from './Button.mdx';
import { ComponentSize } from '../../types/size';
const ButtonStories = storiesOf('General/Button', module);
const defaultProps = {
onClick: [action('Button clicked')],
children: ['Click click!'],
};
const variants = {
size: ['xs', 'sm', 'md', 'lg'],
variant: ['primary', 'secondary', 'danger', 'inverse', 'transparent', 'link'],
};
const combinationOptions = {
CombinationRenderer: ThemeableCombinationsRowRenderer,
export default {
title: 'Forms/Button',
component: Button,
decorators: [withCenteredStory, withHorizontallyCenteredStory],
parameters: {
docs: {
page: mdx,
},
},
};
const renderButtonStory = (buttonComponent: typeof Button | typeof LinkButton) => {
const isDisabled = boolean('Disable button', false);
return withPropsCombinations(
buttonComponent,
{ ...variants, ...defaultProps, disabled: [isDisabled] },
combinationOptions
)();
};
ButtonStories.add('as button element', () => renderButtonStory(Button));
const variants = ['primary', 'secondary', 'destructive', 'link'];
ButtonStories.add('as link element', () => renderButtonStory(LinkButton));
const sizes = ['sm', 'md', 'lg'];
ButtonStories.add('with icon', () => {
export const simple = () => {
const variant = select('Variant', variants, 'primary');
const size = select('Size', sizes, 'md');
const buttonText = text('text', 'Button');
const icon = getIconKnob();
return withPropsCombinations(
Button,
{ ...variants, ...defaultProps, icon: [icon && `fa fa-${icon}`] },
combinationOptions
)();
});
return (
<Button variant={variant as ButtonVariant} size={size as ComponentSize} icon={icon && `fa fa-${icon}`}>
{buttonText}
</Button>
);
};
import React from 'react';
import { Button, LinkButton } from './Button';
import { mount } from 'enzyme';
describe('Button', () => {
it('renders correct html', () => {
const wrapper = mount(<Button icon={'fa fa-plus'}>Click me</Button>);
expect(wrapper.html()).toMatchSnapshot();
});
});
describe('LinkButton', () => {
it('renders correct html', () => {
const wrapper = mount(<LinkButton icon={'fa fa-plus'}>Click me</LinkButton>);
expect(wrapper.html()).toMatchSnapshot();
});
it('allows a disable state on link button', () => {
const wrapper = mount(
<LinkButton disabled icon={'fa fa-plus'}>
Click me
</LinkButton>
);
expect(wrapper.find('a[disabled]').length).toBe(1);
});
});
import React, { AnchorHTMLAttributes, ButtonHTMLAttributes, useContext } from 'react';
import { ThemeContext } from '../../themes';
import { getButtonStyles } from './styles';
import { css, cx } from 'emotion';
import tinycolor from 'tinycolor2';
import { selectThemeVariant, stylesFactory, ThemeContext } from '../../themes';
import { getFocusStyle, getPropertiesForButtonSize } from '../Forms/commonStyles';
import { GrafanaTheme } from '@grafana/data';
import { ButtonContent } from './ButtonContent';
import { ComponentSize } from '../../types/size';
import { ButtonStyles, ButtonVariant } from './types';
import { cx } from 'emotion';
const buttonVariantStyles = (from: string, to: string, textColor: string) => css`
background: linear-gradient(180deg, ${from} 0%, ${to} 100%);
color: ${textColor};
&:hover {
background: ${from};
color: ${textColor};
}
&:focus {
background: ${from};
outline: none;
}
`;
const getPropertiesForVariant = (theme: GrafanaTheme, variant: ButtonVariant) => {
switch (variant) {
case 'secondary':
const from = selectThemeVariant({ light: theme.colors.gray7, dark: theme.colors.gray15 }, theme.type) as string;
const to = selectThemeVariant(
{
light: tinycolor(from)
.darken(5)
.toString(),
dark: tinycolor(from)
.lighten(4)
.toString(),
},
theme.type
) as string;
return {
borderColor: selectThemeVariant({ light: theme.colors.gray85, dark: theme.colors.gray25 }, theme.type),
background: buttonVariantStyles(
from,
to,
selectThemeVariant({ light: theme.colors.gray25, dark: theme.colors.gray4 }, theme.type) as string
),
};
case 'destructive':
return {
borderColor: theme.colors.redShade,
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`
&:focus {
outline: none;
box-shadow: none;
}
`,
};
case 'primary':
default:
return {
borderColor: theme.colors.blueShade,
background: buttonVariantStyles(theme.colors.blueBase, theme.colors.blueShade, theme.colors.white),
};
}
};
export interface StyleProps {
theme: GrafanaTheme;
size: ComponentSize;
variant: ButtonVariant;
textAndIcon?: boolean;
}
export const getButtonStyles = stylesFactory(({ theme, size, variant }: StyleProps) => {
const { padding, fontSize, height } = getPropertiesForButtonSize(theme, size);
const { background, borderColor, variantStyles } = getPropertiesForVariant(theme, variant);
return {
button: cx(
css`
label: button;
display: inline-flex;
align-items: center;
font-weight: ${theme.typography.weight.semibold};
font-family: ${theme.typography.fontFamily.sansSerif};
font-size: ${fontSize};
padding: ${padding};
height: ${height};
vertical-align: middle;
cursor: pointer;
border: 1px solid ${borderColor};
border-radius: ${theme.border.radius.sm};
${background};
&[disabled],
&:disabled {
cursor: not-allowed;
opacity: 0.65;
box-shadow: none;
}
`,
getFocusStyle(theme),
css`
${variantStyles}
`
),
buttonWithIcon: css`
padding-left: ${theme.spacing.sm};
`,
// used for buttons with icon only
iconButton: css`
padding-right: 0;
`,
iconWrap: css`
label: button-icon-wrap;
& + * {
margin-left: ${theme.spacing.sm};
}
`,
};
});
export type ButtonVariant = 'primary' | 'secondary' | 'destructive' | 'link';
type CommonProps = {
size?: ComponentSize;
variant?: ButtonVariant;
/**
* icon prop is a temporary solution. It accepts legacy icon class names for the icon to be rendered.
* TODO: migrate to a component when we are going to migrate icons to @grafana/ui
*/
icon?: string;
className?: string;
styles?: ButtonStyles;
};
export type ButtonProps = CommonProps & ButtonHTMLAttributes<HTMLButtonElement>;
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
const theme = useContext(ThemeContext);
const { size, variant, icon, children, className, styles: stylesProp, ...buttonProps } = props;
// Default this to 'button', otherwise html defaults to 'submit' which then submits any form it is in.
buttonProps.type = buttonProps.type || 'button';
const styles: ButtonStyles =
stylesProp ||
getButtonStyles({
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ variant, icon, children, className, ...otherProps }, ref) => {
const theme = useContext(ThemeContext);
const styles = getButtonStyles({
theme,
size: size || 'md',
size: otherProps.size || 'md',
variant: variant || 'primary',
textAndIcon: !!(children && icon),
});
return (
<button className={cx(styles.button, className)} {...buttonProps} ref={ref}>
<ButtonContent icon={icon}>{children}</ButtonContent>
</button>
);
});
return (
<button className={cx(styles.button, className)} {...otherProps} ref={ref}>
<ButtonContent icon={icon}>{children}</ButtonContent>
</button>
);
}
);
Button.displayName = 'Button';
export type LinkButtonProps = CommonProps &
AnchorHTMLAttributes<HTMLAnchorElement> & {
// We allow disabled here even though it is not standard for a link. We use it as a selector to style it as
// disabled.
disabled?: boolean;
};
export const LinkButton = React.forwardRef<HTMLAnchorElement, LinkButtonProps>((props, ref) => {
const theme = useContext(ThemeContext);
const { size, variant, icon, children, className, styles: stylesProp, ...anchorProps } = props;
const styles: ButtonStyles =
stylesProp ||
getButtonStyles({
type ButtonLinkProps = CommonProps & AnchorHTMLAttributes<HTMLAnchorElement>;
export const LinkButton = React.forwardRef<HTMLAnchorElement, ButtonLinkProps>(
({ variant, icon, children, className, ...otherProps }, ref) => {
const theme = useContext(ThemeContext);
const styles = getButtonStyles({
theme,
size: size || 'md',
size: otherProps.size || 'md',
variant: variant || 'primary',
textAndIcon: !!(children && icon),
});
return (
<a className={cx(styles.button, className)} {...anchorProps} ref={ref}>
<ButtonContent icon={icon}>{children}</ButtonContent>
</a>
);
});
return (
<a className={cx(styles.button, className)} {...otherProps} ref={ref}>
<ButtonContent icon={icon}>{children}</ButtonContent>
</a>
);
}
);
LinkButton.displayName = 'LinkButton';
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Button renders correct html 1`] = `"<button class=\\"css-12s5hlm-button\\" type=\\"button\\"><span class=\\"css-1beih13\\"><span class=\\"css-1rgbe4\\"><i class=\\"fa fa-plus\\"></i></span><span>Click me</span></span></button>"`;
exports[`LinkButton renders correct html 1`] = `"<a class=\\"css-12s5hlm-button\\"><span class=\\"css-1beih13\\"><span class=\\"css-1rgbe4\\"><i class=\\"fa fa-plus\\"></i></span><span>Click me</span></span></a>"`;
import tinycolor from 'tinycolor2';
import { css } from 'emotion';
import { selectThemeVariant, stylesFactory } from '../../themes';
import { ComponentSize } from '../../types/size';
import { StyleDeps } from './types';
import { GrafanaTheme } from '@grafana/data';
const buttonVariantStyles = (
from: string,
to: string,
textColor: string,
textShadowColor = 'rgba(0, 0, 0, 0.1)',
invert = false
) => css`
background: linear-gradient(to bottom, ${from}, ${to});
color: ${textColor};
text-shadow: 0 ${invert ? '1px' : '-1px'} ${textShadowColor};
&:hover {
background: ${from};
color: ${textColor};
}
&:focus {
background: ${from};
outline: none;
}
`;
export const getButtonStyles = stylesFactory(({ theme, size, variant, textAndIcon }: StyleDeps) => {
const borderRadius = theme.border.radius.sm;
const { padding, fontSize, height, fontWeight } = calculateMeasures(theme, size, !!textAndIcon);
let background;
switch (variant) {
case 'primary':
background = buttonVariantStyles(theme.colors.greenBase, theme.colors.greenShade, theme.colors.white);
break;
case 'secondary':
background = buttonVariantStyles(theme.colors.blueBase, theme.colors.blueShade, theme.colors.white);
break;
case 'danger':
background = buttonVariantStyles(theme.colors.redBase, theme.colors.redShade, theme.colors.white);
break;
case 'inverse':
const from = selectThemeVariant({ light: theme.colors.gray5, dark: theme.colors.dark6 }, theme.type) as string;
const to = selectThemeVariant(
{
light: tinycolor(from)
.darken(5)
.toString(),
dark: tinycolor(from)
.lighten(4)
.toString(),
},
theme.type
) as string;
background = buttonVariantStyles(from, to, theme.colors.link, 'rgba(0, 0, 0, 0.1)', true);
break;
case 'transparent':
background = css`
${buttonVariantStyles('', '', theme.colors.link, 'rgba(0, 0, 0, 0.1)', true)};
background: transparent;
`;
break;
case 'link':
background = css`
${buttonVariantStyles('', '', theme.colors.linkExternal, 'rgba(0, 0, 0, 0.1)', true)};
background: transparent;
`;
break;
}
return {
button: css`
label: button;
display: inline-flex;
align-items: center;
font-weight: ${fontWeight};
font-size: ${fontSize};
font-family: ${theme.typography.fontFamily.sansSerif};
line-height: ${theme.typography.lineHeight.md};
padding: ${padding};
vertical-align: middle;
cursor: pointer;
border: none;
height: ${height};
border-radius: ${borderRadius};
${background};
&[disabled],
&:disabled {
cursor: not-allowed;
opacity: 0.65;
box-shadow: none;
}
`,
iconWrap: css`
label: button-icon-wrap;
& + * {
margin-left: ${theme.spacing.sm};
}
`,
};
});
type ButtonMeasures = {
padding: string;
fontSize: string;
height: string;
fontWeight: number;
};
const calculateMeasures = (theme: GrafanaTheme, size: ComponentSize, textAndIcon: boolean): ButtonMeasures => {
switch (size) {
case 'sm': {
return {
padding: `0 ${theme.spacing.sm}`,
fontSize: theme.typography.size.sm,
height: theme.height.sm,
fontWeight: theme.typography.weight.semibold,
};
}
case 'md': {
const leftPadding = textAndIcon ? theme.spacing.sm : theme.spacing.md;
return {
padding: `0 ${theme.spacing.md} 0 ${leftPadding}`,
fontSize: theme.typography.size.md,
height: theme.height.md,
fontWeight: theme.typography.weight.semibold,
};
}
case 'lg': {
const leftPadding = textAndIcon ? theme.spacing.md : theme.spacing.lg;
return {
padding: `0 ${theme.spacing.lg} 0 ${leftPadding}`,
fontSize: theme.typography.size.lg,
height: theme.height.lg,
fontWeight: theme.typography.weight.regular,
};
}
default: {
const leftPadding = textAndIcon ? theme.spacing.sm : theme.spacing.md;
return {
padding: `0 ${theme.spacing.md} 0 ${leftPadding}`,
fontSize: theme.typography.size.base,
height: theme.height.md,
fontWeight: theme.typography.weight.regular,
};
}
}
};
import { GrafanaTheme } from '@grafana/data';
import { ComponentSize } from '../../types/size';
export type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'inverse' | 'transparent' | 'destructive' | 'link';
export interface StyleDeps {
theme: GrafanaTheme;
size: ComponentSize;
variant: ButtonVariant;
textAndIcon?: boolean;
}
export interface ButtonStyles {
button: string;
iconWrap: string;
icon?: string;
}
import React, { PureComponent } from 'react';
import Clipboard from 'clipboard';
import { Button, ButtonProps } from '../Button/Button';
import { Button, ButtonProps } from '../Button';
interface Props extends ButtonProps {
getText(): string;
......
export { ClipboardButton } from '../ClipboardButton/ClipboardButton';
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';
import { Button } from '../Button';
const getKnobs = () => {
return {
......@@ -16,9 +17,8 @@ const getKnobs = () => {
{
primary: 'primary',
secondary: 'secondary',
danger: 'danger',
inverse: 'inverse',
transparent: 'transparent',
destructive: 'destructive',
link: 'link',
},
'primary'
),
......
import React from 'react';
import { ConfirmButton } from './ConfirmButton';
import { mount, ShallowWrapper } from 'enzyme';
import { Button } from '../Button/Button';
import { Button } from '../Button';
describe('ConfirmButton', () => {
let wrapper: any;
......
......@@ -4,9 +4,7 @@ import { stylesFactory, withTheme } from '../../themes';
import { GrafanaTheme } from '@grafana/data';
import { Themeable } from '../../types';
import { ComponentSize } from '../../types/size';
import { Button } from '../Button/Button';
import Forms from '../Forms';
import { ButtonVariant } from '../Button/types';
import { Button, ButtonVariant } from '../Button';
const getStyles = stylesFactory((theme: GrafanaTheme) => {
return {
......@@ -135,9 +133,9 @@ class UnThemedConfirmButton extends PureComponent<Props, State> {
<span className={styles.buttonContainer}>
{typeof children === 'string' ? (
<span className={buttonClass}>
<Forms.Button size={size} variant="link" onClick={onClick}>
<Button size={size} variant="link" onClick={onClick}>
{children}
</Forms.Button>
</Button>
</span>
) : (
<span className={buttonClass} onClick={onClick}>
......@@ -146,7 +144,7 @@ class UnThemedConfirmButton extends PureComponent<Props, State> {
)}
<span className={styles.confirmButtonContainer}>
<span className={confirmButtonClass}>
<Button size={size} variant="transparent" onClick={this.onClickCancel}>
<Button size={size} variant="secondary" onClick={this.onClickCancel}>
Cancel
</Button>
<Button size={size} variant={confirmButtonVariant} onClick={onConfirm}>
......
import React, { FC } from 'react';
import { ConfirmButton } from './ConfirmButton';
import { Button } from '../Button/Button';
import { ComponentSize } from '../../types/size';
import { Button } from '../Button';
interface Props {
size?: ComponentSize;
......@@ -13,12 +13,12 @@ export const DeleteButton: FC<Props> = ({ size, disabled, onConfirm }) => {
return (
<ConfirmButton
confirmText="Delete"
confirmVariant="danger"
confirmVariant="destructive"
size={size || 'md'}
disabled={disabled}
onConfirm={onConfirm}
>
<Button variant="danger" icon="fa fa-remove" size={size || 'sm'} />
<Button variant="destructive" icon="fa fa-remove" size={size || 'sm'} />
</ConfirmButton>
);
};
......@@ -2,7 +2,7 @@ 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 { Button } from '../Button';
import { stylesFactory, ThemeContext } from '../../themes';
import { GrafanaTheme } from '@grafana/data';
import { HorizontalGroup } from '..';
......@@ -53,10 +53,10 @@ export const ConfirmModal: FC<Props> = ({
<div className={styles.modalContent}>
<div className={styles.modalText}>{body}</div>
<HorizontalGroup justify="center">
<Button variant="danger" onClick={onConfirm}>
<Button variant="destructive" onClick={onConfirm}>
{confirmText}
</Button>
<Button variant="inverse" onClick={onDismiss}>
<Button variant="secondary" onClick={onDismiss}>
{dismissText}
</Button>
</HorizontalGroup>
......
......@@ -73,7 +73,7 @@ export const DataLinksEditor: FC<DataLinksEditorProps> = React.memo(
)}
{(!value || (value && value.length < (maxLinks || Infinity))) && (
<Button variant="inverse" icon="fa fa-plus" onClick={() => onAdd()}>
<Button variant="secondary" icon="fa fa-plus" onClick={() => onAdd()}>
Add link
</Button>
)}
......
......@@ -2,7 +2,7 @@ import { DataFrame, DataLink, VariableSuggestion } from '@grafana/data';
import React, { FC, useState } from 'react';
import { DataLinkEditor } from '../DataLinkEditor';
import { HorizontalGroup } from '../../Layout/Layout';
import Forms from '../../Forms';
import { Button } from '../../Button';
interface DataLinkEditorModalContentProps {
link: DataLink;
......@@ -34,17 +34,17 @@ export const DataLinkEditorModalContent: FC<DataLinkEditorModalContentProps> = (
onRemove={() => {}}
/>
<HorizontalGroup>
<Forms.Button
<Button
onClick={() => {
onChange(index, dirtyLink);
onClose();
}}
>
Save
</Forms.Button>
<Forms.Button variant="secondary" onClick={() => onClose()}>
</Button>
<Button variant="secondary" onClick={() => onClose()}>
Cancel
</Forms.Button>
</Button>
</HorizontalGroup>
</>
);
......
import { DataFrame, DataLink, GrafanaTheme, VariableSuggestion } from '@grafana/data';
import React, { useState } from 'react';
import { css } from 'emotion';
import Forms from '../../Forms';
import { Button } from '../../Button/Button';
import cloneDeep from 'lodash/cloneDeep';
import { Modal } from '../../Modal/Modal';
import { FullWidthButtonContainer } from '../../Button/FullWidthButtonContainer';
......@@ -100,9 +100,9 @@ export const DataLinksInlineEditor: React.FC<DataLinksInlineEditorProps> = ({ li
)}
<FullWidthButtonContainer>
<Forms.Button size="sm" icon="fa fa-plus" onClick={onDataLinkAdd}>
<Button size="sm" icon="fa fa-plus" onClick={onDataLinkAdd}>
Add link
</Forms.Button>
</Button>
</FullWidthButtonContainer>
</>
);
......
......@@ -57,7 +57,7 @@ describe('Render', () => {
},
},
});
const removeButton = wrapper.find('Button').find({ variant: 'transparent' });
const removeButton = wrapper.find('Button').find({ variant: 'destructive' });
removeButton.simulate('click', { preventDefault: () => {} });
expect(wrapper.find('FormField').exists()).toBeFalsy();
expect(wrapper.find('SecretFormField').exists()).toBeFalsy();
......
......@@ -2,7 +2,7 @@ import React, { PureComponent } from 'react';
import { css } from 'emotion';
import uniqueId from 'lodash/uniqueId';
import { DataSourceSettings } from '@grafana/data';
import { Button } from '../Button/Button';
import { Button } from '../Button';
import { FormField } from '../FormField/FormField';
import { SecretFormField } from '../SecretFormFied/SecretFormField';
import { stylesFactory } from '../../themes';
......@@ -76,7 +76,7 @@ const CustomHeaderRow: React.FC<CustomHeaderRowProps> = ({ header, onBlur, onCha
onChange={e => onChange({ ...header, value: e.target.value })}
onBlur={onBlur}
/>
<Button variant="transparent" size="xs" onClick={_e => onRemove(header.id)}>
<Button variant="destructive" size="xs" onClick={_e => onRemove(header.id)}>
<i className="fa fa-trash" />
</Button>
</div>
......@@ -202,7 +202,7 @@ export class CustomHeadersSettings extends PureComponent<Props, State> {
</div>
<div className="gf-form">
<Button
variant="inverse"
variant="secondary"
size="xs"
onClick={e => {
this.onHeaderAdd();
......
import React from 'react';
import { select, text } from '@storybook/addon-knobs';
import { Button, ButtonVariant } from './Button';
import { withCenteredStory, withHorizontallyCenteredStory } from '../../utils/storybook/withCenteredStory';
import { getIconKnob } from '../../utils/storybook/knobs';
import { ComponentSize } from '../../types/size';
import mdx from './Button.mdx';
export default {
title: 'Forms/Button',
component: Button,
decorators: [withCenteredStory, withHorizontallyCenteredStory],
parameters: {
docs: {
page: mdx,
},
},
};
const variants = ['primary', 'secondary', 'destructive', 'link'];
const sizes = ['sm', 'md', 'lg'];
export const simple = () => {
const variant = select('Variant', variants, 'primary');
const size = select('Size', sizes, 'md');
const buttonText = text('text', 'Button');
const icon = getIconKnob();
return (
<Button variant={variant as ButtonVariant} size={size as ComponentSize} icon={icon && `fa fa-${icon}`}>
{buttonText}
</Button>
);
};
import React, { AnchorHTMLAttributes, ButtonHTMLAttributes, useContext } from 'react';
import { css, cx } from 'emotion';
import tinycolor from 'tinycolor2';
import { selectThemeVariant, stylesFactory, ThemeContext } from '../../themes';
import { Button as DefaultButton, LinkButton as DefaultLinkButton } from '../Button/Button';
import { getFocusStyle, getPropertiesForButtonSize } from './commonStyles';
import { ComponentSize } from '../../types/size';
import { StyleDeps } from '../Button/types';
import { GrafanaTheme } from '@grafana/data';
const buttonVariantStyles = (from: string, to: string, textColor: string) => css`
background: linear-gradient(180deg, ${from} 0%, ${to} 100%);
color: ${textColor};
&:hover {
background: ${from};
color: ${textColor};
}
&:focus {
background: ${from};
outline: none;
}
`;
const getPropertiesForVariant = (theme: GrafanaTheme, variant: ButtonVariant) => {
switch (variant) {
case 'secondary':
const from = selectThemeVariant({ light: theme.colors.gray7, dark: theme.colors.gray15 }, theme.type) as string;
const to = selectThemeVariant(
{
light: tinycolor(from)
.darken(5)
.toString(),
dark: tinycolor(from)
.lighten(4)
.toString(),
},
theme.type
) as string;
return {
borderColor: selectThemeVariant({ light: theme.colors.gray85, dark: theme.colors.gray25 }, theme.type),
background: buttonVariantStyles(
from,
to,
selectThemeVariant({ light: theme.colors.gray25, dark: theme.colors.gray4 }, theme.type) as string
),
};
case 'destructive':
return {
borderColor: theme.colors.redShade,
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`
&:focus {
outline: none;
box-shadow: none;
}
`,
};
case 'primary':
default:
return {
borderColor: theme.colors.blueShade,
background: buttonVariantStyles(theme.colors.blueBase, theme.colors.blueShade, theme.colors.white),
};
}
};
// Need to do this because of mismatch between variants in standard buttons and here
type StyleProps = Omit<StyleDeps, 'variant'> & { variant: ButtonVariant };
export const getButtonStyles = stylesFactory(({ theme, size, variant }: StyleProps) => {
const { padding, fontSize, height } = getPropertiesForButtonSize(theme, size);
const { background, borderColor, variantStyles } = getPropertiesForVariant(theme, variant);
return {
button: cx(
css`
label: button;
display: inline-flex;
align-items: center;
font-weight: ${theme.typography.weight.semibold};
font-family: ${theme.typography.fontFamily.sansSerif};
line-height: ${theme.typography.lineHeight.md};
font-size: ${fontSize};
padding: ${padding};
height: ${height};
vertical-align: middle;
cursor: pointer;
border: 1px solid ${borderColor};
border-radius: ${theme.border.radius.sm};
${background};
&[disabled],
&:disabled {
cursor: not-allowed;
opacity: 0.65;
box-shadow: none;
}
`,
getFocusStyle(theme),
css`
${variantStyles}
`
),
buttonWithIcon: css`
padding-left: ${theme.spacing.sm};
`,
// used for buttons with icon only
iconButton: css`
padding-right: 0;
`,
iconWrap: css`
label: button-icon-wrap;
& + * {
margin-left: ${theme.spacing.sm};
}
`,
};
});
// 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 = {
size?: ComponentSize;
variant?: ButtonVariant;
icon?: string;
className?: string;
};
export type ButtonProps = CommonProps & ButtonHTMLAttributes<HTMLButtonElement>;
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(({ variant, ...otherProps }, ref) => {
const theme = useContext(ThemeContext);
const styles = getButtonStyles({
theme,
size: otherProps.size || 'md',
variant: variant || 'primary',
});
return <DefaultButton {...otherProps} variant={variant} styles={styles} ref={ref} />;
});
type ButtonLinkProps = CommonProps & AnchorHTMLAttributes<HTMLAnchorElement>;
export const LinkButton = React.forwardRef<HTMLAnchorElement, ButtonLinkProps>(({ variant, ...otherProps }, ref) => {
const theme = useContext(ThemeContext);
const styles = getButtonStyles({
theme,
size: otherProps.size || 'md',
variant: variant || 'primary',
});
return <DefaultLinkButton {...otherProps} variant={variant} styles={styles} ref={ref} />;
});
......@@ -5,7 +5,7 @@ import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { withStoryContainer } from '../../utils/storybook/withStoryContainer';
import { Field } from './Field';
import { Input } from './Input/Input';
import { Button } from './Button';
import { Button } from '../Button';
import { Form } from './Form';
import { Switch } from './Switch';
import { Checkbox } from './Checkbox';
......
......@@ -2,7 +2,7 @@ import React from 'react';
import { boolean, text, select, number } from '@storybook/addon-knobs';
import { withCenteredStory } from '../../../utils/storybook/withCenteredStory';
import { Input } from './Input';
import { Button } from '../Button';
import { Button } from '../../Button';
import mdx from './Input.mdx';
import { getAvailableIcons, IconType } from '../../Icon/types';
import { KeyValue } from '@grafana/data';
......
......@@ -2,7 +2,7 @@ import React from 'react';
import { css } from 'emotion';
import { GrafanaTheme } from '@grafana/data';
import { Button, ButtonVariant, ButtonProps } from '../Button';
import { Button, ButtonVariant, ButtonProps } from '../../Button';
import { ComponentSize } from '../../../types/size';
import { SelectCommonProps, CustomControlProps } from './types';
import { SelectBase } from './SelectBase';
......
......@@ -5,7 +5,7 @@ import { SelectableValue } from '@grafana/data';
import { getAvailableIcons, IconType } from '../../Icon/types';
import { select, boolean } from '@storybook/addon-knobs';
import { Icon } from '../../Icon/Icon';
import { Button } from '../Button';
import { Button } from '../../Button';
import { ButtonSelect } from './ButtonSelect';
import { getIconKnob } from '../../../utils/storybook/knobs';
import kebabCase from 'lodash/kebabCase';
......
......@@ -3,7 +3,7 @@ import { GrafanaTheme } from '@grafana/data';
import { getLabelStyles } from './Label';
import { getLegendStyles } from './Legend';
import { getFieldValidationMessageStyles } from './FieldValidationMessage';
import { getButtonStyles, ButtonVariant } from './Button';
import { getButtonStyles, ButtonVariant } from '../Button';
import { ComponentSize } from '../../types/size';
import { getInputStyles } from './Input/Input';
import { getSwitchStyles } from './Switch';
......
......@@ -7,21 +7,22 @@ import { RadioButtonGroup } from './RadioButtonGroup/RadioButtonGroup';
import { AsyncSelect, Select } from './Select/Select';
import { Form } from './Form';
import { Field } from './Field';
import { Button, LinkButton } from './Button';
import { Switch } from './Switch';
import { TextArea } from './TextArea/TextArea';
import { Checkbox } from './Checkbox';
//Will be removed after Enterprise changes have been merged
import { Button, LinkButton } from '../Button';
const Forms = {
RadioButtonGroup,
Button,
LinkButton,
Switch,
getFormStyles,
Label,
Input,
Form,
Field,
Button,
LinkButton,
Select,
ButtonSelect,
InputControl,
......@@ -30,5 +31,4 @@ const Forms = {
Checkbox,
};
export { ButtonVariant } from './Button';
export default Forms;
import React from 'react';
import { withCenteredStory, withHorizontallyCenteredStory } from '../../utils/storybook/withCenteredStory';
import { VerticalGroup, HorizontalGroup, Layout } from './Layout';
import { Button } from '../Forms/Button';
import { Button } from '../Button';
import { withStoryContainer } from '../../utils/storybook/withStoryContainer';
import { select } from '@storybook/addon-knobs';
......@@ -11,7 +11,7 @@ export default {
decorators: [withStoryContainer, withCenteredStory, withHorizontallyCenteredStory],
};
const justifyVariants = ['flex-start', 'flex-end', 'space-between'];
const justifyVariants = ['flex-start', 'flex-end', 'space-betw een'];
const spacingVariants = ['xs', 'sm', 'md', 'lg'];
......
......@@ -127,7 +127,7 @@ class UnThemedLogDetailsRow extends PureComponent<Props, State> {
<>
&nbsp;
<LinkButton
variant={'transparent'}
variant="link"
size={'sm'}
icon={cx('fa', link.onClick ? 'fa-list' : 'fa-external-link')}
href={link.href}
......
import React from 'react';
import { css } from 'emotion';
import { stylesFactory } from '../../themes';
import { Button, ButtonVariant } from '../Forms/Button';
import { Button, ButtonVariant } from '../Button';
interface Props {
currentPage: number;
......
import React, { ChangeEvent, KeyboardEvent, PureComponent } from 'react';
import { css, cx } from 'emotion';
import { stylesFactory } from '../../themes/stylesFactory';
import { Button } from '../Button/Button';
import { Button } from '../Button';
import { Input } from '../Input/Input';
import { TagItem } from './TagItem';
......@@ -106,7 +106,7 @@ export class TagsInput extends PureComponent<Props, State> {
)}
>
<Input placeholder="Add Name" onChange={this.onNameChange} value={newTag} onKeyUp={this.onKeyboardAdd} />
<Button className={getStyles().addButtonStyle} onClick={this.onAdd} variant="secondary" size="md">
<Button className={getStyles().addButtonStyle} onClick={this.onAdd} variant="primary" size="md">
Add
</Button>
</div>
......
......@@ -16,7 +16,7 @@ import { stylesFactory } from '../../themes';
import { Icon } from '../Icon/Icon';
import { RadioButtonGroup } from '../Forms/RadioButtonGroup/RadioButtonGroup';
import { Field } from '../Forms/Field';
import { Button } from '../Forms/Button';
import { Button } from '../Button';
import { FullWidthButtonContainer } from '../Button/FullWidthButtonContainer';
const modes: Array<SelectableValue<ThresholdsMode>> = [
......
......@@ -5,7 +5,7 @@ import { GrafanaTheme, dateTime, TIME_FORMAT } from '@grafana/data';
import { stringToDateTimeType } from '../time';
import { useTheme, stylesFactory } from '../../../themes';
import { TimePickerTitle } from './TimePickerTitle';
import Forms from '../../Forms';
import { Button } from '../../Button';
import { Portal } from '../../Portal/Portal';
import { getThemeColors } from './colors';
import { ClickOutsideWrapper } from '../../ClickOutsideWrapper/ClickOutsideWrapper';
......@@ -281,12 +281,12 @@ const Footer = memo<Props>(({ onClose, onApply }) => {
return (
<div className={styles.container}>
<Forms.Button className={styles.apply} onClick={onApply}>
<Button className={styles.apply} onClick={onApply}>
Apply time range
</Forms.Button>
<Forms.Button variant="secondary" onClick={onClose}>
</Button>
<Button variant="secondary" onClick={onClose}>
Cancel
</Forms.Button>
</Button>
</div>
);
});
......
......@@ -4,6 +4,7 @@ import { stringToDateTimeType, isValidTimeString } from '../time';
import { mapStringsToTimeRange } from './mapper';
import { TimePickerCalendar } from './TimePickerCalendar';
import Forms from '../../Forms';
import { Button } from '../../Button';
interface Props {
isFullscreen: boolean;
......@@ -60,7 +61,7 @@ export const TimeRangeForm: React.FC<Props> = props => {
[timeZone]
);
const icon = isFullscreen ? null : <Forms.Button icon="fa fa-calendar" variant="secondary" onClick={onOpen} />;
const icon = isFullscreen ? null : <Button icon="fa fa-calendar" variant="secondary" onClick={onOpen} />;
return (
<>
......@@ -82,7 +83,7 @@ export const TimeRangeForm: React.FC<Props> = props => {
value={to.value}
/>
</Forms.Field>
<Forms.Button onClick={onApply}>Apply time range</Forms.Button>
<Button onClick={onApply}>Apply time range</Button>
<TimePickerCalendar
isFullscreen={isFullscreen}
......
......@@ -3,7 +3,7 @@ import { Select } from '../Select/Select';
import { transformersUIRegistry } from './transformers';
import React from 'react';
import { TransformationRow } from './TransformationRow';
import { Button } from '../Button/Button';
import { Button } from '../Button';
import { css } from 'emotion';
interface TransformationsEditorState {
......@@ -118,7 +118,7 @@ export class TransformationsEditor extends React.PureComponent<TransformationsEd
return (
<>
{this.renderTransformationEditors()}
<Button variant="inverse" icon="fa fa-plus" onClick={this.onTransformationAdd}>
<Button variant="secondary" icon="fa fa-plus" onClick={this.onTransformationAdd}>
Add transformation
</Button>
</>
......
......@@ -2,7 +2,7 @@ import React, { PureComponent } from 'react';
import LegacyMappingRow from './LegacyMappingRow';
import { MappingType, ValueMapping } from '@grafana/data';
import { Button } from '../Button/Button';
import { Button } from '../Button';
import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup';
export interface Props {
......@@ -98,7 +98,7 @@ export class LegacyValueMappingsEditor extends PureComponent<Props, State> {
removeValueMapping={() => this.onRemoveMapping(valueMapping.id)}
/>
))}
<Button variant="inverse" icon="fa fa-plus" onClick={this.onAddMapping}>
<Button variant="primary" icon="fa fa-plus" onClick={this.onAddMapping}>
Add mapping
</Button>
</div>
......
import React from 'react';
import { MappingType, ValueMapping } from '@grafana/data';
import Forms from '../Forms';
import { Button } from '../Button/Button';
import { FullWidthButtonContainer } from '../Button/FullWidthButtonContainer';
import { MappingRow } from './MappingRow';
......@@ -66,9 +66,9 @@ export const ValueMappingsEditor: React.FC<Props> = ({ valueMappings, onChange,
</>
)}
<FullWidthButtonContainer>
<Forms.Button size="sm" icon="fa fa-plus" onClick={onAdd} aria-label="ValueMappingsEditor add mapping button">
<Button size="sm" icon="fa fa-plus" onClick={onAdd} aria-label="ValueMappingsEditor add mapping button">
Add mapping
</Forms.Button>
</Button>
</FullWidthButtonContainer>
</>
);
......
......@@ -37,7 +37,7 @@ exports[`Render should render component 1`] = `
<Button
icon="fa fa-plus"
onClick={[Function]}
variant="inverse"
variant="primary"
>
Add mapping
</Button>
......
import React, { useState } from 'react';
import { IconType } from '../Icon/types';
import { SelectableValue } from '@grafana/data';
import { Button, ButtonVariant } from '../Forms/Button';
import { Button, ButtonVariant } from '../Button';
import { Select } from '../Forms/Select/Select';
import { FullWidthButtonContainer } from '../Button/FullWidthButtonContainer';
......
......@@ -6,7 +6,6 @@ export { Popover } from './Tooltip/Popover';
export { Portal } from './Portal/Portal';
export { CustomScrollbar } from './CustomScrollbar/CustomScrollbar';
export * from './Button/Button';
export { ClipboardButton } from './ClipboardButton/ClipboardButton';
// Select
......@@ -99,7 +98,7 @@ export { LogLabels } from './Logs/LogLabels';
export { LogRows } from './Logs/LogRows';
export { getLogRowStyles } from './Logs/getLogRowStyles';
export { ToggleButtonGroup, ToggleButton } from './ToggleButtonGroup/ToggleButtonGroup';
// Panel editors
// Panel editors./Forms/Legacy/Button/FullWidthButtonContainer
export { FullWidthButtonContainer } from './Button/FullWidthButtonContainer';
export { ThresholdsEditor } from './ThresholdsEditor/ThresholdsEditor';
export { ClickOutsideWrapper } from './ClickOutsideWrapper/ClickOutsideWrapper';
......@@ -151,7 +150,8 @@ export {
export { FieldConfigItemHeaderTitle } from './FieldConfigs/FieldConfigItemHeaderTitle';
// Next-gen forms
export { default as Forms, ButtonVariant } from './Forms';
export { default as Forms } from './Forms';
export * from './Button';
export { ValuePicker } from './ValuePicker/ValuePicker';
export { fieldMatchersUI } from './MatchersUI/fieldMatchersUI';
export { getStandardFieldConfigs } from './FieldConfigs/standardFieldConfigEditors';
......
......@@ -168,11 +168,11 @@ $table-bg-hover: $dark-6;
// Buttons
// -------------------------
$btn-secondary-bg: $blue-base;
$btn-secondary-bg-hl: $blue-shade;
$btn-primary-bg: $blue-base;
$btn-primary-bg-hl: $blue-shade;
$btn-primary-bg: $green-base;
$btn-primary-bg-hl: $green-shade;
$btn-secondary-bg: $dark-6;
$btn-secondary-bg-hl: lighten($dark-6, 4%);
$btn-success-bg: $green-base;
$btn-success-bg-hl: $green-shade;
......
......@@ -160,11 +160,11 @@ $table-bg-hover: $gray-5;
// Buttons
// -------------------------
$btn-primary-bg: $green-base;
$btn-primary-bg-hl: $green-shade;
$btn-secondary-bg: $gray-5;
$btn-secondary-bg-hl: $gray-4;
$btn-secondary-bg: $blue-base;
$btn-secondary-bg-hl: $blue-shade;
$btn-primary-bg: $blue-base;
$btn-primary-bg-hl: $blue-shade;
$btn-success-bg: $green-base;
$btn-success-bg-hl: $green-shade;
......@@ -173,7 +173,6 @@ $btn-danger-bg: $red-base;
$btn-danger-bg-hl: $red-shade;
$btn-inverse-bg: $gray-5;
$btn-inverse-bg-hl: darken($gray-5, 5%);
$btn-inverse-bg-hl: $gray-4;
$btn-inverse-text-color: $gray-1;
$btn-inverse-text-shadow: 0 1px 0 rgba(255, 255, 255, 0.4);
......
......@@ -173,7 +173,6 @@ export function registerAngularDirectives() {
]);
react2AngularDirective('saveDashboardAsButton', SaveDashboardAsButtonConnected, [
'variant',
'useNewForms',
['getDashboard', { watchDepth: 'reference', wrapApply: true }],
['onSaveSuccess', { watchDepth: 'reference', wrapApply: true }],
]);
......
......@@ -65,7 +65,7 @@ export class OrgSwitcher extends React.PureComponent<Props, State> {
{org.orgId === currentOrgId ? (
<Button size="sm">Current</Button>
) : (
<Button variant="inverse" size="sm" onClick={() => this.setCurrentOrg(org)}>
<Button variant="secondary" size="sm" onClick={() => this.setCurrentOrg(org)}>
Switch to
</Button>
)}
......
......@@ -3,7 +3,7 @@ import { css } from 'emotion';
import { NavModel } from '@grafana/data';
import Page from '../../core/components/Page/Page';
import { LicenseChrome } from './LicenseChrome';
import { Forms } from '@grafana/ui';
import { LinkButton } from '@grafana/ui';
import { hot } from 'react-hot-loader';
import { StoreState } from '../../types';
import { getNavModel } from '../../core/selectors/navModel';
......@@ -69,13 +69,13 @@ const GetEnterprise: React.FC = () => {
const CallToAction: React.FC = () => {
return (
<Forms.LinkButton
<LinkButton
variant="primary"
size="lg"
href="https://grafana.com/contact?about=grafana-enterprise&utm_source=grafana-upgrade-page"
>
Contact us and get a free trial
</Forms.LinkButton>
</LinkButton>
);
};
......
import React, { useCallback } from 'react';
import { hot } from 'react-hot-loader';
import { connect } from 'react-redux';
import { Forms } from '@grafana/ui';
import { Forms, Button } from '@grafana/ui';
import { NavModel } from '@grafana/data';
import { getBackendSrv } from '@grafana/runtime';
import { StoreState } from '../../types';
......@@ -62,7 +62,7 @@ const UserCreatePage: React.FC<UserCreatePageProps> = ({ navModel, updateLocatio
})}
/>
</Forms.Field>
<Forms.Button type="submit">Create user</Forms.Button>
<Button type="submit">Create user</Button>
</>
);
}}
......
......@@ -71,7 +71,7 @@ export class UserLdapSyncInfo extends PureComponent<Props, State> {
<Button variant="secondary" onClick={this.onUserSync}>
Sync user
</Button>
<LinkButton variant="inverse" href={debugLDAPMappingURL}>
<LinkButton variant="secondary" href={debugLDAPMappingURL}>
Debug LDAP Mapping
</LinkButton>
</div>
......
......@@ -3,7 +3,7 @@ import { css, cx } from 'emotion';
import { hot } from 'react-hot-loader';
import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux';
import { NavModel } from '@grafana/data';
import { Pagination, Forms, Tooltip, HorizontalGroup, stylesFactory } from '@grafana/ui';
import { Pagination, Forms, Tooltip, HorizontalGroup, stylesFactory, LinkButton } from '@grafana/ui';
import { StoreState, UserDTO } from '../../types';
import Page from 'app/core/components/Page/Page';
import { getNavModel } from '../../core/selectors/navModel';
......@@ -53,9 +53,9 @@ const UserListAdminPageUnConnected: React.FC<Props> = props => {
onChange={event => props.changeQuery(event.currentTarget.value)}
prefix={<i className="fa fa-search" />}
/>
<Forms.LinkButton href="admin/users/create" variant="primary">
<LinkButton href="admin/users/create" variant="primary">
New user
</Forms.LinkButton>
</LinkButton>
</HorizontalGroup>
</div>
......
import React, { PureComponent } from 'react';
import { css, cx } from 'emotion';
import { Modal, Themeable, stylesFactory, withTheme, ConfirmButton, Forms } from '@grafana/ui';
import { Modal, Themeable, stylesFactory, withTheme, ConfirmButton, Button } from '@grafana/ui';
import { GrafanaTheme } from '@grafana/data';
import { UserOrg, Organization } from 'app/types';
import { OrgPicker, OrgSelectItem } from 'app/core/components/Select/OrgPicker';
......@@ -52,9 +52,9 @@ export class UserOrgs extends PureComponent<Props, State> {
</table>
</div>
<div className={addToOrgContainerClass}>
<Forms.Button variant="secondary" onClick={this.showOrgAddModal(true)}>
<Button variant="secondary" onClick={this.showOrgAddModal(true)}>
Add user to organization
</Forms.Button>
</Button>
</div>
<AddToOrgModal isOpen={showAddOrgModal} onOrgAdd={onOrgAdd} onDismiss={this.showOrgAddModal(false)} />
</div>
......@@ -169,7 +169,7 @@ class UnThemedOrgRow extends PureComponent<OrgRowProps, OrgRowState> {
<div className="pull-right">
<ConfirmButton
confirmText="Confirm removal"
confirmVariant="danger"
confirmVariant="destructive"
onClick={this.onOrgRemoveClick}
onCancel={this.onCancelClick}
onConfirm={this.onOrgRemove}
......@@ -258,12 +258,12 @@ export class AddToOrgModal extends PureComponent<AddToOrgModalProps, AddToOrgMod
</div>
</div>
<div className={buttonRowClass}>
<Forms.Button variant="primary" onClick={this.onAddUserToOrg}>
<Button variant="primary" onClick={this.onAddUserToOrg}>
Add to organization
</Forms.Button>
<Forms.Button variant="secondary" onClick={this.onCancel}>
</Button>
<Button variant="secondary" onClick={this.onCancel}>
Cancel
</Forms.Button>
</Button>
</div>
</Modal>
);
......
......@@ -3,7 +3,7 @@ import { UserDTO } from 'app/types';
import { cx, css } from 'emotion';
import { config } from 'app/core/config';
import { GrafanaTheme } from '@grafana/data';
import { ConfirmButton, Input, ConfirmModal, InputStatus, Forms, stylesFactory } from '@grafana/ui';
import { ConfirmButton, Input, ConfirmModal, InputStatus, Button, stylesFactory } from '@grafana/ui';
interface Props {
user: UserDTO;
......@@ -125,9 +125,9 @@ export class UserProfile extends PureComponent<Props, State> {
</table>
</div>
<div className={styles.buttonRow}>
<Forms.Button variant="destructive" onClick={this.showDeleteUserModal(true)}>
<Button variant="destructive" onClick={this.showDeleteUserModal(true)}>
Delete User
</Forms.Button>
</Button>
<ConfirmModal
isOpen={showDeleteModal}
title="Delete user"
......@@ -137,13 +137,13 @@ export class UserProfile extends PureComponent<Props, State> {
onDismiss={this.showDeleteUserModal(false)}
/>
{user.isDisabled ? (
<Forms.Button variant="secondary" onClick={this.onUserEnable}>
<Button variant="secondary" onClick={this.onUserEnable}>
Enable User
</Forms.Button>
</Button>
) : (
<Forms.Button variant="secondary" onClick={this.showDisableUserModal(true)}>
<Button variant="secondary" onClick={this.showDisableUserModal(true)}>
Disable User
</Forms.Button>
</Button>
)}
<ConfirmModal
isOpen={showDisableModal}
......
import React, { PureComponent } from 'react';
import { css } from 'emotion';
import { ConfirmButton, ConfirmModal, Forms } from '@grafana/ui';
import { ConfirmButton, ConfirmModal, Button } from '@grafana/ui';
import { UserSession } from 'app/types';
interface Props {
......@@ -68,7 +68,7 @@ export class UserSessions extends PureComponent<Props, State> {
<div className="pull-right">
<ConfirmButton
confirmText="Confirm logout"
confirmVariant="danger"
confirmVariant="destructive"
onConfirm={this.onSessionRevoke(session.id)}
>
Force logout
......@@ -82,9 +82,9 @@ export class UserSessions extends PureComponent<Props, State> {
</div>
<div className={logoutFromAllDevicesClass}>
{sessions.length > 0 && (
<Forms.Button variant="secondary" onClick={this.showLogoutConfirmationModal(true)}>
<Button variant="secondary" onClick={this.showLogoutConfirmationModal(true)}>
Force logout from all devices
</Forms.Button>
</Button>
)}
<ConfirmModal
isOpen={showLogoutModal}
......
......@@ -13,7 +13,7 @@
<save-dashboard-button getDashboard="ctrl.getDashboard" aria-label="Dashboard settings aside actions Save button" />
</div>
<div ng-show="ctrl.canSaveAs">
<save-dashboard-as-button getDashboard="ctrl.getDashboard" aria-label="Dashboard settings aside actions Save as button" variant="'secondary'" useNewForms="true"/>
<save-dashboard-as-button getDashboard="ctrl.getDashboard" aria-label="Dashboard settings aside actions Save as button" variant="'secondary'" />
</div>
</div>
</aside>
......
......@@ -6,7 +6,7 @@ import { css } from 'emotion';
import { InspectHeader } from './InspectHeader';
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
import { JSONFormatter, Drawer, Select, Table, TabContent, Forms, stylesFactory, CustomScrollbar } from '@grafana/ui';
import { JSONFormatter, Drawer, Select, Table, TabContent, stylesFactory, CustomScrollbar, Button } from '@grafana/ui';
import { getLocationSrv, getDataSourceSrv } from '@grafana/runtime';
import {
DataFrame,
......@@ -189,9 +189,9 @@ export class PanelInspector extends PureComponent<Props, State> {
</div>
)}
<div className={styles.downloadCsv}>
<Forms.Button variant="primary" onClick={() => this.exportCsv(processed[selected])}>
<Button variant="primary" onClick={() => this.exportCsv(processed[selected])}>
Download CSV
</Forms.Button>
</Button>
</div>
</div>
<div style={{ flexGrow: 1 }}>
......
import React, { PureComponent } from 'react';
import { FieldConfigSource, GrafanaTheme, PanelData, PanelPlugin, SelectableValue } from '@grafana/data';
import { Forms, stylesFactory } from '@grafana/ui';
import { Forms, stylesFactory, Button } from '@grafana/ui';
import { css, cx } from 'emotion';
import config from 'app/core/config';
import AutoSizer from 'react-virtualized-auto-sizer';
......@@ -198,9 +198,9 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
</div>
<div className={styles.toolbarLeft}>
<div className={styles.toolbarItem}>
<Forms.Button className={styles.toolbarItem} variant="secondary" onClick={this.onDiscard}>
<Button className={styles.toolbarItem} variant="secondary" onClick={this.onDiscard}>
Discard changes
</Forms.Button>
</Button>
</div>
<div className={styles.toolbarItem}>
<Forms.Select
......@@ -213,7 +213,7 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
<DashNavTimeControls dashboard={dashboard} location={location} updateLocation={updateLocation} />
</div>
<div className={styles.toolbarItem}>
<Forms.Button
<Button
className={styles.toolbarItem}
icon="fa fa-sliders"
variant="secondary"
......
import React from 'react';
import { css } from 'emotion';
import { Button, Forms, ModalsController } from '@grafana/ui';
import { Button, ButtonVariant, ModalsController, FullWidthButtonContainer } from '@grafana/ui';
import { DashboardModel } from 'app/features/dashboard/state';
import { connectWithProvider } from 'app/core/utils/connectWithReduxStore';
import { provideModalsContext } from 'app/routes/ReactContainer';
......@@ -24,12 +23,11 @@ export const SaveDashboardButton: React.FC<SaveDashboardButtonProps> = ({
getDashboard,
useNewForms,
}) => {
const ButtonComponent = useNewForms ? Forms.Button : Button;
return (
<ModalsController>
{({ showModal, hideModal }) => {
return (
<ButtonComponent
<Button
onClick={() => {
showModal(SaveDashboardModalProxy, {
// TODO[angular-migrations]: Remove tenary op when we migrate Dashboard Settings view to React
......@@ -40,46 +38,41 @@ export const SaveDashboardButton: React.FC<SaveDashboardButtonProps> = ({
}}
>
Save dashboard
</ButtonComponent>
</Button>
);
}}
</ModalsController>
);
};
export const SaveDashboardAsButton: React.FC<SaveDashboardButtonProps & { variant?: string }> = ({
export const SaveDashboardAsButton: React.FC<SaveDashboardButtonProps & { variant?: ButtonVariant }> = ({
dashboard,
onSaveSuccess,
getDashboard,
useNewForms,
variant,
}) => {
const ButtonComponent = useNewForms ? Forms.Button : Button;
return (
<ModalsController>
{({ showModal, hideModal }) => {
return (
<ButtonComponent
/* Styles applied here are specific to dashboard settings view */
className={css`
width: 100%;
justify-content: center;
`}
onClick={() => {
showModal(SaveDashboardAsModal, {
// TODO[angular-migrations]: Remove tenary op when we migrate Dashboard Settings view to React
dashboard: getDashboard ? getDashboard() : dashboard,
onSaveSuccess,
onDismiss: hideModal,
});
}}
// TODO[angular-migrations]: Hacking the different variants for this single button
// In Dashboard Settings in sidebar we need to use new form but with inverse variant to make it look like it should
// Everywhere else we use old button component :(
variant={variant as any}
>
Save As...
</ButtonComponent>
<FullWidthButtonContainer>
<Button
onClick={() => {
showModal(SaveDashboardAsModal, {
// TODO[angular-migrations]: Remove tenary op when we migrate Dashboard Settings view to React
dashboard: getDashboard ? getDashboard() : dashboard,
onSaveSuccess,
onDismiss: hideModal,
});
}}
// TODO[angular-migrations]: Hacking the different variants for this single button
// In Dashboard Settings in sidebar we need to use new form but with inverse variant to make it look like it should
// Everywhere else we use old button component :(
variant={variant as ButtonVariant}
>
Save As...
</Button>
</FullWidthButtonContainer>
);
}}
</ModalsController>
......
......@@ -89,7 +89,7 @@ const ConfirmPluginDashboardSaveModal: React.FC<SaveDashboardModalProps> = ({ on
<HorizontalGroup justify="center">
<SaveDashboardAsButton dashboard={dashboard} onSaveSuccess={onDismiss} />
<Button
variant="danger"
variant="destructive"
onClick={async () => {
await onDashboardSave(dashboard.getSaveModelClone(), { overwrite: true }, dashboard);
onDismiss();
......@@ -97,7 +97,7 @@ const ConfirmPluginDashboardSaveModal: React.FC<SaveDashboardModalProps> = ({ on
>
Overwrite
</Button>
<Button variant="inverse" onClick={onDismiss}>
<Button variant="secondary" onClick={onDismiss}>
Cancel
</Button>
</HorizontalGroup>
......
......@@ -101,9 +101,9 @@ export const SaveDashboardAsForm: React.FC<SaveDashboardFormProps & { isNew?: bo
<Button type="submit" aria-label="Save dashboard button">
Save
</Button>
<Forms.Button variant="secondary" onClick={onCancel}>
<Button variant="secondary" onClick={onCancel}>
Cancel
</Forms.Button>
</Button>
</HorizontalGroup>
</>
)}
......
......@@ -62,9 +62,9 @@ export const SaveDashboardForm: React.FC<SaveDashboardFormProps> = ({ dashboard,
<Button type="submit" aria-label={e2e.pages.SaveDashboardModal.selectors.save}>
Save
</Button>
<Forms.Button variant="secondary" onClick={onCancel}>
<Button variant="secondary" onClick={onCancel}>
Cancel
</Forms.Button>
</Button>
</HorizontalGroup>
</>
)}
......
import React, { useCallback, useMemo } from 'react';
import { css } from 'emotion';
import { saveAs } from 'file-saver';
import { CustomScrollbar, Forms, Button, HorizontalGroup, JSONFormatter, VerticalGroup } from '@grafana/ui';
import { CustomScrollbar, Button, HorizontalGroup, JSONFormatter, VerticalGroup } from '@grafana/ui';
import { CopyToClipboard } from 'app/core/components/CopyToClipboard/CopyToClipboard';
import { SaveDashboardFormProps } from '../types';
......@@ -61,9 +61,9 @@ export const SaveProvisionedDashboardForm: React.FC<SaveDashboardFormProps> = ({
Copy JSON to clipboard
</CopyToClipboard>
<Button onClick={saveToFile}>Save JSON to file</Button>
<Forms.Button variant="secondary" onClick={onCancel}>
<Button variant="secondary" onClick={onCancel}>
Cancel
</Forms.Button>
</Button>
</HorizontalGroup>
</VerticalGroup>
</>
......
......@@ -108,7 +108,7 @@ export class ShareExport extends PureComponent<Props, State> {
<Button variant="secondary" onClick={this.onViewJson}>
View JSON
</Button>
<Button variant="inverse" onClick={onDismiss}>
<Button variant="secondary" onClick={onDismiss}>
Cancel
</Button>
</div>
......
......@@ -121,7 +121,7 @@ export class ShareLink extends PureComponent<Props, State> {
<input type="text" className="gf-form-input" defaultValue={shareUrl} />
</div>
<div className="gf-form">
<ClipboardButton variant="inverse" getText={this.getShareUrl} onClipboardCopy={this.onShareUrlCopy}>
<ClipboardButton variant="primary" getText={this.getShareUrl} onClipboardCopy={this.onShareUrlCopy}>
Copy
</ClipboardButton>
</div>
......
......@@ -249,7 +249,7 @@ export class ShareSnapshot extends PureComponent<Props, State> {
{sharingButtonText}
</Button>
)}
<Button variant="inverse" onClick={onDismiss}>
<Button variant="secondary" onClick={onDismiss}>
Cancel
</Button>
</div>
......@@ -268,7 +268,7 @@ export class ShareSnapshot extends PureComponent<Props, State> {
<i className="fa fa-external-link-square"></i> {snapshotUrl}
</a>
<br />
<ClipboardButton variant="inverse" getText={this.getSnapshotUrl} onClipboardCopy={this.onSnapshotUrlCopy}>
<ClipboardButton variant="secondary" getText={this.getSnapshotUrl} onClipboardCopy={this.onSnapshotUrlCopy}>
Copy Link
</ClipboardButton>
</div>
......
......@@ -2,7 +2,7 @@ import React, { useState } from 'react';
import { connect } from 'react-redux';
import { hot } from 'react-hot-loader';
import { css, cx } from 'emotion';
import { stylesFactory, useTheme, Forms } from '@grafana/ui';
import { stylesFactory, useTheme, Forms, Button } from '@grafana/ui';
import { GrafanaTheme, AppEvents, DataSourceApi } from '@grafana/data';
import { RichHistoryQuery, ExploreId } from 'app/types/explore';
import { copyStringToClipboard, createUrlFromRichHistory, createDataQuery } from 'app/core/utils/richHistory';
......@@ -202,10 +202,10 @@ export function RichHistoryCard(props: Props) {
className={styles.textArea}
/>
<div className={styles.commentButtonRow}>
<Forms.Button onClick={onUpdateComment}>Save comment</Forms.Button>
<Forms.Button variant="secondary" onClick={onCancelUpdateComment}>
<Button onClick={onUpdateComment}>Save comment</Button>
<Button variant="secondary" onClick={onCancelUpdateComment}>
Cancel
</Forms.Button>
</Button>
</div>
</div>
);
......@@ -257,9 +257,9 @@ export function RichHistoryCard(props: Props) {
</div>
{!activeUpdateComment && (
<div className={styles.runButton}>
<Forms.Button variant="secondary" onClick={onRunQuery} disabled={isRemoved}>
<Button variant="secondary" onClick={onRunQuery} disabled={isRemoved}>
{datasourceInstance?.name === query.datasourceName ? 'Run query' : 'Switch data source and run query'}
</Forms.Button>
</Button>
</div>
)}
</div>
......
import React from 'react';
import { css } from 'emotion';
import { stylesFactory, useTheme, Forms } from '@grafana/ui';
import { stylesFactory, useTheme, Forms, Button } from '@grafana/ui';
import { GrafanaTheme, AppEvents } from '@grafana/data';
import appEvents from 'app/core/app_events';
import { CoreEvents } from 'app/types';
......@@ -112,9 +112,9 @@ export function RichHistorySettings(props: RichHistorySettingsProps) {
>
Delete all of your query history, permanently.
</div>
<Forms.Button variant="destructive" onClick={onDelete}>
<Button variant="destructive" onClick={onDelete}>
Clear query history
</Forms.Button>
</Button>
</div>
);
}
......@@ -36,7 +36,7 @@ export function RunButton(props: Props) {
title={loading ? 'Cancel' : 'Run Query'}
onClick={() => onRun(loading)}
buttonClassName={classNames({
'navbar-button--secondary': !loading,
'navbar-button--primary': !loading,
'navbar-button--danger': loading,
'btn--radius-right-0': showDropdown,
})}
......@@ -49,7 +49,7 @@ export function RunButton(props: Props) {
<RefreshPicker
onIntervalChanged={onChangeRefreshInterval}
value={refreshInterval}
buttonSelectClassName={`${loading ? 'navbar-button--danger' : 'navbar-button--secondary'} ${
buttonSelectClassName={`${loading ? 'navbar-button--danger' : 'navbar-button--primary'} ${
styles.selectButtonOverride
}`}
refreshButton={runButton}
......
import React, { PureComponent } from 'react';
import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux';
import { NavModel } from '@grafana/data';
import { Forms } from '@grafana/ui';
import { Forms, Button } from '@grafana/ui';
import Page from 'app/core/components/Page/Page';
import { createNewFolder } from '../state/actions';
import { getNavModel } from 'app/core/selectors/navModel';
......@@ -63,7 +63,7 @@ export class NewDashboardsFolder extends PureComponent<Props> {
})}
/>
</Forms.Field>
<Forms.Button type="submit">Create</Forms.Button>
<Button type="submit">Create</Button>
</>
)}
</Forms.Form>
......
import React, { FC } from 'react';
import { getBackendSrv } from '@grafana/runtime';
import Page from 'app/core/components/Page/Page';
import { Forms } from '@grafana/ui';
import { Forms, Button } from '@grafana/ui';
import { getConfig } from 'app/core/config';
import { StoreState } from 'app/types';
import { hot } from 'react-hot-loader';
......@@ -68,7 +68,7 @@ export const NewOrgPage: FC<PropsWithState> = ({ navModel }) => {
})}
/>
</Forms.Field>
<Forms.Button type="submit">Create</Forms.Button>
<Button type="submit">Create</Button>
</>
);
}}
......
import React, { FC } from 'react';
import { Forms, HorizontalGroup } from '@grafana/ui';
import { Forms, HorizontalGroup, Button, LinkButton } from '@grafana/ui';
import { getConfig } from 'app/core/config';
import { OrgRole } from 'app/types';
import { getBackendSrv } from '@grafana/runtime';
......@@ -71,10 +71,10 @@ export const UserInviteForm: FC<Props> = ({ updateLocation }) => {
<Forms.Switch name="sendEmail" ref={register} />
</Forms.Field>
<HorizontalGroup>
<Forms.Button type="submit">Submit</Forms.Button>
<Forms.LinkButton href={assureBaseUrl(getConfig().appSubUrl + '/org/users')} variant="secondary">
<Button type="submit">Submit</Button>
<LinkButton href={assureBaseUrl(getConfig().appSubUrl + '/org/users')} variant="secondary">
Back
</Forms.LinkButton>
</LinkButton>
</HorizontalGroup>
</>
);
......
......@@ -80,7 +80,7 @@ export class AppConfigCtrlWrapper extends PureComponent<Props, State> {
</Button>
)}
{model.enabled && (
<Button variant="danger" onClick={this.disable} className={withRightMargin}>
<Button variant="destructive" onClick={this.disable} className={withRightMargin}>
Disable
</Button>
)}
......
......@@ -63,7 +63,7 @@ export class ChangePasswordForm extends PureComponent<Props, State> {
<Button variant="primary" onClick={this.onSubmitChangePassword} disabled={isSaving}>
Change Password
</Button>
<LinkButton variant="transparent" href={`${config.appSubUrl}/profile`}>
<LinkButton variant="secondary" href={`${config.appSubUrl}/profile`}>
Cancel
</LinkButton>
</div>
......
import React, { FC } from 'react';
import { Forms } from '@grafana/ui';
import { Forms, Button, LinkButton } from '@grafana/ui';
import { css } from 'emotion';
import { getConfig } from 'app/core/config';
......@@ -106,11 +106,11 @@ export const SignupForm: FC<Props> = props => {
/>
</Forms.Field>
<Forms.Button type="submit">Submit</Forms.Button>
<Button type="submit">Submit</Button>
<span className={buttonSpacing}>
<Forms.LinkButton href={getConfig().appSubUrl + '/login'} variant="secondary">
<LinkButton href={getConfig().appSubUrl + '/login'} variant="secondary">
Back
</Forms.LinkButton>
</LinkButton>
</span>
</>
);
......
......@@ -48,7 +48,7 @@ export class UserOrganizations extends PureComponent<Props> {
<span className="btn btn-primary btn-small">Current</span>
) : (
<Button
variant="inverse"
variant="secondary"
size="sm"
onClick={() => {
this.props.setUserOrg(org);
......
......@@ -4,7 +4,7 @@ import { connect, MapStateToProps, MapDispatchToProps } from 'react-redux';
import { StoreState } from 'app/types';
import { updateLocation } from 'app/core/actions';
import { UrlQueryValue, getBackendSrv } from '@grafana/runtime';
import { Forms } from '@grafana/ui';
import { Forms, Button } from '@grafana/ui';
import { useAsync } from 'react-use';
import Page from 'app/core/components/Page/Page';
import { contextSrv } from 'app/core/core';
......@@ -115,7 +115,7 @@ const SingupInvitedPageUnconnected: FC<DispatchProps & ConnectedProps> = ({ code
/>
</Forms.Field>
<Forms.Button type="submit">Sign Up</Forms.Button>
<Button type="submit">Sign Up</Button>
</>
)}
</Forms.Form>
......
......@@ -49,7 +49,7 @@ export const DataLink = (props: Props) => {
onChange={handleChange('field')}
/>
<Button
variant={'inverse'}
variant={'destructive'}
title="Remove field"
icon={'fa fa-times'}
onClick={event => {
......
......@@ -63,7 +63,7 @@ export const DataLinks = (props: Props) => {
})}
<div>
<Button
variant={'inverse'}
variant={'secondary'}
className={css`
margin-right: 10px;
`}
......
......@@ -66,7 +66,7 @@ export const DerivedField = (props: Props) => {
}
/>
<Button
variant={'inverse'}
variant="destructive"
title="Remove field"
icon={'fa fa-times'}
onClick={event => {
......
......@@ -66,7 +66,7 @@ export const DerivedFields = (props: Props) => {
})}
<div>
<Button
variant={'inverse'}
variant="primary"
className={css`
margin-right: 10px;
`}
......@@ -81,7 +81,7 @@ export const DerivedFields = (props: Props) => {
</Button>
{value && value.length > 0 && (
<Button variant="inverse" onClick={() => setShowDebug(!showDebug)}>
<Button variant="secondary" onClick={() => setShowDebug(!showDebug)}>
{showDebug ? 'Hide example log message' : 'Show example log message'}
</Button>
)}
......
......@@ -58,7 +58,7 @@ export class NewsPanelEditor extends PureComponent<PanelEditorProps<NewsOptions>
<div>
<br />
<div>If the feed is unable to connect, consider a CORS proxy</div>
<Button variant="inverse" onClick={this.onSetProxyPrefix}>
<Button variant="secondary" onClick={this.onSetProxyPrefix}>
Use Proxy
</Button>
</div>
......
......@@ -171,11 +171,11 @@ $table-bg-hover: $dark-6;
// Buttons
// -------------------------
$btn-secondary-bg: $blue-base;
$btn-secondary-bg-hl: $blue-shade;
$btn-primary-bg: $blue-base;
$btn-primary-bg-hl: $blue-shade;
$btn-primary-bg: $green-base;
$btn-primary-bg-hl: $green-shade;
$btn-secondary-bg: $dark-6;
$btn-secondary-bg-hl: lighten($dark-6, 4%);
$btn-success-bg: $green-base;
$btn-success-bg-hl: $green-shade;
......
......@@ -163,11 +163,11 @@ $table-bg-hover: $gray-5;
// Buttons
// -------------------------
$btn-primary-bg: $green-base;
$btn-primary-bg-hl: $green-shade;
$btn-secondary-bg: $gray-5;
$btn-secondary-bg-hl: $gray-4;
$btn-secondary-bg: $blue-base;
$btn-secondary-bg-hl: $blue-shade;
$btn-primary-bg: $blue-base;
$btn-primary-bg-hl: $blue-shade;
$btn-success-bg: $green-base;
$btn-success-bg-hl: $green-shade;
......@@ -176,7 +176,6 @@ $btn-danger-bg: $red-base;
$btn-danger-bg-hl: $red-shade;
$btn-inverse-bg: $gray-5;
$btn-inverse-bg-hl: darken($gray-5, 5%);
$btn-inverse-bg-hl: $gray-4;
$btn-inverse-text-color: $gray-1;
$btn-inverse-text-shadow: 0 1px 0 rgba(255, 255, 255, 0.4);
......
......@@ -171,8 +171,8 @@ i.navbar-page-btn__search {
}
}
&--secondary {
@include buttonBackground($btn-secondary-bg, $btn-secondary-bg-hl);
&--primary {
@include buttonBackground($btn-primary-bg, $btn-primary-bg-hl);
}
&:hover {
......
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