diff --git a/superset-frontend/spec/javascripts/addSlice/AddSliceContainer_spec.tsx b/superset-frontend/spec/javascripts/addSlice/AddSliceContainer_spec.tsx index 4af7fec71e..c3b8bac8a4 100644 --- a/superset-frontend/spec/javascripts/addSlice/AddSliceContainer_spec.tsx +++ b/superset-frontend/spec/javascripts/addSlice/AddSliceContainer_spec.tsx @@ -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(); + wrapper = mount() 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', () => { diff --git a/superset-frontend/spec/javascripts/dashboard/components/nativeFilters/FilterBar_spec.tsx b/superset-frontend/spec/javascripts/dashboard/components/nativeFilters/FilterBar_spec.tsx index bdd6dd92d4..7e6e9b04a5 100644 --- a/superset-frontend/spec/javascripts/dashboard/components/nativeFilters/FilterBar_spec.tsx +++ b/superset-frontend/spec/javascripts/dashboard/components/nativeFilters/FilterBar_spec.tsx @@ -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'); }); }); diff --git a/superset-frontend/spec/javascripts/explore/components/QueryAndSaveBtns_spec.jsx b/superset-frontend/spec/javascripts/explore/components/QueryAndSaveBtns_spec.jsx index 6bca5fb55d..12e1a95e8e 100644 --- a/superset-frontend/spec/javascripts/explore/components/QueryAndSaveBtns_spec.jsx +++ b/superset-frontend/spec/javascripts/explore/components/QueryAndSaveBtns_spec.jsx @@ -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(); - }); + const wrapper = mount(); 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); }); diff --git a/superset-frontend/src/CRUD/CollectionTable.tsx b/superset-frontend/src/CRUD/CollectionTable.tsx index 8166034d4f..ea46856df7 100644 --- a/superset-frontend/src/CRUD/CollectionTable.tsx +++ b/superset-frontend/src/CRUD/CollectionTable.tsx @@ -308,7 +308,7 @@ export default class CRUDCollection extends React.PureComponent< {this.props.allowAddItem && ( + - ))} - - ))} - -); - -export const InteractiveButton = args => { - const { label, ...btnArgs } = args; - return ; -}; - -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' } }; diff --git a/superset-frontend/src/components/Button/Button.stories.tsx b/superset-frontend/src/components/Button/Button.stories.tsx new file mode 100644 index 0000000000..b5a683b36f --- /dev/null +++ b/superset-frontend/src/components/Button/Button.stories.tsx @@ -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; +type ButtonStyleValue = ButtonStyle[keyof ButtonStyle]; +type ButtonSize = Pick; +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 => ( +
+

{size}

+ {Object.values(STYLES.options).map(style => ( + + ))} +
+ ))} + +); + +ButtonGallery.story = { + parameters: { + actions: { + disabled: true, + }, + controls: { + disabled: true, + }, + knobs: { + disabled: true, + }, + }, +}; + +export const InteractiveButton = (args: ButtonProps & { label: string }) => { + const { label, ...btnArgs } = args; + return ; +}; + +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' }, +}; diff --git a/superset-frontend/src/components/Button/Button.test.tsx b/superset-frontend/src/components/Button/Button.test.tsx index ab0f368fd9..3c2f895480 100644 --- a/superset-frontend/src/components/Button/Button.test.tsx +++ b/superset-frontend/src/components/Button/Button.test.tsx @@ -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( + + + + + + + + + + + +); +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, + }, + }, +}; diff --git a/superset-frontend/src/components/ButtonGroup/ButtonGroup.test.tsx b/superset-frontend/src/components/ButtonGroup/ButtonGroup.test.tsx new file mode 100644 index 0000000000..e305eba122 --- /dev/null +++ b/superset-frontend/src/components/ButtonGroup/ButtonGroup.test.tsx @@ -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( + + + , + ), + ).toBe(true); + }); + + it('renders 3 buttons', () => { + wrapper = mount( + + + + + , + ); + + expect(wrapper.find(Button).length).toEqual(3); + }); +}); diff --git a/superset-frontend/src/components/ButtonGroup/index.tsx b/superset-frontend/src/components/ButtonGroup/index.tsx new file mode 100644 index 0000000000..a812207d02 --- /dev/null +++ b/superset-frontend/src/components/ButtonGroup/index.tsx @@ -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 ( +
+ {children} +
+ ); +} diff --git a/superset-frontend/src/components/ImportModal/ImportModal.test.tsx b/superset-frontend/src/components/ImportModal/ImportModal.test.tsx index a7a1cf5e2f..8594005937 100644 --- a/superset-frontend/src/components/ImportModal/ImportModal.test.tsx +++ b/superset-frontend/src/components/ImportModal/ImportModal.test.tsx @@ -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', () => { diff --git a/superset-frontend/src/dashboard/components/Header.jsx b/superset-frontend/src/dashboard/components/Header.jsx index e155413bad..d15232c563 100644 --- a/superset-frontend/src/dashboard/components/Header.jsx +++ b/superset-frontend/src/dashboard/components/Header.jsx @@ -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, diff --git a/superset-frontend/src/dashboard/components/PropertiesModal.jsx b/superset-frontend/src/dashboard/components/PropertiesModal.jsx index f45f483d6d..411eda0fae 100644 --- a/superset-frontend/src/dashboard/components/PropertiesModal.jsx +++ b/superset-frontend/src/dashboard/components/PropertiesModal.jsx @@ -296,8 +296,8 @@ class PropertiesModal extends React.PureComponent { footer={ <> - diff --git a/superset-frontend/src/dashboard/components/filterscope/FilterScopeSelector.jsx b/superset-frontend/src/dashboard/components/filterscope/FilterScopeSelector.jsx index 6cfcc0eee0..882931983e 100644 --- a/superset-frontend/src/dashboard/components/filterscope/FilterScopeSelector.jsx +++ b/superset-frontend/src/dashboard/components/filterscope/FilterScopeSelector.jsx @@ -531,11 +531,15 @@ export default class FilterScopeSelector extends React.PureComponent { - {showSelector && ( - )} diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar.tsx index 6922070934..617928ce8c 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar.tsx @@ -497,15 +497,15 @@ const FilterBar: React.FC = ({ ); + const theme = useTheme(); + return ( - -
- - {qryOrStopButton} - + + {errorMessage && ( + + {' '} + - {t('Save')} - - - {errorMessage && ( - - {' '} - - - - - )} -
-
+ + +
+ )} + ); } diff --git a/superset-frontend/src/explore/components/SaveModal.tsx b/superset-frontend/src/explore/components/SaveModal.tsx index 46d0d5a10a..767099a6f9 100644 --- a/superset-frontend/src/explore/components/SaveModal.tsx +++ b/superset-frontend/src/explore/components/SaveModal.tsx @@ -170,12 +170,16 @@ class SaveModal extends React.Component { title={t('Save chart')} footer={
-
{isNew ? ( - ) : ( - )}
)} diff --git a/superset-frontend/src/views/CRUD/welcome/SavedQueries.tsx b/superset-frontend/src/views/CRUD/welcome/SavedQueries.tsx index 2eef7d4f24..f86b0b82c3 100644 --- a/superset-frontend/src/views/CRUD/welcome/SavedQueries.tsx +++ b/superset-frontend/src/views/CRUD/welcome/SavedQueries.tsx @@ -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: ( - - SQL Query{' '} - +
+ + SQL Query{' '} +
), buttonStyle: 'tertiary', onClick: () => {