Migrates Button component from Bootstrap to AntD (#12832)

This commit is contained in:
Michael S. Molina 2021-02-01 20:13:10 -03:00 committed by GitHub
parent 51195af4fa
commit c781ab8adf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 605 additions and 558 deletions

View File

@ -17,7 +17,7 @@
* under the License.
*/
import React from 'react';
import { shallow, ShallowWrapper } from 'enzyme';
import { ReactWrapper } from 'enzyme';
import Button from 'src/components/Button';
import Select from 'src/components/Select';
import AddSliceContainer, {
@ -25,6 +25,7 @@ import AddSliceContainer, {
AddSliceContainerState,
} from 'src/addSlice/AddSliceContainer';
import VizTypeControl from 'src/explore/components/controls/VizTypeControl';
import { styledMount as mount } from 'spec/helpers/theming';
const defaultProps = {
datasources: [
@ -34,14 +35,18 @@ const defaultProps = {
};
describe('AddSliceContainer', () => {
let wrapper: ShallowWrapper<
let wrapper: ReactWrapper<
AddSliceContainerProps,
AddSliceContainerState,
AddSliceContainer
>;
beforeEach(() => {
wrapper = shallow(<AddSliceContainer {...defaultProps} />);
wrapper = mount(<AddSliceContainer {...defaultProps} />) as ReactWrapper<
AddSliceContainerProps,
AddSliceContainerState,
AddSliceContainer
>;
});
it('uses table as default visType', () => {
@ -58,9 +63,9 @@ describe('AddSliceContainer', () => {
});
it('renders a disabled button if no datasource is selected', () => {
expect(wrapper.find(Button).dive().find({ disabled: true })).toHaveLength(
1,
);
expect(
wrapper.find(Button).find({ disabled: true }).hostNodes(),
).toHaveLength(1);
});
it('renders an enabled button if datasource is selected', () => {
@ -70,9 +75,9 @@ describe('AddSliceContainer', () => {
datasourceId: datasourceValue.split('__')[0],
datasourceType: datasourceValue.split('__')[1],
});
expect(wrapper.find(Button).dive().find({ disabled: true })).toHaveLength(
0,
);
expect(
wrapper.find(Button).find({ disabled: true }).hostNodes(),
).toHaveLength(0);
});
it('formats explore url', () => {

View File

@ -20,6 +20,7 @@ import React from 'react';
import { styledMount as mount } from 'spec/helpers/theming';
import { Provider } from 'react-redux';
import FilterBar from 'src/dashboard/components/nativeFilters/FilterBar';
import Button from 'src/components/Button';
import { mockStore } from 'spec/fixtures/mockStore';
describe('FilterBar', () => {
@ -42,7 +43,8 @@ describe('FilterBar', () => {
expect(wrapper.find({ name: 'collapse' })).toExist();
});
it('has apply and reset all buttons', () => {
expect(wrapper.find('.btn-primary')).toExist();
expect(wrapper.find('.btn-secondary')).toExist();
expect(wrapper.find(Button).length).toBe(2);
expect(wrapper.find(Button).at(0)).toHaveProp('buttonStyle', 'secondary');
expect(wrapper.find(Button).at(1)).toHaveProp('buttonStyle', 'primary');
});
});

View File

@ -17,7 +17,7 @@
* under the License.
*/
import React from 'react';
import { shallow } from 'enzyme';
import { styledMount as mount } from 'spec/helpers/theming';
import sinon from 'sinon';
import QueryAndSaveButtons from 'src/explore/components/QueryAndSaveBtns';
@ -38,23 +38,21 @@ describe('QueryAndSaveButtons', () => {
// Test the output
describe('output', () => {
let wrapper;
beforeEach(() => {
wrapper = shallow(<QueryAndSaveButtons {...defaultProps} />);
});
const wrapper = mount(<QueryAndSaveButtons {...defaultProps} />);
it('renders 2 buttons', () => {
expect(wrapper.find(Button)).toHaveLength(2);
});
it('renders buttons with correct text', () => {
expect(wrapper.find(Button).contains('Run')).toBe(true);
expect(wrapper.find(Button).contains('Save')).toBe(true);
expect(wrapper.find(Button).at(0).text().trim()).toBe('Run');
expect(wrapper.find(Button).at(1).text().trim()).toBe('Save');
});
it('calls onQuery when query button is clicked', () => {
const queryButton = wrapper.find('[data-test="run-query-button"]');
const queryButton = wrapper
.find('[data-test="run-query-button"]')
.hostNodes();
queryButton.simulate('click');
expect(defaultProps.onQuery.called).toBe(true);
});

View File

@ -308,7 +308,7 @@ export default class CRUDCollection extends React.PureComponent<
{this.props.allowAddItem && (
<span className="m-t-10 m-r-10">
<Button
buttonSize="sm"
buttonSize="small"
buttonStyle="primary"
onClick={this.onAddItem}
data-test="add-item-button"

View File

@ -17,7 +17,8 @@
* under the License.
*/
import React, { CSSProperties } from 'react';
import { Alert, ButtonGroup } from 'react-bootstrap';
import { Alert } from 'react-bootstrap';
import ButtonGroup from 'src/components/ButtonGroup';
import ProgressBar from 'src/common/components/ProgressBar';
import moment from 'moment';
import { RadioChangeEvent } from 'antd/lib/radio';
@ -580,7 +581,7 @@ export default class ResultSet extends React.PureComponent<
if (query.isDataPreview) {
return (
<Button
buttonSize="sm"
buttonSize="small"
className="fetch"
buttonStyle="primary"
onClick={() =>
@ -597,7 +598,7 @@ export default class ResultSet extends React.PureComponent<
if (query.resultsKey) {
return (
<Button
buttonSize="sm"
buttonSize="small"
className="fetch"
buttonStyle="primary"
onClick={() => this.fetchResults(query)}

View File

@ -101,7 +101,6 @@ export const SaveDatasetModal: FunctionComponent<SaveDatasetModalProps> = ({
{!shouldOverwriteDataset && (
<Button
disabled={disableSaveAndExploreBtn}
buttonSize="medium"
buttonStyle="primary"
onClick={onOk}
>
@ -110,12 +109,9 @@ export const SaveDatasetModal: FunctionComponent<SaveDatasetModalProps> = ({
)}
{shouldOverwriteDataset && (
<>
<Button buttonSize="medium" onClick={handleOverwriteCancel}>
Back
</Button>
<Button onClick={handleOverwriteCancel}>Back</Button>
<Button
className="md"
buttonSize="medium"
buttonStyle="primary"
onClick={handleOverwriteDataset}
disabled={disableSaveAndExploreBtn}

View File

@ -18,7 +18,8 @@
*/
import React from 'react';
import PropTypes from 'prop-types';
import { ButtonGroup, Collapse, Well } from 'react-bootstrap';
import { Collapse, Well } from 'react-bootstrap';
import ButtonGroup from 'src/components/ButtonGroup';
import shortid from 'shortid';
import { t, styled } from '@superset-ui/core';

View File

@ -1,147 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import Button from './index';
export default {
title: 'Button',
component: Button,
includeStories: ['ButtonGallery', 'InteractiveButton'],
};
export const STYLES = {
label: 'Types',
options: {
Primary: 'primary',
Secondary: 'secondary',
Tertiary: 'tertiary',
Dashed: 'dashed',
Danger: 'danger',
Warning: 'warning',
Success: 'success',
Link: 'link',
Default: 'default',
None: null,
},
defaultValue: null,
// groupId: 'ButtonType',
};
export const SIZES = {
label: 'Sizes',
options: {
XS: 'xsmall',
S: 'small',
Default: null,
L: 'large',
},
defaultValue: null,
};
// TODO remove the use of these class names in the codebase where they're not necessary
// 'fetch' // haven't yet seen this (in ResultSet.tsx) actually show up to verify the styles are needed
// 'm-r-3' // open a PR with a prop of `pullRight` that adds an automatic right-margin for second and subseqent sibling buttons.
const TYPES = {
label: 'Type',
options: {
Submit: 'submit',
Button: 'button',
None: null,
},
defaultValue: null,
};
const TARGETS = {
label: 'Target',
options: {
Blank: '_blank',
None: null,
},
defaultValue: null,
};
const HREFS = {
label: 'HREF',
options: {
Superset: 'http://https://superset.apache.org/',
None: null,
},
defaultValue: null,
};
export const ButtonGallery = () => (
<>
{Object.entries(SIZES.options).map(([name, size]) => (
<div key={size}>
<h4>{name}</h4>
{Object.values(STYLES.options)
.filter(o => o)
.map(style => (
<Button
buttonStyle={style}
buttonSize={size}
onClick={() => true}
key={`${style}_${size}`}
>
{style}
</Button>
))}
</div>
))}
</>
);
export const InteractiveButton = args => {
const { label, ...btnArgs } = args;
return <Button {...btnArgs}>{label}</Button>;
};
InteractiveButton.args = {
buttonStyle: STYLES.defaultValue,
buttonSize: SIZES.defaultValue,
type: TYPES.defaultValue,
target: TARGETS.defaultValue,
href: HREFS.defaultValue,
label: 'Button!',
};
InteractiveButton.argTypes = {
buttonStyle: {
name: STYLES.label,
control: { type: 'select', options: Object.values(STYLES.options) },
},
size: {
name: SIZES.label,
control: { type: 'select', options: Object.values(SIZES.options) },
},
type: {
name: TYPES.label,
control: { type: 'select', options: Object.values(TYPES.options) },
},
target: {
name: TARGETS.label,
control: { type: 'select', options: Object.values(TARGETS.options) },
},
href: {
name: HREFS.label,
control: { type: 'select', options: Object.values(HREFS.options) },
},
onClick: { action: 'clicked' },
label: { name: 'Label', control: { type: 'text' } },
};
ButtonGallery.argTypes = { onClick: { action: 'clicked' } };

View File

@ -0,0 +1,141 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import Button, { ButtonProps } from './index';
type ButtonStyle = Pick<ButtonProps, 'buttonStyle'>;
type ButtonStyleValue = ButtonStyle[keyof ButtonStyle];
type ButtonSize = Pick<ButtonProps, 'buttonSize'>;
type ButtonSizeValue = ButtonSize[keyof ButtonSize];
export default {
title: 'Button',
component: Button,
includeStories: ['ButtonGallery', 'InteractiveButton'],
};
const buttonStyles: ButtonStyleValue[] = [
'primary',
'secondary',
'tertiary',
'dashed',
'danger',
'warning',
'success',
'link',
'default',
];
const buttonSizes: ButtonSizeValue[] = ['xsmall', 'small', 'default'];
export const STYLES = {
label: 'styles',
options: buttonStyles,
defaultValue: undefined,
};
export const SIZES = {
label: 'sizes',
options: buttonSizes,
defaultValue: undefined,
};
const TARGETS = {
label: 'target',
options: {
blank: '_blank',
none: null,
},
defaultValue: null,
};
const HREFS = {
label: 'href',
options: {
superset: 'https://superset.apache.org/',
none: null,
},
defaultValue: null,
};
export const ButtonGallery = () => (
<>
{SIZES.options.map(size => (
<div key={size} style={{ marginBottom: 40 }}>
<h4>{size}</h4>
{Object.values(STYLES.options).map(style => (
<Button
buttonStyle={style}
buttonSize={size}
onClick={() => true}
key={`${style}_${size}`}
style={{ marginRight: 20, marginBottom: 10 }}
>
{style}
</Button>
))}
</div>
))}
</>
);
ButtonGallery.story = {
parameters: {
actions: {
disabled: true,
},
controls: {
disabled: true,
},
knobs: {
disabled: true,
},
},
};
export const InteractiveButton = (args: ButtonProps & { label: string }) => {
const { label, ...btnArgs } = args;
return <Button {...btnArgs}>{label}</Button>;
};
InteractiveButton.story = {
parameters: {
knobs: {
disabled: true,
},
},
};
InteractiveButton.args = {
buttonStyle: 'default',
buttonSize: 'default',
label: 'Button!',
};
InteractiveButton.argTypes = {
target: {
name: TARGETS.label,
control: { type: 'select', options: Object.values(TARGETS.options) },
},
href: {
name: HREFS.label,
control: { type: 'select', options: Object.values(HREFS.options) },
},
onClick: { action: 'clicked' },
};

View File

@ -59,10 +59,4 @@ describe('Button', () => {
expect(wrapper.find(Button).length).toEqual(permutationCount);
});
// test things NOT in the storybook!
it('renders custom button styles without melting', () => {
wrapper = mount(<Button buttonStyle="foobar" />);
expect(wrapper.find('Button.btn-foobar')).toHaveLength(1);
});
});

View File

@ -20,18 +20,11 @@ import React, { CSSProperties } from 'react';
import { kebabCase } from 'lodash';
import { mix } from 'polished';
import cx from 'classnames';
import { Button as BootstrapButton } from 'react-bootstrap';
import { styled } from '@superset-ui/core';
import { Button as AntdButton } from 'src/common/components';
import { useTheme } from '@superset-ui/core';
import { Tooltip } from 'src/common/components/Tooltip';
import { Menu } from 'src/common/components';
export type OnClickHandler = React.MouseEventHandler<BootstrapButton>;
export interface DropdownItemProps {
label: string;
url: string;
icon?: string;
}
export type OnClickHandler = React.MouseEventHandler<HTMLElement>;
export interface ButtonProps {
id?: string;
@ -52,264 +45,164 @@ export interface ButtonProps {
| 'rightBottom';
onClick?: OnClickHandler;
disabled?: boolean;
buttonStyle?: string;
btnStyles?: string;
buttonSize?: BootstrapButton.ButtonProps['bsSize'];
style?: BootstrapButton.ButtonProps['style'];
buttonStyle?:
| 'primary'
| 'secondary'
| 'tertiary'
| 'success'
| 'warning'
| 'danger'
| 'default'
| 'link'
| 'dashed';
buttonSize?: 'default' | 'small' | 'xsmall';
style?: CSSProperties;
children?: React.ReactNode;
dropdownItems?: DropdownItemProps[];
href?: string; // React-Bootstrap creates a link when this is passed in.
target?: string; // React-Bootstrap creates a link when this is passed in.
type?: string; // React-Bootstrap supports this when rendering an HTML button element
href?: string;
htmlType?: 'button' | 'submit' | 'reset';
cta?: boolean;
}
const BUTTON_WRAPPER_STYLE = { display: 'inline-block', cursor: 'not-allowed' };
export default function Button(props: ButtonProps) {
const {
tooltip,
placement,
disabled = false,
buttonSize,
buttonStyle,
className,
cta,
children,
href,
...restProps
} = props;
const SupersetButton = styled(BootstrapButton)`
&:focus,
&:active,
&:focus:active {
outline: none;
box-shadow: none;
}
transition: all ${({ theme }) => theme.transitionTiming}s;
border-radius: ${({ theme }) => theme.borderRadius}px;
border: none;
font-size: ${({ theme }) => theme.typography.sizes.s}px;
font-weight: ${({ theme }) => theme.typography.weights.bold};
margin-left: ${({ theme }) => theme.gridUnit * 4}px;
&:first-of-type {
margin-left: 0;
const theme = useTheme();
const { colors, transitionTiming, borderRadius, typography } = theme;
const { primary, grayscale, success, warning, error } = colors;
let height = 32;
let padding = 18;
if (buttonSize === 'xsmall') {
height = 22;
padding = 5;
} else if (buttonSize === 'small') {
height = 30;
padding = 10;
}
i {
padding: 0 ${({ theme }) => theme.gridUnit * 2}px 0 0;
let backgroundColor = primary.light4;
let backgroundColorHover = mix(0.1, primary.base, primary.light4);
let backgroundColorActive = mix(0.25, primary.base, primary.light4);
let backgroundColorDisabled = grayscale.light2;
let color = primary.dark1;
let colorHover = color;
let borderWidth = 0;
let borderStyle = 'none';
let borderColor = 'transparent';
let borderColorHover = 'transparent';
let borderColorDisabled = 'transparent';
if (buttonStyle === 'primary') {
backgroundColor = primary.dark1;
backgroundColorHover = mix(0.1, grayscale.light5, primary.dark1);
backgroundColorActive = mix(0.2, grayscale.dark2, primary.dark1);
color = grayscale.light5;
colorHover = color;
} else if (buttonStyle === 'tertiary' || buttonStyle === 'dashed') {
backgroundColor = grayscale.light5;
backgroundColorHover = grayscale.light5;
backgroundColorActive = grayscale.light5;
backgroundColorDisabled = grayscale.light5;
borderWidth = 1;
borderStyle = buttonStyle === 'dashed' ? 'dashed' : 'solid';
borderColor = primary.dark1;
borderColorHover = primary.light1;
borderColorDisabled = grayscale.light2;
} else if (buttonStyle === 'danger') {
backgroundColor = error.base;
backgroundColorHover = mix(0.1, grayscale.light5, error.base);
backgroundColorActive = mix(0.2, grayscale.dark2, error.base);
color = grayscale.light5;
colorHover = color;
} else if (buttonStyle === 'warning') {
backgroundColor = warning.base;
backgroundColorHover = mix(0.1, grayscale.dark2, warning.base);
backgroundColorActive = mix(0.2, grayscale.dark2, warning.base);
color = grayscale.light5;
colorHover = color;
} else if (buttonStyle === 'success') {
backgroundColor = success.base;
backgroundColorHover = mix(0.1, grayscale.light5, success.base);
backgroundColorActive = mix(0.2, grayscale.dark2, success.base);
color = grayscale.light5;
colorHover = color;
} else if (buttonStyle === 'link') {
backgroundColor = 'transparent';
backgroundColorHover = 'transparent';
backgroundColorActive = 'transparent';
colorHover = primary.base;
}
/* SIP 34 colors! */
&.btn {
border: 1px solid transparent; /* this just makes sure the height is the same as tertiary/dashed buttons */
&:hover,
&:active {
border: 1px solid transparent;
}
&-default,
&-secondary {
background-color: ${({ theme }) => theme.colors.primary.light4};
color: ${({ theme }) => theme.colors.primary.dark1};
&:hover {
background-color: ${({ theme }) =>
mix(0.1, theme.colors.grayscale.light5, theme.colors.primary.light4)};
color: ${({ theme }) => theme.colors.primary.dark1};
}
&:active {
background-color: ${({ theme }) =>
mix(0.25, theme.colors.primary.base, theme.colors.primary.light4)};
color: ${({ theme }) => theme.colors.primary.dark1};
}
}
&-tertiary,
&-dashed {
border-width: 1px;
border-style: solid;
background-color: ${({ theme }) => theme.colors.grayscale.light5};
color: ${({ theme }) => theme.colors.primary.dark1};
border-color: ${({ theme }) => theme.colors.primary.dark1};
&:hover {
background-color: ${({ theme }) => theme.colors.grayscale.light5};
color: ${({ theme }) => theme.colors.primary.dark1};
border-color: ${({ theme }) => theme.colors.primary.light1};
}
&:active {
background-color: ${({ theme }) => theme.colors.grayscale.light5};
color: ${({ theme }) => theme.colors.primary.dark1};
border-color: ${({ theme }) => theme.colors.primary.dark1};
}
&[disabled],
&[disabled]:hover {
background-color: ${({ theme }) => theme.colors.grayscale.light5};
color: ${({ theme }) => theme.colors.grayscale.base};
border-color: ${({ theme }) => theme.colors.grayscale.light2};
}
}
&-dashed {
border-style: dashed;
&:hover,
&:active {
border-style: dashed;
}
}
&-link {
background: none;
text-decoration: none;
color: ${({ theme }) => theme.colors.primary.dark1};
&:hover {
background: none;
color: ${({ theme }) => theme.colors.primary.base};
}
&:active {
background: none;
color: ${({ theme }) => theme.colors.primary.dark1};
}
&[disabled],
&[disabled]:hover {
background: none;
color: ${({ theme }) => theme.colors.grayscale.base};
}
}
&-primary {
background-color: ${({ theme }) => theme.colors.primary.dark1};
color: ${({ theme }) => theme.colors.grayscale.light5};
&:hover {
background-color: ${({ theme }) =>
mix(0.1, theme.colors.grayscale.light5, theme.colors.primary.dark1)};
color: ${({ theme }) => theme.colors.grayscale.light5};
}
&:active {
background-color: ${({ theme }) =>
mix(0.2, theme.colors.grayscale.dark2, theme.colors.primary.dark1)};
color: ${({ theme }) => theme.colors.grayscale.light5};
}
}
&-danger {
background-color: ${({ theme }) => theme.colors.error.base};
color: ${({ theme }) => theme.colors.grayscale.light5};
&:hover {
background-color: ${({ theme }) =>
mix(0.1, theme.colors.grayscale.light5, theme.colors.error.base)};
color: ${({ theme }) => theme.colors.grayscale.light5};
}
&:active {
background-color: ${({ theme }) =>
mix(0.2, theme.colors.grayscale.dark2, theme.colors.error.base)};
color: ${({ theme }) => theme.colors.grayscale.light5};
}
}
&-success {
background-color: ${({ theme }) => theme.colors.success.base};
color: ${({ theme }) => theme.colors.grayscale.light5};
&:hover {
background-color: ${({ theme }) =>
mix(0.1, theme.colors.grayscale.light5, theme.colors.success.base)};
color: ${({ theme }) => theme.colors.grayscale.light5};
}
&:active {
background-color: ${({ theme }) =>
mix(0.2, theme.colors.grayscale.dark2, theme.colors.success.base)};
color: ${({ theme }) => theme.colors.grayscale.light5};
}
}
&-warning {
background-color: ${({ theme }) => theme.colors.warning.base};
color: ${({ theme }) => theme.colors.grayscale.light5};
&:hover {
background-color: ${({ theme }) =>
mix(0.1, theme.colors.grayscale.light5, theme.colors.warning.base)};
color: ${({ theme }) => theme.colors.grayscale.light5};
}
&:active {
background-color: ${({ theme }) =>
mix(0.2, theme.colors.grayscale.dark2, theme.colors.warning.base)};
color: ${({ theme }) => theme.colors.grayscale.light5};
}
}
&-info {
background-color: ${({ theme }) => theme.colors.info.dark1};
color: ${({ theme }) => theme.colors.grayscale.light5};
&:hover {
background-color: ${({ theme }) =>
mix(0.1, theme.colors.grayscale.light5, theme.colors.info.dark1)};
color: ${({ theme }) => theme.colors.grayscale.light5};
}
&:active {
background-color: ${({ theme }) =>
mix(0.2, theme.colors.grayscale.dark2, theme.colors.info.dark1)};
color: ${({ theme }) => theme.colors.grayscale.light5};
}
}
&[disabled],
&[disabled]:hover {
background-color: ${({ theme }) => theme.colors.grayscale.light2};
color: ${({ theme }) => theme.colors.grayscale.base};
}
}
/* big Call to Action buttons */
&.cta {
min-width: ${({ theme }) => theme.gridUnit * 36}px;
min-height: ${({ theme }) => theme.gridUnit * 8}px;
text-transform: uppercase;
}
`;
export default function Button({
tooltip,
placement,
dropdownItems,
disabled = false,
buttonSize: bsSize,
buttonStyle: bsStyle,
className,
style: style_,
cta,
children,
...restProps
}: ButtonProps) {
// Working around the fact that tooltips don't get triggered when buttons are disabled
// https://github.com/react-bootstrap/react-bootstrap/issues/1588
const style: CSSProperties | undefined =
tooltip && disabled ? { ...style_, pointerEvents: 'none' } : style_;
const officialBootstrapStyles = [
'success',
'warning',
'danger',
'info',
'default',
'primary',
];
const transformedProps = {
...restProps,
disabled,
bsSize,
bsStyle: officialBootstrapStyles.includes(bsStyle || '')
? bsStyle
: 'default',
className: cx(className, {
cta: !!cta,
[`btn-${bsStyle}`]: !officialBootstrapStyles.includes(bsStyle || ''),
}),
style,
};
let button = (
<SupersetButton {...transformedProps}>{children}</SupersetButton>
const button = (
<AntdButton
href={disabled ? undefined : href}
disabled={disabled}
className={cx(className, { cta: !!cta })}
css={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
lineHeight: 1.5715,
fontSize: typography.sizes.s,
fontWeight: typography.weights.bold,
height,
textTransform: 'uppercase',
padding: `0px ${padding}px`,
transition: `all ${transitionTiming}s`,
minWidth: cta ? theme.gridUnit * 36 : undefined,
minHeight: cta ? theme.gridUnit * 8 : undefined,
boxShadow: 'none',
borderWidth,
borderStyle,
borderColor,
borderRadius,
color,
backgroundColor,
'&:hover': {
color: colorHover,
backgroundColor: backgroundColorHover,
borderColor: borderColorHover,
},
'&:active': {
color,
backgroundColor: backgroundColorActive,
},
'&:focus': {
color,
backgroundColor,
borderColor,
},
'&[disabled], &[disabled]:hover': {
color: grayscale.base,
backgroundColor: backgroundColorDisabled,
borderColor: borderColorDisabled,
},
'i:first-of-type, svg:first-of-type': {
marginRight: theme.gridUnit * 2,
padding: `0 ${theme.gridUnit * 2} 0 0`,
},
marginLeft: theme.gridUnit * 2,
'&:first-of-type': {
marginLeft: 0,
},
}}
{...restProps}
>
{children}
</AntdButton>
);
if (dropdownItems) {
button = (
<div style={BUTTON_WRAPPER_STYLE}>
<SupersetButton {...transformedProps} data-toggle="dropdown">
{children}
</SupersetButton>
<ul className="dropdown-menu">
<Menu>
{dropdownItems.map((dropdownItem: DropdownItemProps) => (
<Menu.Item key={`${dropdownItem.label}`}>
<a href={dropdownItem.url}>
<i className={`fa ${dropdownItem.icon}`} />
&nbsp; {dropdownItem.label}
</a>
</Menu.Item>
))}
</Menu>
</ul>
</div>
);
}
if (tooltip) {
return (
<Tooltip

View File

@ -0,0 +1,70 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import Button, { ButtonProps } from 'src/components/Button';
import { STYLES, SIZES } from 'src/components/Button/Button.stories';
import ButtonGroup from './index';
export default {
title: 'ButtonGroup',
component: ButtonGroup,
};
export const InteractiveButtonGroup = (args: ButtonProps) => (
<>
<ButtonGroup css={{ marginBottom: 40 }}>
<Button {...args}>Button 1</Button>
</ButtonGroup>
<ButtonGroup css={{ marginBottom: 40 }}>
<Button {...args}>Button 1</Button>
<Button {...args}>Button 2</Button>
</ButtonGroup>
<ButtonGroup>
<Button {...args}>Button 1</Button>
<Button {...args}>Button 2</Button>
<Button {...args}>Button 3</Button>
</ButtonGroup>
</>
);
InteractiveButtonGroup.args = {
buttonStyle: 'tertiary',
buttonSize: 'default',
};
InteractiveButtonGroup.argTypes = {
buttonStyle: {
name: STYLES.label,
control: { type: 'select', options: STYLES.options },
},
buttonSize: {
name: SIZES.label,
control: { type: 'select', options: SIZES.options },
},
};
InteractiveButtonGroup.story = {
parameters: {
actions: {
disabled: true,
},
knobs: {
disabled: true,
},
},
};

View File

@ -0,0 +1,50 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { ReactWrapper } from 'enzyme';
import { styledMount as mount } from 'spec/helpers/theming';
import Button from 'src/components/Button';
import ButtonGroup from '.';
describe('ButtonGroup', () => {
let wrapper: ReactWrapper;
it('renders 1 button', () => {
expect(
React.isValidElement(
<ButtonGroup>
<Button>Button</Button>
</ButtonGroup>,
),
).toBe(true);
});
it('renders 3 buttons', () => {
wrapper = mount(
<ButtonGroup>
<Button>Button</Button>
<Button>Button</Button>
<Button>Button</Button>
</ButtonGroup>,
);
expect(wrapper.find(Button).length).toEqual(3);
});
});

View File

@ -0,0 +1,53 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
export interface ButtonGroupProps {
className?: string;
children?: React.ReactNode;
}
export default function ButtonGroup(props: ButtonGroupProps) {
const { className, children } = props;
return (
<div
className={className}
css={{
'& :nth-child(1):not(:nth-last-child(1))': {
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
borderRight: 0,
marginLeft: 0,
},
'& :not(:nth-child(1)):not(:nth-last-child(1))': {
borderRadius: 0,
borderRight: 0,
marginLeft: 0,
},
'& :nth-last-child(1):not(:nth-child(1))': {
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
marginLeft: 0,
},
}}
>
{children}
</div>
);
}

View File

@ -21,7 +21,7 @@ import thunk from 'redux-thunk';
import configureStore from 'redux-mock-store';
import { styledMount as mount } from 'spec/helpers/theming';
import { ReactWrapper } from 'enzyme';
import Button from 'src/components/Button';
import { ImportResourceName } from 'src/views/CRUD/types';
import ImportModelsModal from 'src/components/ImportModal';
import Modal from 'src/common/components/Modal';
@ -82,18 +82,13 @@ describe('ImportModelsModal', () => {
});
it('should render the import button initially disabled', () => {
expect(wrapper.find('button[children="Import"]').prop('disabled')).toBe(
true,
);
expect(wrapper.find(Button).at(1).prop('disabled')).toBe(true);
});
it('should render the import button enabled when a file is selected', () => {
const file = new File([new ArrayBuffer(1)], 'model_export.zip');
wrapper.find('input').simulate('change', { target: { files: [file] } });
expect(wrapper.find('button[children="Import"]').prop('disabled')).toBe(
false,
);
expect(wrapper.find(Button).at(1).prop('disabled')).toBe(false);
});
it('should render password fields when needed for import', () => {

View File

@ -21,7 +21,7 @@ import moment from 'moment';
import React from 'react';
import PropTypes from 'prop-types';
import { styled, CategoricalColorNamespace, t } from '@superset-ui/core';
import { ButtonGroup } from 'react-bootstrap';
import ButtonGroup from 'src/components/ButtonGroup';
import {
LOG_ACTIONS_PERIODIC_RENDER_DASHBOARD,

View File

@ -296,8 +296,8 @@ class PropertiesModal extends React.PureComponent {
footer={
<>
<Button
type="button"
buttonSize="sm"
htmlType="button"
buttonSize="small"
onClick={onHide}
data-test="properties-modal-cancel-button"
cta
@ -306,7 +306,7 @@ class PropertiesModal extends React.PureComponent {
</Button>
<Button
onClick={this.submit}
buttonSize="sm"
buttonSize="small"
buttonStyle="primary"
className="m-r-5"
disabled={errors.length > 0}

View File

@ -135,10 +135,14 @@ class RefreshIntervalModal extends React.PureComponent<
}
modalFooter={
<>
<Button buttonStyle="primary" buttonSize="sm" onClick={this.onSave}>
<Button
buttonStyle="primary"
buttonSize="small"
onClick={this.onSave}
>
{editMode ? t('Save') : t('Save for this session')}
</Button>
<Button onClick={this.onCancel} buttonSize="sm">
<Button onClick={this.onCancel} buttonSize="small">
{t('Cancel')}
</Button>
</>

View File

@ -531,11 +531,15 @@ export default class FilterScopeSelector extends React.PureComponent {
</div>
<ActionsContainer>
<Button buttonSize="sm" onClick={this.onClose}>
<Button buttonSize="small" onClick={this.onClose}>
{t('Close')}
</Button>
{showSelector && (
<Button buttonSize="sm" buttonStyle="primary" onClick={this.onSave}>
<Button
buttonSize="small"
buttonStyle="primary"
onClick={this.onSave}
>
{t('Save')}
</Button>
)}

View File

@ -497,15 +497,15 @@ const FilterBar: React.FC<FiltersBarProps> = ({
<ActionButtons>
<Button
buttonStyle="secondary"
buttonSize="sm"
buttonSize="small"
onClick={handleResetAll}
>
{t('Reset all')}
</Button>
<Button
buttonStyle="primary"
type="submit"
buttonSize="sm"
htmlType="submit"
buttonSize="small"
onClick={handleApply}
>
{t('Apply')}

View File

@ -1000,7 +1000,7 @@ class DatasourceEditor extends React.PureComponent {
<ColumnButtonWrapper>
<span className="m-t-10 m-r-10">
<Button
buttonSize="sm"
buttonSize="small"
buttonStyle="primary"
onClick={this.syncMetadata}
className="sync-from-source"

View File

@ -181,7 +181,7 @@ const DatasourceModal: FunctionComponent<DatasourceModalProps> = ({
<>
{isFeatureEnabled(FeatureFlag.ENABLE_REACT_CRUD_VIEWS) && (
<Button
buttonSize="sm"
buttonSize="small"
buttonStyle="default"
data-test="datasource-modal-legacy-edit"
className="m-r-5"
@ -195,14 +195,14 @@ const DatasourceModal: FunctionComponent<DatasourceModalProps> = ({
)}
<Button
data-test="datasource-modal-cancel"
buttonSize="sm"
buttonSize="small"
className="m-r-5"
onClick={onHide}
>
{t('Cancel')}
</Button>
<Button
buttonSize="sm"
buttonSize="small"
buttonStyle="primary"
data-test="datasource-modal-save"
onClick={onClickSave}

View File

@ -45,7 +45,7 @@ export const CopyButton = styled(Button)`
`;
const CopyNode = (
<CopyButton buttonSize="xs">
<CopyButton buttonSize="xsmall">
<i className="fa fa-clipboard" />
</CopyButton>
);

View File

@ -140,7 +140,7 @@ export const DisplayQueryButton = props => {
text={query}
shouldShowText={false}
copyNode={
<CopyButtonViewQuery buttonSize="xs">
<CopyButtonViewQuery buttonSize="xsmall">
<i className="fa fa-clipboard" />
</CopyButtonViewQuery>
}

View File

@ -167,8 +167,8 @@ export default function PropertiesModal({
<>
<Button
data-test="properties-modal-cancel-button"
type="button"
buttonSize="sm"
htmlType="button"
buttonSize="small"
onClick={onHide}
cta
>
@ -176,8 +176,8 @@ export default function PropertiesModal({
</Button>
<Button
data-test="properties-modal-save-button"
type="button"
buttonSize="sm"
htmlType="button"
buttonSize="small"
buttonStyle="primary"
// @ts-ignore
onClick={onSubmit}

View File

@ -18,8 +18,8 @@
*/
import React from 'react';
import PropTypes from 'prop-types';
import { ButtonGroup } from 'react-bootstrap';
import { t, styled } from '@superset-ui/core';
import ButtonGroup from 'src/components/ButtonGroup';
import { t, useTheme } from '@superset-ui/core';
import { Tooltip } from 'src/common/components/Tooltip';
import Button from 'src/components/Button';
@ -39,20 +39,6 @@ const defaultProps = {
onSave: () => {},
};
const Styles = styled.div`
display: flex;
flex-shrink: 0;
flex-direction: row;
align-items: center;
padding: ${({ theme }) => 2 * theme.gridUnit}px
${({ theme }) => 2 * theme.gridUnit}px 0
${({ theme }) => 4 * theme.gridUnit}px;
.btn {
/* just to make sure buttons don't jiggle */
width: 100px;
}
`;
export default function QueryAndSaveBtns({
canAdd,
onQuery,
@ -91,37 +77,51 @@ export default function QueryAndSaveBtns({
</Button>
);
const theme = useTheme();
return (
<Styles>
<div>
<ButtonGroup className="query-and-save">
{qryOrStopButton}
<Button
buttonStyle="tertiary"
buttonSize="small"
data-target="#save_modal"
data-toggle="modal"
disabled={saveButtonDisabled}
onClick={onSave}
data-test="query-save-button"
<div
css={{
display: 'flex',
flexShrink: 0,
flexDirection: 'row',
alignItems: 'center',
paddingTop: theme.gridUnit * 2,
paddingRight: theme.gridUnit * 2,
paddingBottom: 0,
paddingLeft: theme.gridUnit * 4,
'& button': {
width: 100,
},
}}
>
<ButtonGroup className="query-and-save">
{qryOrStopButton}
<Button
buttonStyle="tertiary"
buttonSize="small"
data-target="#save_modal"
data-toggle="modal"
disabled={saveButtonDisabled}
onClick={onSave}
data-test="query-save-button"
>
<i className="fa fa-plus-circle" /> {t('Save')}
</Button>
</ButtonGroup>
{errorMessage && (
<span>
{' '}
<Tooltip
id="query-error-tooltip"
placement="right"
title={errorMessage}
>
<i className="fa fa-plus-circle" /> {t('Save')}
</Button>
</ButtonGroup>
{errorMessage && (
<span>
{' '}
<Tooltip
id="query-error-tooltip"
placement="right"
title={errorMessage}
>
<i className="fa fa-exclamation-circle text-danger fa-lg" />
</Tooltip>
</span>
)}
</div>
</Styles>
<i className="fa fa-exclamation-circle text-danger fa-lg" />
</Tooltip>
</span>
)}
</div>
);
}

View File

@ -170,12 +170,16 @@ class SaveModal extends React.Component<SaveModalProps, SaveModalState> {
title={t('Save chart')}
footer={
<div data-test="save-modal-footer">
<Button id="btn_cancel" buttonSize="sm" onClick={this.props.onHide}>
<Button
id="btn_cancel"
buttonSize="small"
onClick={this.props.onHide}
>
{t('Cancel')}
</Button>
<Button
id="btn_modal_save_goto_dash"
buttonSize="sm"
buttonSize="small"
disabled={
!this.state.newSliceName ||
(!this.state.saveToDashboardId && !this.state.newDashboardName)
@ -186,7 +190,7 @@ class SaveModal extends React.Component<SaveModalProps, SaveModalState> {
</Button>
<Button
id="btn_modal_save"
buttonSize="sm"
buttonSize="small"
buttonStyle="primary"
onClick={() => this.saveOrOverwrite(false)}
disabled={!this.state.newSliceName}

View File

@ -745,17 +745,17 @@ export default class AnnotationLayer extends React.PureComponent {
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
{isNew ? (
<Button buttonSize="sm" onClick={() => this.props.close()}>
<Button buttonSize="small" onClick={() => this.props.close()}>
{t('Cancel')}
</Button>
) : (
<Button buttonSize="sm" onClick={this.deleteAnnotation}>
<Button buttonSize="small" onClick={this.deleteAnnotation}>
{t('Remove')}
</Button>
)}
<div>
<Button
buttonSize="sm"
buttonSize="small"
disabled={!isValid}
onClick={this.applyAnnotation}
>
@ -763,7 +763,7 @@ export default class AnnotationLayer extends React.PureComponent {
</Button>
<Button
buttonSize="sm"
buttonSize="small"
buttonStyle="primary"
disabled={!isValid}
onClick={this.submitAnnotation}

View File

@ -302,10 +302,3 @@ export const CardStyles = styled.div`
text-decoration: none;
}
`;
export const IconContainer = styled.div`
svg {
vertical-align: -7px;
color: ${({ theme }) => theme.colors.primary.dark1};
}
`;

View File

@ -27,13 +27,12 @@ import withToasts from 'src/messageToasts/enhancers/withToasts';
import { useHistory } from 'react-router-dom';
import PropertiesModal from 'src/explore/components/PropertiesModal';
import { User } from 'src/types/bootstrapTypes';
import Icon from 'src/components/Icon';
import ChartCard from 'src/views/CRUD/chart/ChartCard';
import Chart from 'src/types/Chart';
import ErrorBoundary from 'src/components/ErrorBoundary';
import SubMenu from 'src/components/Menu/SubMenu';
import EmptyState from './EmptyState';
import { CardContainer, IconContainer } from '../utils';
import { CardContainer } from '../utils';
const PAGE_SIZE = 3;
@ -143,10 +142,10 @@ function ChartTable({
buttons={[
{
name: (
<IconContainer>
<Icon name="plus-small" />
<div>
<i className="fa fa-plus" />
{t('Chart')}
</IconContainer>
</div>
),
buttonStyle: 'tertiary',
onClick: () => {

View File

@ -25,9 +25,8 @@ import withToasts from 'src/messageToasts/enhancers/withToasts';
import PropertiesModal from 'src/dashboard/components/PropertiesModal';
import DashboardCard from 'src/views/CRUD/dashboard/DashboardCard';
import SubMenu from 'src/components/Menu/SubMenu';
import Icon from 'src/components/Icon';
import EmptyState from './EmptyState';
import { createErrorHandler, CardContainer, IconContainer } from '../utils';
import { createErrorHandler, CardContainer } from '../utils';
const PAGE_SIZE = 3;
@ -149,9 +148,9 @@ function DashboardTable({
buttons={[
{
name: (
<IconContainer>
<Icon name="plus-small" /> Dashboard{' '}
</IconContainer>
<div>
<i className="fa fa-plus" /> Dashboard{' '}
</div>
),
buttonStyle: 'tertiary',
onClick: () => {

View File

@ -20,8 +20,6 @@ import React from 'react';
import Button from 'src/components/Button';
import { Empty } from 'src/common/components';
import { t, styled } from '@superset-ui/core';
import Icon from 'src/components/Icon';
import { IconContainer } from '../utils';
interface EmptyStateProps {
tableName: string;
@ -108,16 +106,14 @@ export default function EmptyState({ tableName, tab }: EmptyStateProps) {
window.location = mineRedirects[tableName];
}}
>
<IconContainer>
<Icon name="plus-small" />{' '}
{tableName === 'SAVED_QUERIES'
? t('SQL query')
: t(`${tableName
.split('')
.slice(0, tableName.length - 1)
.join('')}
<i className="fa fa-plus" />
{tableName === 'SAVED_QUERIES'
? t('SQL query')
: t(`${tableName
.split('')
.slice(0, tableName.length - 1)
.join('')}
`)}
</IconContainer>
</Button>
</ButtonContainer>
)}

View File

@ -29,12 +29,7 @@ import DeleteModal from 'src/components/DeleteModal';
import Icon from 'src/components/Icon';
import SubMenu from 'src/components/Menu/SubMenu';
import EmptyState from './EmptyState';
import {
IconContainer,
CardContainer,
createErrorHandler,
shortenSQL,
} from '../utils';
import { CardContainer, createErrorHandler, shortenSQL } from '../utils';
SyntaxHighlighter.registerLanguage('sql', sql);
@ -272,9 +267,10 @@ const SavedQueries = ({
buttons={[
{
name: (
<IconContainer>
<Icon name="plus-small" /> SQL Query{' '}
</IconContainer>
<div>
<i className="fa fa-plus" />
SQL Query{' '}
</div>
),
buttonStyle: 'tertiary',
onClick: () => {