Buttons: replace usage of .btn classnames (#33226)

* refactor(loginpage): migrate custom button styles to use Button component

* refactor(certificationkey): prefer grafana-ui form elements over html elements and classnames

* refactor(axisselector): prefer grafana-ui Button component over html button element

* refactor(input-datasource): replace use of btn class with grafana-ui components

* chore(grafana-ui): delete deprecated ToggleButtonGroup component

* refactor: replace btn and cta-form__close class usage with IconButton

* chore(closebutton): post master merge use v2 theme

* refactor(permissionlist): remove usage of .btn classname

* Wip

* docs(styling): update styling and theme docs import paths

* refactor(alerting): remote btn classnames from TestRuleResult

* refactor(apikeys): prefer grafana-ui Button components over btn classNames

* refactor(folders): prefer grafana-ui Button components over btn classNames

* refactor(teams): prefer grafana-ui Button components over btn classNames

* refactor(datasources): prefer grafana-ui Button components over btn classNames

* refactor: prefer grafana-ui Button components over btn classNames

* Minor style tweak to service buttons

* test: update snapshots related to button changes

* chore(input-datasource): remove unused import declaration

* refactor(loginservicebuttons): rename theme.palette to theme.colors

Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
This commit is contained in:
Jack Westbrook 2021-04-23 10:06:42 +02:00 committed by GitHub
parent 6034bf37c6
commit c809d63065
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 335 additions and 494 deletions

View File

@ -10,7 +10,7 @@ For styling components, use [Emotion's `css` function](https://emotion.sh/docs/e
```tsx
import React from 'react';
import { css } from 'emotion';
import { css } from '@emotion/css';
const ComponentA = () => (
<div
@ -33,14 +33,13 @@ To access the theme in your styles, use the `useStyles` hook. It provides basic
import React, { FC } from 'react';
import { GrafanaTheme } from '@grafana/data';
import { useStyles } from '@grafana/ui';
import { css } from 'emotion';
import { css } from '@emotion/css';
const Foo: FC<FooProps> = () => {
const styles = useStyles(getStyles);
// Use styles with classNames
return <div className={styles}>...</div>
return <div className={styles}>...</div>;
};
const getStyles = (theme: GrafanaTheme) => css`
@ -56,15 +55,15 @@ Let's say you need to style a component that has a different background dependin
```tsx
import React from 'react';
import { css } from 'emotion';
import { css } from '@emotion/css';
import { GrafanaTheme } from '@grafana/data';
import { selectThemeVariant, stylesFactory, useTheme } from '@grafana/ui';
interface ComponentAProps {
isActive: boolean
isActive: boolean;
}
const ComponentA: React.FC<ComponentAProps> = ({isActive}) => {
const ComponentA: React.FC<ComponentAProps> = ({ isActive }) => {
const theme = useTheme();
const styles = getStyles(theme, isActive);
@ -76,7 +75,6 @@ const ComponentA: React.FC<ComponentAProps> = ({isActive}) => {
);
};
// Mind, that you can pass multiple arguments, theme included
const getStyles = stylesFactory((theme: GrafanaTheme, isActive: boolean) => {
const backgroundColor = isActive ? theme.colors.red : theme.colors.blue;
@ -100,7 +98,7 @@ For class composition, use [Emotion's `cx` function](https://emotion.sh/docs/emo
```tsx
import React from 'react';
import { css, cx } from 'emotion';
import { css, cx } from '@emotion/css';
interface Props {
className?: string;

View File

@ -26,7 +26,7 @@ Here's how to use Grafana themes in React components.
import React, { FC } from 'react';
import { GrafanaTheme } from '@grafana/data';
import { useStyles } from '@grafana/ui';
import { css } from 'emotion';
import { css } from '@emotion/css';
const getComponentStyles = (theme: GrafanaTheme) => css`
padding: ${theme.spacing.md};
@ -57,7 +57,7 @@ const Foo: FC<FooProps> = () => {
```tsx
import { ThemeContext } from '@grafana/ui';
<ThemeContext.Consumer>{theme => <Foo theme={theme} />}</ThemeContext.Consumer>;
<ThemeContext.Consumer>{(theme) => <Foo theme={theme} />}</ThemeContext.Consumer>;
```
#### Using `withTheme` higher-order component (HOC)
@ -97,9 +97,8 @@ describe('MyComponent', () => {
restoreThemeContext();
});
it('renders correctly', () => {
const wrapper = mount(<MyComponent />)
const wrapper = mount(<MyComponent />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@ -5,6 +5,7 @@ import { PopoverContentProps } from '../Tooltip/Tooltip';
import { Switch } from '../Forms/Legacy/Switch/Switch';
import { css } from '@emotion/css';
import { withTheme, useStyles } from '../../themes';
import { Button } from '../Button';
export interface SeriesColorPickerPopoverProps extends ColorPickerProps, PopoverContentProps {
yaxis?: number;
@ -70,18 +71,18 @@ export class AxisSelector extends React.PureComponent<AxisSelectorProps, AxisSel
}
render() {
const leftButtonClass = this.state.yaxis === 1 ? 'btn-primary' : 'btn-inverse';
const rightButtonClass = this.state.yaxis === 2 ? 'btn-primary' : 'btn-inverse';
const leftButtonVariant = this.state.yaxis === 1 ? 'primary' : 'secondary';
const rightButtonVariant = this.state.yaxis === 2 ? 'primary' : 'secondary';
return (
<div className="p-b-1">
<label className="small p-r-1">Y Axis:</label>
<button onClick={this.onToggleAxis} className={'btn btn-small ' + leftButtonClass}>
<Button onClick={this.onToggleAxis} size="sm" variant={leftButtonVariant}>
Left
</button>
<button onClick={this.onToggleAxis} className={'btn btn-small ' + rightButtonClass}>
</Button>
<Button onClick={this.onToggleAxis} size="sm" variant={rightButtonVariant}>
Right
</button>
</Button>
</div>
);
}

View File

@ -1,4 +1,8 @@
import React, { ChangeEvent, MouseEvent, FC } from 'react';
import { Input } from '../Input/Input';
import { Button } from '../Button';
import { TextArea } from '../TextArea/TextArea';
import { InlineField } from '../Forms/InlineField';
interface Props {
label: string;
@ -6,35 +10,22 @@ interface Props {
placeholder: string;
onChange: (event: ChangeEvent<HTMLTextAreaElement>) => void;
onClick: (event: MouseEvent<HTMLAnchorElement>) => void;
onClick: (event: MouseEvent<HTMLButtonElement>) => void;
}
export const CertificationKey: FC<Props> = ({ hasCert, label, onChange, onClick, placeholder }) => {
return (
<div className="gf-form-inline">
<div className="gf-form gf-form--v-stretch">
<label className="gf-form-label width-7">{label}</label>
</div>
{!hasCert && (
<div className="gf-form gf-form--grow">
<textarea
rows={7}
className="gf-form-input gf-form-textarea"
onChange={onChange}
placeholder={placeholder}
required
/>
</div>
<InlineField label={label} labelWidth={14}>
{hasCert ? (
<>
<Input type="text" disabled value="configured" width={24} />
<Button variant="secondary" onClick={onClick} style={{ marginLeft: 4 }}>
Reset
</Button>
</>
) : (
<TextArea rows={7} onChange={onChange} placeholder={placeholder} required />
)}
{hasCert && (
<div className="gf-form">
<input type="text" className="gf-form-input max-width-12" disabled value="configured" />
<a className="btn btn-secondary gf-form-btn" onClick={onClick}>
reset
</a>
</div>
)}
</div>
</InlineField>
);
};

View File

@ -13,7 +13,7 @@ export const TLSAuthSettings: React.FC<HttpSettingsBaseProps> = ({ dataSourceCon
const hasTLSClientKey = dataSourceConfig.secureJsonFields && dataSourceConfig.secureJsonFields.tlsClientKey;
const hasServerName = dataSourceConfig.jsonData && dataSourceConfig.jsonData.serverName;
const onResetClickFactory = (field: string) => (event: React.MouseEvent<HTMLAnchorElement>) => {
const onResetClickFactory = (field: string) => (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
const newSecureJsonFields: KeyValue<boolean> = { ...dataSourceConfig.secureJsonFields };
newSecureJsonFields[field] = false;

View File

@ -1,50 +0,0 @@
import React from 'react';
import { action } from '@storybook/addon-actions';
import { ToggleButton, ToggleButtonGroup } from './ToggleButtonGroup';
import { UseState } from '../../utils/storybook/UseState';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
export default {
title: 'Forms/Legacy/ToggleButtonGroup',
component: ToggleButtonGroup,
decorators: [withCenteredStory],
};
const options = [
{ value: 'first', label: 'First' },
{ value: 'second', label: 'Second' },
{ value: 'third', label: 'Third' },
];
export const basic = () => {
return (
<UseState
initialState={{
value: 'first',
}}
>
{(value, updateValue) => {
return (
<ToggleButtonGroup label="Options">
{options.map((option, index) => {
return (
<ToggleButton
key={`${option.value}-${index}`}
value={option.value}
onChange={(newValue) => {
action('on change')(newValue);
updateValue({ value: newValue });
}}
selected={value.value === option.value}
>
{option.label}
</ToggleButton>
);
})}
</ToggleButtonGroup>
);
}}
</UseState>
);
};

View File

@ -1,77 +0,0 @@
import React, { FC, ReactNode, PureComponent } from 'react';
import classNames from 'classnames';
import { Tooltip } from '../Tooltip/Tooltip';
import { deprecationWarning } from '@grafana/data';
interface ToggleButtonGroupProps {
label?: string;
children: JSX.Element[];
transparent?: boolean;
width?: number;
}
/** @deprecated */
export class ToggleButtonGroup extends PureComponent<ToggleButtonGroupProps> {
render() {
const { children, label, transparent, width } = this.props;
const labelClasses = classNames('gf-form-label', {
'gf-form-label--transparent': transparent,
[`width-${width}`]: width,
});
const buttonGroupClasses = classNames('toggle-button-group', {
'toggle-button-group--transparent': transparent,
'toggle-button-group--padded': width, // Add extra padding to compensate for buttons border
});
deprecationWarning('ToggleButtonGroup', 'ToggleButtonGroup', 'RadioButtonGroup');
return (
<div className="gf-form gf-form--align-center">
{label && <label className={labelClasses}>{label}</label>}
<div className={buttonGroupClasses}>{children}</div>
</div>
);
}
}
interface ToggleButtonProps {
onChange?: (value: any) => void;
selected?: boolean;
value: any;
className?: string;
children: ReactNode;
tooltip?: string;
}
export const ToggleButton: FC<ToggleButtonProps> = ({
children,
selected,
className = '',
value = null,
tooltip,
onChange,
}) => {
const onClick = (event: React.SyntheticEvent) => {
event.stopPropagation();
if (!selected && onChange) {
onChange(value);
}
};
const btnClassName = `btn ${className}${selected ? ' active' : ''}`;
const button = (
<button className={btnClassName} onClick={onClick}>
<span>{children}</span>
</button>
);
if (tooltip) {
return (
<Tooltip content={tooltip} placement="bottom">
{button}
</Tooltip>
);
} else {
return button;
}
};

View File

@ -110,7 +110,6 @@ export { LogRows } from './Logs/LogRows';
export { getLogRowStyles } from './Logs/getLogRowStyles';
export { DataLinkButton } from './DataLinks/DataLinkButton';
export { FieldLinkList } from './DataLinks/FieldLinkList';
export { ToggleButtonGroup, ToggleButton } from './ToggleButtonGroup/ToggleButtonGroup';
// Panel editors
export { FullWidthButtonContainer } from './Button/FullWidthButtonContainer';
export { ClickOutsideWrapper } from './ClickOutsideWrapper/ClickOutsideWrapper';

View File

@ -76,8 +76,11 @@ export type IconName =
| 'gf-interpolation-step-after'
| 'gf-interpolation-step-before'
| 'gf-logs'
| 'github'
| 'gitlab'
| 'grafana'
| 'graph-bar'
| 'google'
| 'heart-break'
| 'heart'
| 'history'
@ -90,10 +93,12 @@ export type IconName =
| 'link'
| 'list-ul'
| 'lock'
| 'microsoft'
| 'minus-circle'
| 'minus'
| 'mobile-android'
| 'monitor'
| 'okta'
| 'palette'
| 'panel-add'
| 'pause'

View File

@ -5,8 +5,7 @@ import React, { PureComponent } from 'react';
import { InputDatasource, describeDataFrame } from './InputDatasource';
import { InputQuery, InputOptions } from './types';
import { InlineFormLabel, LegacyForms, TableInputCSV, Icon } from '@grafana/ui';
const { Select } = LegacyForms;
import { Select, TableInputCSV, LinkButton, Icon, InlineField } from '@grafana/ui';
import { DataFrame, toCSV, SelectableValue, MutableDataFrame, QueryEditorProps } from '@grafana/data';
import { dataFrameToCSV } from './utils';
@ -68,21 +67,19 @@ export class InputQueryEditor extends PureComponent<Props, State> {
const selected = query.data ? options[0] : options[1];
return (
<div>
<div className="gf-form">
<InlineFormLabel width={4}>Data</InlineFormLabel>
<Select width={6} options={options} value={selected} onChange={this.onSourceChange} />
<div className="btn btn-link">
<InlineField label="Data" labelWidth={8}>
<>
<Select width={20} options={options} value={selected} onChange={this.onSourceChange} />
{query.data ? (
describeDataFrame(query.data)
<div style={{ alignSelf: 'center' }}>{describeDataFrame(query.data)}</div>
) : (
<a href={`datasources/edit/${id}/`}>
<LinkButton variant="link" href={`datasources/edit/${id}/`}>
{name}: {describeDataFrame(datasource.data)} &nbsp;&nbsp;
<Icon name="pen" />
</a>
</LinkButton>
)}
</div>
</div>
</>
</InlineField>
{query.data && <TableInputCSV text={text} onSeriesParsed={this.onSeriesParsed} width={'100%'} height={200} />}
</div>
);

View File

@ -0,0 +1,20 @@
import React from 'react';
import { css } from '@emotion/css';
import { IconButton, useStyles2 } from '@grafana/ui';
import { GrafanaThemeV2 } from '@grafana/data';
type Props = {
onClick: () => void;
};
export const CloseButton: React.FC<Props> = ({ onClick }) => {
const styles = useStyles2(getStyles);
return <IconButton className={styles} name="times" onClick={onClick} />;
};
const getStyles = (theme: GrafanaThemeV2) =>
css`
position: absolute;
right: ${theme.spacing(0.5)};
top: ${theme.spacing(1)};
`;

View File

@ -40,28 +40,26 @@ export const LoginPage: FC = () => {
{!isChangingPassword && (
<InnerBox>
{!disableLoginForm && (
<>
<LoginForm
onSubmit={login}
loginHint={loginHint}
passwordHint={passwordHint}
isLoggingIn={isLoggingIn}
>
{!(ldapEnabled || authProxyEnabled) ? (
<HorizontalGroup justify="flex-end">
<LinkButton
className={forgottenPasswordStyles}
variant="link"
href={`${config.appSubUrl}/user/password/send-reset-email`}
>
Forgot your password?
</LinkButton>
</HorizontalGroup>
) : (
<></>
)}
</LoginForm>
</>
<LoginForm
onSubmit={login}
loginHint={loginHint}
passwordHint={passwordHint}
isLoggingIn={isLoggingIn}
>
{!(ldapEnabled || authProxyEnabled) ? (
<HorizontalGroup justify="flex-end">
<LinkButton
className={forgottenPasswordStyles}
variant="link"
href={`${config.appSubUrl}/user/password/send-reset-email`}
>
Forgot your password?
</LinkButton>
</HorizontalGroup>
) : (
<></>
)}
</LoginForm>
)}
<LoginServiceButtons />
{!disableUserSignUp && <UserSignup />}

View File

@ -1,89 +1,99 @@
import React from 'react';
import config from 'app/core/config';
import { css, cx } from '@emotion/css';
import { useStyles } from '@grafana/ui';
import { GrafanaTheme } from '@grafana/data';
const loginServices: () => LoginServices = () => {
const oauthEnabled = !!config.oauth;
return {
saml: {
enabled: config.samlEnabled,
name: 'SAML',
className: 'github',
icon: 'key',
},
google: {
enabled: oauthEnabled && config.oauth.google,
name: 'Google',
},
azuread: {
enabled: oauthEnabled && config.oauth.azuread,
name: 'Microsoft',
},
github: {
enabled: oauthEnabled && config.oauth.github,
name: 'GitHub',
},
gitlab: {
enabled: oauthEnabled && config.oauth.gitlab,
name: 'GitLab',
},
grafanacom: {
enabled: oauthEnabled && config.oauth.grafana_com,
name: 'Grafana.com',
hrefName: 'grafana_com',
icon: 'grafana_com',
},
okta: {
enabled: oauthEnabled && config.oauth.okta,
name: 'Okta',
},
oauth: {
enabled: oauthEnabled && config.oauth.generic_oauth,
name: oauthEnabled && config.oauth.generic_oauth ? config.oauth.generic_oauth.name : 'OAuth',
icon: 'sign-in',
hrefName: 'generic_oauth',
},
};
};
import { Icon, IconName, LinkButton, useStyles, useTheme2, VerticalGroup } from '@grafana/ui';
import { GrafanaTheme, GrafanaThemeV2 } from '@grafana/data';
import { pickBy } from 'lodash';
export interface LoginService {
bgColor: string;
enabled: boolean;
name: string;
hrefName?: string;
icon?: string;
className?: string;
icon: IconName;
}
export interface LoginServices {
[key: string]: LoginService;
}
const loginServices: () => LoginServices = () => {
const oauthEnabled = !!config.oauth;
return {
saml: {
bgColor: '#464646',
enabled: config.samlEnabled,
name: 'SAML',
icon: 'key-skeleton-alt',
},
google: {
bgColor: '#e84d3c',
enabled: oauthEnabled && config.oauth.google,
name: 'Google',
icon: 'google',
},
azuread: {
bgColor: '#2f2f2f',
enabled: oauthEnabled && config.oauth.azuread,
name: 'Microsoft',
icon: 'microsoft',
},
github: {
bgColor: '#464646',
enabled: oauthEnabled && config.oauth.github,
name: 'GitHub',
icon: 'github',
},
gitlab: {
bgColor: '#fc6d26',
enabled: oauthEnabled && config.oauth.gitlab,
name: 'GitLab',
icon: 'gitlab',
},
grafanacom: {
bgColor: '#262628',
enabled: oauthEnabled && config.oauth.grafana_com,
name: 'Grafana.com',
hrefName: 'grafana_com',
icon: 'grafana',
},
okta: {
bgColor: '#2f2f2f',
enabled: oauthEnabled && config.oauth.okta,
name: 'Okta',
icon: 'okta',
},
oauth: {
bgColor: '#262628',
enabled: oauthEnabled && config.oauth.generic_oauth,
name: oauthEnabled && config.oauth.generic_oauth ? config.oauth.generic_oauth.name : 'OAuth',
icon: 'signin',
hrefName: 'generic_oauth',
},
};
};
const getServiceStyles = (theme: GrafanaTheme) => {
return {
container: css`
width: 100%;
text-align: center;
`,
button: css`
color: #d8d9da;
margin: 0 0 ${theme.spacing.md};
width: 100%;
&:hover {
color: #fff;
}
position: relative;
`,
buttonIcon: css`
position: absolute;
left: ${theme.spacing.sm};
top: 50%;
transform: translateY(-50%);
`,
divider: {
base: css`
float: left;
width: 100%;
margin: 0 25% ${theme.spacing.md} 25%;
color: ${theme.colors.text};
display: flex;
margin-bottom: ${theme.spacing.sm};
justify-content: space-between;
text-align: center;
color: ${theme.colors.text};
width: 100%;
`,
line: css`
width: 100px;
@ -114,38 +124,46 @@ const LoginDivider = () => {
);
};
export const LoginServiceButtons = () => {
const styles = useStyles(getServiceStyles);
const keyNames = Object.keys(loginServices());
const serviceElementsEnabled = keyNames.filter((key) => {
const service: LoginService = loginServices()[key];
return service.enabled;
});
function getButtonStyleFor(service: LoginService, styles: ReturnType<typeof getServiceStyles>, theme: GrafanaThemeV2) {
return cx(
styles.button,
css`
background-color: ${service.bgColor};
color: ${theme.colors.getContrastText(service.bgColor)};
if (serviceElementsEnabled.length === 0) {
return null;
&:hover {
background-color: ${theme.colors.emphasize(service.bgColor, 0.15)};
box-shadow: ${theme.shadows.z1};
}
`
);
}
export const LoginServiceButtons = () => {
const enabledServices = pickBy(loginServices(), (service) => service.enabled);
const hasServices = Object.keys(enabledServices).length > 0;
const theme = useTheme2();
const styles = useStyles(getServiceStyles);
if (hasServices) {
return (
<VerticalGroup>
<LoginDivider />
{Object.entries(enabledServices).map(([key, service]) => (
<LinkButton
key={key}
className={getButtonStyleFor(service, styles, theme)}
href={`login/${service.hrefName ? service.hrefName : key}`}
target="_self"
fullWidth
>
<Icon className={styles.buttonIcon} name={service.icon} />
Sign in with {service.name}
</LinkButton>
))}
</VerticalGroup>
);
}
const serviceElements = serviceElementsEnabled.map((key) => {
const service: LoginService = loginServices()[key];
return (
<a
key={key}
className={cx(`btn btn-medium btn-service btn-service--${service.className || key}`, styles.button)}
href={`login/${service.hrefName ? service.hrefName : key}`}
target="_self"
>
<i className={`btn-service-icon fa fa-${service.icon ? service.icon : key}`} />
Sign in with {service.name}
</a>
);
});
const divider = LoginDivider();
return (
<>
{divider}
<div className={styles.container}>{serviceElements}</div>
</>
);
return null;
};

View File

@ -8,7 +8,7 @@ export const UserSignup: FC<{}> = () => {
return (
<VerticalGroup
className={css`
margin-top: 8px;
margin-top: 16px;
`}
>
<span>New to Grafana?</span>

View File

@ -3,7 +3,7 @@ import { css } from '@emotion/css';
import config from 'app/core/config';
import { UserPicker } from 'app/core/components/Select/UserPicker';
import { TeamPicker, Team } from 'app/core/components/Select/TeamPicker';
import { Button, Form, HorizontalGroup, Icon, Select, stylesFactory } from '@grafana/ui';
import { Button, Form, HorizontalGroup, Select, stylesFactory } from '@grafana/ui';
import { GrafanaTheme, SelectableValue } from '@grafana/data';
import { User } from 'app/types';
import {
@ -14,6 +14,7 @@ import {
NewDashboardAclItem,
OrgRole,
} from 'app/types/acl';
import { CloseButton } from '../CloseButton/CloseButton';
export interface Props {
onAddPermission: (item: NewDashboardAclItem) => void;
@ -93,9 +94,7 @@ class AddPermissions extends Component<Props, NewDashboardAclItem> {
return (
<div className="cta-form">
<button className="cta-form__close btn btn-transparent" onClick={onCancel}>
<Icon name="times" />
</button>
<CloseButton onClick={onCancel} />
<h5>Add Permission For</h5>
<Form maxWidth="none" onSubmit={this.onSubmit}>
{() => (

View File

@ -1,5 +1,5 @@
import React, { Component } from 'react';
import { Select, Icon } from '@grafana/ui';
import { Select, Icon, Button } from '@grafana/ui';
import { dashboardPermissionLevels } from 'app/types/acl';
export interface Props {
@ -33,9 +33,7 @@ export default class DisabledPermissionListItem extends Component<Props, any> {
</div>
</td>
<td>
<button className="btn btn-inverse btn-small">
<Icon name="lock" />
</button>
<Button size="sm" disabled icon="lock" />
</td>
</tr>
);

View File

@ -1,5 +1,5 @@
import React, { PureComponent } from 'react';
import { Select, Icon } from '@grafana/ui';
import { Select, Icon, Button } from '@grafana/ui';
import { SelectableValue } from '@grafana/data';
import { dashboardPermissionLevels, DashboardAcl, PermissionLevel } from 'app/types/acl';
import { FolderInfo } from 'app/types';
@ -85,13 +85,9 @@ export default class PermissionsListItem extends PureComponent<Props> {
</td>
<td>
{!item.inherited ? (
<a className="btn btn-danger btn-small" onClick={this.onRemoveItem}>
<Icon name="times" style={{ marginBottom: 0 }} />
</a>
<Button size="sm" variant="destructive" icon="times" onClick={this.onRemoveItem} />
) : (
<button className="btn btn-inverse btn-small">
<Icon name="lock" style={{ marginBottom: '3px' }} />
</button>
<Button size="sm" disabled icon="times" />
)}
</td>
</tr>

View File

@ -2,7 +2,7 @@ import React, { PureComponent } from 'react';
import { hot } from 'react-hot-loader';
import { connect } from 'react-redux';
import { NavModel } from '@grafana/data';
import { Alert, LegacyForms } from '@grafana/ui';
import { Alert, Button, LegacyForms } from '@grafana/ui';
const { FormField } = LegacyForms;
import { getNavModel } from 'app/core/selectors/navModel';
import config from 'app/core/config';
@ -122,9 +122,7 @@ export class LdapPage extends PureComponent<Props, State> {
name="username"
defaultValue={queryParams.username}
/>
<button type="submit" className="btn btn-primary">
Run
</button>
<Button type="submit">Run</Button>
</form>
</div>
{userError && userError.title && (

View File

@ -1,6 +1,6 @@
import React, { PureComponent } from 'react';
import { dateTimeFormat } from '@grafana/data';
import { Spinner } from '@grafana/ui';
import { Button, Spinner } from '@grafana/ui';
import { SyncInfo } from 'app/types';
interface Props {
@ -31,10 +31,10 @@ export class LdapSyncInfo extends PureComponent<Props, State> {
<>
<h3 className="page-heading">
LDAP Synchronisation
<button className={`btn btn-secondary pull-right`} onClick={this.handleSyncClick} hidden={true}>
<Button className="pull-right" onClick={this.handleSyncClick} hidden>
<span className="btn-title">Bulk-sync now</span>
{isSyncing && <Spinner inline={true} />}
</button>
</Button>
</h3>
<div className="gf-form-group">
<div className="gf-form">

View File

@ -1,6 +1,5 @@
import React, { PureComponent } from 'react';
import { LoadingPlaceholder, JSONFormatter, Icon } from '@grafana/ui';
import { LoadingPlaceholder, JSONFormatter, Icon, HorizontalGroup } from '@grafana/ui';
import appEvents from 'app/core/app_events';
import { CopyToClipboard } from 'app/core/components/CopyToClipboard/CopyToClipboard';
import { DashboardModel, PanelModel } from '../dashboard/state';
@ -106,16 +105,12 @@ export class TestRuleResult extends PureComponent<Props, State> {
return (
<>
<div className="pull-right">
<button className="btn btn-transparent btn-p-x-0 m-r-1" onClick={this.onToggleExpand}>
{this.renderExpandCollapse()}
</button>
<CopyToClipboard
className="btn btn-transparent btn-p-x-0"
text={this.getTextForClipboard}
onSuccess={this.onClipboardSuccess}
>
<Icon name="copy" /> Copy to Clipboard
</CopyToClipboard>
<HorizontalGroup spacing="md">
<div onClick={this.onToggleExpand}>{this.renderExpandCollapse()}</div>
<CopyToClipboard elType="div" text={this.getTextForClipboard} onSuccess={this.onClipboardSuccess}>
<Icon name="copy" /> Copy to Clipboard
</CopyToClipboard>
</HorizontalGroup>
</div>
<JSONFormatter json={testRuleResponse} open={openNodes} onDidRender={this.setFormattedJson} />

View File

@ -1,5 +1,5 @@
import React, { FC } from 'react';
import { Button } from '@grafana/ui';
import { FilterInput } from '../../core/components/FilterInput/FilterInput';
interface Props {
@ -17,9 +17,9 @@ export const ApiKeysActionBar: FC<Props> = ({ searchQuery, disabled, onAddClick,
</div>
<div className="page-action-bar__spacer" />
<button className="btn btn-primary pull-right" onClick={onAddClick} disabled={disabled}>
<Button className="pull-right" onClick={onAddClick} disabled={disabled}>
Add API key
</button>
</Button>
</div>
);
};

View File

@ -1,8 +1,9 @@
import React, { ChangeEvent, FC, FormEvent, useEffect, useState } from 'react';
import { EventsWithValidation, Icon, InlineFormLabel, LegacyForms, ValidationEvents } from '@grafana/ui';
import { EventsWithValidation, InlineFormLabel, LegacyForms, ValidationEvents, Button } from '@grafana/ui';
import { NewApiKey, OrgRole } from '../../types';
import { rangeUtil } from '@grafana/data';
import { SlideDown } from '../../core/components/Animations/SlideDown';
import { CloseButton } from 'app/core/components/CloseButton/CloseButton';
const { Input } = LegacyForms;
@ -65,9 +66,7 @@ export const ApiKeysForm: FC<Props> = ({ show, onClose, onKeyAdded }) => {
return (
<SlideDown in={show}>
<div className="gf-form-inline cta-form">
<button className="cta-form__close btn btn-transparent" onClick={onClose}>
<Icon name="times" />
</button>
<CloseButton onClick={onClose} />
<form className="gf-form-group" onSubmit={onSubmit}>
<h5>Add API Key</h5>
<div className="gf-form-inline">
@ -101,7 +100,7 @@ export const ApiKeysForm: FC<Props> = ({ show, onClose, onKeyAdded }) => {
/>
</div>
<div className="gf-form">
<button className="btn gf-form-btn btn-primary">Add</button>
<Button>Add</Button>
</div>
</div>
</form>

View File

@ -1,6 +1,6 @@
import React, { FC } from 'react';
import { PluginDashboard } from '../../types';
import { Icon } from '@grafana/ui';
import { Button, Icon } from '@grafana/ui';
export interface Props {
dashboards: PluginDashboard[];
@ -31,18 +31,16 @@ const DashboardsTable: FC<Props> = ({ dashboards, onImport, onRemove }) => {
</td>
<td style={{ textAlign: 'right' }}>
{!dashboard.imported ? (
<button className="btn btn-secondary btn-small" onClick={() => onImport(dashboard, false)}>
<Button variant="secondary" size="sm" onClick={() => onImport(dashboard, false)}>
Import
</button>
</Button>
) : (
<button className="btn btn-secondary btn-small" onClick={() => onImport(dashboard, true)}>
<Button variant="secondary" size="sm" onClick={() => onImport(dashboard, true)}>
{buttonText(dashboard)}
</button>
</Button>
)}
{dashboard.imported && (
<button className="btn btn-danger btn-small" onClick={() => onRemove(dashboard)}>
<Icon name="trash-alt" />
</button>
<Button icon="trash-alt" variant="destructive" size="sm" onClick={() => onRemove(dashboard)} />
)}
</td>
</tr>

View File

@ -35,12 +35,13 @@ exports[`Render should render table 1`] = `
}
}
>
<button
className="btn btn-secondary btn-small"
<Button
onClick={[Function]}
size="sm"
variant="secondary"
>
Import
</button>
</Button>
</td>
</tr>
<tr
@ -67,20 +68,19 @@ exports[`Render should render table 1`] = `
}
}
>
<button
className="btn btn-secondary btn-small"
<Button
onClick={[Function]}
size="sm"
variant="secondary"
>
Update
</button>
<button
className="btn btn-danger btn-small"
</Button>
<Button
icon="trash-alt"
onClick={[Function]}
>
<Icon
name="trash-alt"
/>
</button>
size="sm"
variant="destructive"
/>
</td>
</tr>
</tbody>

View File

@ -21,7 +21,7 @@ import { getNavModel } from 'app/core/selectors/navModel';
// Types
import { StoreState } from 'app/types/';
import { DataSourceSettings } from '@grafana/data';
import { Alert, InfoBox } from '@grafana/ui';
import { Alert, Button, InfoBox, LinkButton } from '@grafana/ui';
import { getDataSourceLoadingNav } from '../state/navModel';
import PluginStateinfo from 'app/features/plugins/PluginStateInfo';
import { dataSourceLoaded, setDataSourceName, setIsDefault } from '../state/reducers';
@ -170,13 +170,13 @@ export class DataSourceSettingsPage extends PureComponent<Props> {
<div>
<div className="gf-form-button-row">
{showDelete && (
<button type="submit" className="btn btn-danger" onClick={this.onDelete}>
<Button type="submit" variant="destructive" onClick={this.onDelete}>
Delete
</button>
</Button>
)}
<a className="btn btn-inverse" href="datasources">
<LinkButton variant="link" href="datasources">
Back
</a>
</LinkButton>
</div>
</div>
</Page.Contents>

View File

@ -2,7 +2,7 @@ import React, { PureComponent } from 'react';
import { css, cx } from '@emotion/css';
import tinycolor from 'tinycolor2';
import { LogMessageAnsi, Themeable, withTheme, getLogRowStyles, Icon } from '@grafana/ui';
import { LogMessageAnsi, Themeable, withTheme, getLogRowStyles, Icon, Button } from '@grafana/ui';
import { GrafanaTheme, LogRowModel, TimeZone, dateTimeFormat } from '@grafana/data';
import { ElapsedTime } from './ElapsedTime';
@ -142,16 +142,16 @@ class LiveLogs extends PureComponent<Props, State> {
/>
</tbody>
</table>
<div className={cx([styles.logsRowsIndicator])}>
<button onClick={isPaused ? onResume : onPause} className={cx('btn btn-secondary', styles.button)}>
<div className={styles.logsRowsIndicator}>
<Button variant="secondary" onClick={isPaused ? onResume : onPause} className={styles.button}>
<Icon name={isPaused ? 'play' : 'pause'} />
&nbsp;
{isPaused ? 'Resume' : 'Pause'}
</button>
<button onClick={this.props.stopLive} className={cx('btn btn-inverse', styles.button)}>
</Button>
<Button variant="secondary" onClick={this.props.stopLive} className={styles.button}>
<Icon name="square-shape" size="lg" type="mono" />
&nbsp; Exit live mode
</button>
</Button>
{isPaused || (
<span>
Last line received: <ElapsedTime resetKey={this.props.logRows} humanize={true} /> ago

View File

@ -1,7 +1,7 @@
import React, { PureComponent } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import Page from 'app/core/components/Page/Page';
import { Tooltip, Icon } from '@grafana/ui';
import { Tooltip, Icon, Button } from '@grafana/ui';
import { SlideDown } from 'app/core/components/Animations/SlideDown';
import { getNavModel } from 'app/core/selectors/navModel';
import { StoreState } from 'app/types';
@ -105,9 +105,9 @@ export class FolderPermissions extends PureComponent<Props, State> {
<Icon className="icon--has-hover page-sub-heading-icon" name="question-circle" />
</Tooltip>
<div className="page-action-bar__spacer" />
<button className="btn btn-primary pull-right" onClick={this.onOpenAddPermissions} disabled={isAdding}>
<Button className="pull-right" onClick={this.onOpenAddPermissions} disabled={isAdding}>
Add Permission
</button>
</Button>
</div>
<SlideDown in={isAdding}>
<AddPermission onAddPermission={this.onAddPermission} onCancel={this.onCancelAddPermission} />

View File

@ -1,6 +1,6 @@
import React, { PureComponent } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { LegacyForms } from '@grafana/ui';
import { Button, LegacyForms } from '@grafana/ui';
const { Input } = LegacyForms;
import Page from 'app/core/components/Page/Page';
import appEvents from 'app/core/app_events';
@ -99,12 +99,12 @@ export class FolderSettingsPage extends PureComponent<Props, State> {
/>
</div>
<div className="gf-form-button-row">
<button type="submit" className="btn btn-primary" disabled={!folder.canSave || !folder.hasChanged}>
<Button type="submit" disabled={!folder.canSave || !folder.hasChanged}>
Save
</button>
<button className="btn btn-danger" onClick={this.onDelete} disabled={!folder.canSave}>
</Button>
<Button variant="destructive" onClick={this.onDelete} disabled={!folder.canSave}>
Delete
</button>
</Button>
</div>
</form>
</div>

View File

@ -37,20 +37,19 @@ exports[`Render should enable save button 1`] = `
<div
className="gf-form-button-row"
>
<button
className="btn btn-primary"
<Button
disabled={false}
type="submit"
>
Save
</button>
<button
className="btn btn-danger"
</Button>
<Button
disabled={false}
onClick={[Function]}
variant="destructive"
>
Delete
</button>
</Button>
</div>
</form>
</div>
@ -95,20 +94,19 @@ exports[`Render should render component 1`] = `
<div
className="gf-form-button-row"
>
<button
className="btn btn-primary"
<Button
disabled={true}
type="submit"
>
Save
</button>
<button
className="btn btn-danger"
</Button>
<Button
disabled={false}
onClick={[Function]}
variant="destructive"
>
Delete
</button>
</Button>
</div>
</form>
</div>

View File

@ -44,7 +44,9 @@ export class UserOrganizations extends PureComponent<Props> {
<td>{org.role}</td>
<td className="text-right">
{org.orgId === user.orgId ? (
<span className="btn btn-primary btn-small">Current</span>
<Button variant="secondary" size="sm" disabled>
Current
</Button>
) : (
<Button
variant="secondary"

View File

@ -2,13 +2,14 @@ import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import { SlideDown } from 'app/core/components/Animations/SlideDown';
import { LegacyForms, Tooltip, Icon } from '@grafana/ui';
import { LegacyForms, Tooltip, Icon, Button } from '@grafana/ui';
const { Input } = LegacyForms;
import { TeamGroup } from '../../types';
import { addTeamGroup, loadTeamGroups, removeTeamGroup } from './state/actions';
import { getTeamGroups } from './state/selectors';
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
import { CloseButton } from 'app/core/components/CloseButton/CloseButton';
export interface Props {
groups: TeamGroup[];
@ -65,9 +66,9 @@ export class TeamGroupSync extends PureComponent<Props, State> {
<tr key={group.groupId}>
<td>{group.groupId}</td>
<td style={{ width: '1%' }}>
<a className="btn btn-danger btn-small" onClick={() => this.onRemoveGroup(group)}>
<Icon name="times" style={{ marginBottom: 0 }} />
</a>
<Button size="sm" variant="destructive" onClick={() => this.onRemoveGroup(group)}>
<Icon name="times" />
</Button>
</td>
</tr>
);
@ -86,17 +87,15 @@ export class TeamGroupSync extends PureComponent<Props, State> {
</Tooltip>
<div className="page-action-bar__spacer" />
{groups.length > 0 && (
<button className="btn btn-primary pull-right" onClick={this.onToggleAdding}>
<Button className="pull-right" onClick={this.onToggleAdding}>
<Icon name="plus" /> Add group
</button>
</Button>
)}
</div>
<SlideDown in={isAdding}>
<div className="cta-form">
<button className="cta-form__close btn btn-transparent" onClick={this.onToggleAdding}>
<Icon name="times" />
</button>
<CloseButton onClick={this.onToggleAdding} />
<h5>Add External Group</h5>
<form className="gf-form-inline" onSubmit={this.onAddGroup}>
<div className="gf-form">
@ -110,9 +109,9 @@ export class TeamGroupSync extends PureComponent<Props, State> {
</div>
<div className="gf-form">
<button className="btn btn-primary gf-form-btn" type="submit" disabled={!this.isNewGroupValid()}>
<Button type="submit" disabled={!this.isNewGroupValid()}>
Add group
</button>
</Button>
</div>
</form>
</div>

View File

@ -1,6 +1,5 @@
import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import { Icon } from '@grafana/ui';
import { SlideDown } from 'app/core/components/Animations/SlideDown';
import { UserPicker } from 'app/core/components/Select/UserPicker';
import { TagBadge } from 'app/core/components/TagFilter/TagBadge';
@ -13,6 +12,8 @@ import { config } from 'app/core/config';
import { contextSrv, User as SignedInUser } from 'app/core/services/context_srv';
import TeamMemberRow from './TeamMemberRow';
import { setSearchMemberQuery } from './state/reducers';
import { CloseButton } from 'app/core/components/CloseButton/CloseButton';
import { Button } from '@grafana/ui';
export interface Props {
members: TeamMember[];
@ -79,28 +80,21 @@ export class TeamMembers extends PureComponent<Props, State> {
</div>
<div className="page-action-bar__spacer" />
<button
className="btn btn-primary pull-right"
onClick={this.onToggleAdding}
disabled={isAdding || !isTeamAdmin}
>
<Button className="pull-right" onClick={this.onToggleAdding} disabled={isAdding || !isTeamAdmin}>
Add member
</button>
</Button>
</div>
<SlideDown in={isAdding}>
<div className="cta-form">
<button className="cta-form__close btn btn-transparent" onClick={this.onToggleAdding}>
<Icon name="times" />
</button>
<CloseButton onClick={this.onToggleAdding} />
<h5>Add team member</h5>
<div className="gf-form-inline">
<UserPicker onSelected={this.onUserSelected} className="min-width-30" />
{this.state.newTeamMember && (
<button className="btn btn-primary gf-form-btn" type="submit" onClick={this.onAddUserToTeam}>
<Button type="submit" onClick={this.onAddUserToTeam}>
Add to team
</button>
</Button>
)}
</div>
</div>

View File

@ -29,14 +29,9 @@ exports[`Render should render component 1`] = `
<div
className="cta-form"
>
<button
className="cta-form__close btn btn-transparent"
<CloseButton
onClick={[Function]}
>
<Icon
name="times"
/>
</button>
/>
<h5>
Add External Group
</h5>
@ -58,13 +53,12 @@ exports[`Render should render component 1`] = `
<div
className="gf-form"
>
<button
className="btn btn-primary gf-form-btn"
<Button
disabled={true}
type="submit"
>
Add group
</button>
</Button>
</div>
</form>
</div>
@ -104,15 +98,15 @@ exports[`Render should render groups table 1`] = `
<div
className="page-action-bar__spacer"
/>
<button
className="btn btn-primary pull-right"
<Button
className="pull-right"
onClick={[Function]}
>
<Icon
name="plus"
/>
Add group
</button>
</Button>
</div>
<SlideDown
in={false}
@ -120,14 +114,9 @@ exports[`Render should render groups table 1`] = `
<div
className="cta-form"
>
<button
className="cta-form__close btn btn-transparent"
<CloseButton
onClick={[Function]}
>
<Icon
name="times"
/>
</button>
/>
<h5>
Add External Group
</h5>
@ -149,13 +138,12 @@ exports[`Render should render groups table 1`] = `
<div
className="gf-form"
>
<button
className="btn btn-primary gf-form-btn"
<Button
disabled={true}
type="submit"
>
Add group
</button>
</Button>
</div>
</form>
</div>
@ -194,19 +182,15 @@ exports[`Render should render groups table 1`] = `
}
}
>
<a
className="btn btn-danger btn-small"
<Button
onClick={[Function]}
size="sm"
variant="destructive"
>
<Icon
name="times"
style={
Object {
"marginBottom": 0,
}
}
/>
</a>
</Button>
</td>
</tr>
<tr
@ -222,19 +206,15 @@ exports[`Render should render groups table 1`] = `
}
}
>
<a
className="btn btn-danger btn-small"
<Button
onClick={[Function]}
size="sm"
variant="destructive"
>
<Icon
name="times"
style={
Object {
"marginBottom": 0,
}
}
/>
</a>
</Button>
</td>
</tr>
<tr
@ -250,19 +230,15 @@ exports[`Render should render groups table 1`] = `
}
}
>
<a
className="btn btn-danger btn-small"
<Button
onClick={[Function]}
size="sm"
variant="destructive"
>
<Icon
name="times"
style={
Object {
"marginBottom": 0,
}
}
/>
</a>
</Button>
</td>
</tr>
</tbody>

View File

@ -17,13 +17,13 @@ exports[`Render should render component 1`] = `
<div
className="page-action-bar__spacer"
/>
<button
className="btn btn-primary pull-right"
<Button
className="pull-right"
disabled={false}
onClick={[Function]}
>
Add member
</button>
</Button>
</div>
<SlideDown
in={false}
@ -31,14 +31,9 @@ exports[`Render should render component 1`] = `
<div
className="cta-form"
>
<button
className="cta-form__close btn btn-transparent"
<CloseButton
onClick={[Function]}
>
<Icon
name="times"
/>
</button>
/>
<h5>
Add team member
</h5>
@ -109,13 +104,13 @@ exports[`Render should render team members 1`] = `
<div
className="page-action-bar__spacer"
/>
<button
className="btn btn-primary pull-right"
<Button
className="pull-right"
disabled={false}
onClick={[Function]}
>
Add member
</button>
</Button>
</div>
<SlideDown
in={false}
@ -123,14 +118,9 @@ exports[`Render should render team members 1`] = `
<div
className="cta-form"
>
<button
className="cta-form__close btn btn-transparent"
<CloseButton
onClick={[Function]}
>
<Icon
name="times"
/>
</button>
/>
<h5>
Add team member
</h5>

View File

@ -1,5 +1,5 @@
import React, { MouseEvent, PureComponent } from 'react';
import { Icon } from '@grafana/ui';
import { Icon, LinkButton } from '@grafana/ui';
import { selectors } from '@grafana/e2e-selectors';
import { toVariableIdentifier, toVariablePayload, VariableIdentifier } from '../state/types';
@ -98,14 +98,13 @@ class VariableEditorContainerUnconnected extends PureComponent<Props> {
{this.props.variables.length > 0 && variableToEdit === null && (
<>
<VariablesDependenciesButton variables={this.props.variables} />
<a
<LinkButton
type="button"
className="btn btn-primary"
onClick={this.onNewVariable}
aria-label={selectors.pages.Dashboard.Settings.Variables.List.newButton}
>
New
</a>
</LinkButton>
</>
)}
</div>

View File

@ -1,5 +1,6 @@
// Libraries
import React, { PureComponent } from 'react';
import { LinkButton } from '@grafana/ui';
// Types
import { PluginConfigPageProps, DataSourcePluginMeta, DataSourceJsonData } from '@grafana/data';
@ -17,14 +18,14 @@ export class TestInfoTab extends PureComponent<Props> {
See github for more information about setting up a reproducible test environment.
<br />
<br />
<a
className="btn btn-inverse"
<LinkButton
variant="secondary"
href="https://github.com/grafana/grafana/tree/master/devenv"
target="_blank"
rel="noopener noreferrer"
>
GitHub
</a>
</LinkButton>
<br />
</div>
);