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>
This commit is contained in:
Tobias Skarhed
2020-03-26 11:50:27 +01:00
committed by GitHub
co-authored by Torkel Ödegaard
parent f63877f247
commit 5cdb8f8e44
81 changed files with 388 additions and 693 deletions
@@ -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} />
@@ -1,45 +1,35 @@
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!'],
export default {
title: 'Forms/Button',
component: Button,
decorators: [withCenteredStory, withHorizontallyCenteredStory],
parameters: {
docs: {
page: mdx,
},
},
};
const variants = {
size: ['xs', 'sm', 'md', 'lg'],
variant: ['primary', 'secondary', 'danger', 'inverse', 'transparent', 'link'],
};
const combinationOptions = {
CombinationRenderer: ThemeableCombinationsRowRenderer,
};
const variants = ['primary', 'secondary', 'destructive', 'link'];
const renderButtonStory = (buttonComponent: typeof Button | typeof LinkButton) => {
const isDisabled = boolean('Disable button', false);
return withPropsCombinations(
buttonComponent,
{ ...variants, ...defaultProps, disabled: [isDisabled] },
combinationOptions
)();
};
const sizes = ['sm', 'md', 'lg'];
ButtonStories.add('as button element', () => renderButtonStory(Button));
ButtonStories.add('as link element', () => renderButtonStory(LinkButton));
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>
);
};
@@ -1,26 +0,0 @@
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);
});
});
@@ -1,72 +1,178 @@
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';
@@ -1,5 +0,0 @@
// 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>"`;
@@ -0,0 +1 @@
export * from './Button';
@@ -1,164 +0,0 @@
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,
};
}
}
};
@@ -1,17 +0,0 @@
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;
}
@@ -1,6 +1,6 @@
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;
@@ -1,10 +1,11 @@
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'
),
@@ -1,7 +1,7 @@
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}>
@@ -1,7 +1,7 @@
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>
</>
);
@@ -1,7 +1,7 @@
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();
@@ -1,35 +0,0 @@
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>
);
};
@@ -1,161 +0,0 @@
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;
@@ -1,7 +1,7 @@
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}
@@ -1,7 +1,7 @@
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;
@@ -1,7 +1,7 @@
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>
@@ -1,6 +1,6 @@
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>
@@ -1,7 +1,7 @@
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';
+3 -3
View File
@@ -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);