diff --git a/superset-frontend/babel.config.js b/superset-frontend/babel.config.js index 51ae332c2..5aa3420cf 100644 --- a/superset-frontend/babel.config.js +++ b/superset-frontend/babel.config.js @@ -63,6 +63,7 @@ module.exports = { targets: { node: 'current' }, }, ], + ['@emotion/babel-preset-css-prop'], ], plugins: ['babel-plugin-dynamic-import-node'], }, diff --git a/superset-frontend/cypress-base/cypress/integration/explore/annotations.test.ts b/superset-frontend/cypress-base/cypress/integration/explore/annotations.test.ts index ebbaddf72..a771ea5d2 100644 --- a/superset-frontend/cypress-base/cypress/integration/explore/annotations.test.ts +++ b/superset-frontend/cypress-base/cypress/integration/explore/annotations.test.ts @@ -29,7 +29,7 @@ describe('Annotations', () => { const layerLabel = 'Goal line'; - cy.get('[data-test=annotation_layers] button').click(); + cy.get('[data-test=annotation_layers]').click(); cy.get('[data-test="popover-content"]').within(() => { cy.get('[data-test=annotation-layer-name-header]') diff --git a/superset-frontend/spec/javascripts/dashboard/components/Header_spec.jsx b/superset-frontend/spec/javascripts/dashboard/components/Header_spec.jsx index da0467b03..e5a7f52e7 100644 --- a/superset-frontend/spec/javascripts/dashboard/components/Header_spec.jsx +++ b/superset-frontend/spec/javascripts/dashboard/components/Header_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 Header from 'src/dashboard/components/Header'; import EditableTitle from 'src/components/EditableTitle'; import FaveStar from 'src/components/FaveStar'; @@ -83,7 +83,7 @@ describe('Header', () => { }; function setup(overrideProps) { - const wrapper = shallow(
); + const wrapper = mount(
); return wrapper; } diff --git a/superset-frontend/spec/javascripts/sqllab/ShareSqlLabQuery_spec.jsx b/superset-frontend/spec/javascripts/sqllab/ShareSqlLabQuery_spec.jsx index cb32ab182..8a68d386a 100644 --- a/superset-frontend/spec/javascripts/sqllab/ShareSqlLabQuery_spec.jsx +++ b/superset-frontend/spec/javascripts/sqllab/ShareSqlLabQuery_spec.jsx @@ -121,14 +121,12 @@ describe('ShareSqlLabQuery', () => { remoteId: undefined, }, }; - await act(async () => { - render(, { - wrapper: standardProvider, - }); + + render(, { + wrapper: standardProvider, }); - const button = screen.getByRole('button', { name: /copy link/i }); - const style = window.getComputedStyle(button); - expect(style.color).toBe('rgb(102, 102, 102)'); + const button = await screen.findByRole('button', { name: /copy link/i }); + expect(button).toBeDisabled(); }); }); }); diff --git a/superset-frontend/src/SqlLab/components/ShareSqlLabQuery.tsx b/superset-frontend/src/SqlLab/components/ShareSqlLabQuery.tsx index 66c709c5d..10e3e43c3 100644 --- a/superset-frontend/src/SqlLab/components/ShareSqlLabQuery.tsx +++ b/superset-frontend/src/SqlLab/components/ShareSqlLabQuery.tsx @@ -17,9 +17,7 @@ * under the License. */ import React from 'react'; -import { Tooltip } from 'src/common/components/Tooltip'; -import { t, styled, supersetTheme } from '@superset-ui/core'; -import cx from 'classnames'; +import { t, useTheme } from '@superset-ui/core'; import Button from 'src/components/Button'; import withToasts from 'src/messageToasts/enhancers/withToasts'; @@ -41,24 +39,12 @@ interface ShareSqlLabQueryPropTypes { addDangerToast: (msg: string) => void; } -const Styles = styled.div` - .btn-disabled { - &, - &:hover { - cursor: default; - background-color: ${supersetTheme.colors.grayscale.light2}; - color: ${supersetTheme.colors.grayscale.base}; - } - } - svg { - vertical-align: -${supersetTheme.gridUnit * 1.25}px; - } -`; - function ShareSqlLabQuery({ queryEditor, addDangerToast, }: ShareSqlLabQueryPropTypes) { + const theme = useTheme(); + const getCopyUrlForKvStore = (callback: Function) => { const { dbId, title, schema, autorun, sql } = queryEditor; const sharedQuery = { dbId, title, schema, autorun, sql }; @@ -94,58 +80,41 @@ function ShareSqlLabQuery({ return getCopyUrlForSavedQuery(callback); }; - const buildButton = () => { - const canShare = - queryEditor.remoteId || - isFeatureEnabled(FeatureFlag.SHARE_QUERIES_VIA_KV_STORE); + const buildButton = (canShare: boolean) => { + const tooltip = canShare + ? t('Copy query link to your clipboard') + : t('Save the query to enable this feature'); return ( - - - + ); }; const canShare = - queryEditor.remoteId || + !!queryEditor.remoteId || isFeatureEnabled(FeatureFlag.SHARE_QUERIES_VIA_KV_STORE); return ( - + <> {canShare ? ( ) : ( - buildButton() + buildButton(canShare) )} - + ); } diff --git a/superset-frontend/src/common/components/index.tsx b/superset-frontend/src/common/components/index.tsx index 4d52d3ead..9c336110f 100644 --- a/superset-frontend/src/common/components/index.tsx +++ b/superset-frontend/src/common/components/index.tsx @@ -57,6 +57,7 @@ export { RadioChangeEvent } from 'antd/lib/radio'; export { TreeProps } from 'antd/lib/tree'; export { default as Alert, AlertProps } from 'antd/lib/alert'; export { default as Select, SelectProps } from 'antd/lib/select'; +export { default as List, ListItemProps } from 'antd/lib/list'; export { default as Collapse } from './Collapse'; export { default as Badge } from 'src/components/Badge'; diff --git a/superset-frontend/src/explore/components/controls/AnnotationLayerControl/index.jsx b/superset-frontend/src/explore/components/controls/AnnotationLayerControl/index.jsx index eae832c4e..e5965ee7c 100644 --- a/superset-frontend/src/explore/components/controls/AnnotationLayerControl/index.jsx +++ b/superset-frontend/src/explore/components/controls/AnnotationLayerControl/index.jsx @@ -18,7 +18,7 @@ */ import React from 'react'; import PropTypes from 'prop-types'; -import { ListGroup, ListGroupItem } from 'react-bootstrap'; +import { List } from 'src/common/components'; import { connect } from 'react-redux'; import { t, withTheme } from '@superset-ui/core'; import { InfoTooltipWithTrigger } from '@superset-ui/chart-controls'; @@ -26,6 +26,7 @@ import Popover from 'src/components/Popover'; import AsyncEsmComponent from 'src/components/AsyncEsmComponent'; import { getChartKey } from 'src/explore/exploreUtils'; import { runAnnotationQuery } from 'src/chart/chartAction'; +import CustomListItem from 'src/explore/components/controls/CustomListItem'; const AnnotationLayer = AsyncEsmComponent( () => import('./AnnotationLayer'), @@ -164,6 +165,12 @@ class AnnotationLayerControl extends React.PureComponent { trigger="click" placement="right" title={t('Edit annotation layer')} + css={theme => ({ + '&:hover': { + cursor: 'pointer', + backgroundColor: theme.colors.grayscale.light4, + }, + })} content={this.renderPopover( i, anno, @@ -172,17 +179,17 @@ class AnnotationLayerControl extends React.PureComponent { visible={this.state.popoverVisible[i]} onVisibleChange={visible => this.handleVisibleChange(visible, i)} > - + {anno.name} {this.renderInfo(anno)} - + )); const addLayerPopoverKey = 'add'; return (
- + ({ borderRadius: theme.gridUnit })}> {annotations} - + {' '}   {t('Add annotation layer')} - + - +
); } diff --git a/superset-frontend/src/explore/components/controls/CollectionControl/CollectionControl.less b/superset-frontend/src/explore/components/controls/CollectionControl/CollectionControl.less deleted file mode 100644 index 5d23e4e3c..000000000 --- a/superset-frontend/src/explore/components/controls/CollectionControl/CollectionControl.less +++ /dev/null @@ -1,21 +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. - */ -.CollectionControl .list-group-item i.fa { - padding-top: 5px; -} diff --git a/superset-frontend/src/explore/components/controls/CollectionControl/CollectionControl.test.tsx b/superset-frontend/src/explore/components/controls/CollectionControl/CollectionControl.test.tsx index f2c049030..53d57030c 100644 --- a/superset-frontend/src/explore/components/controls/CollectionControl/CollectionControl.test.tsx +++ b/superset-frontend/src/explore/components/controls/CollectionControl/CollectionControl.test.tsx @@ -74,7 +74,7 @@ const createProps = () => ({ description: null, hovered: false, itemGenerator: jest.fn(), - keyAccessor: jest.fn(), + keyAccessor: jest.fn(() => 'hrYAZ5iBH'), label: 'Time series columns', name: 'column_collection', onChange: jest.fn(), @@ -105,7 +105,7 @@ test('Should have add button', () => { render(); expect(props.onChange).toBeCalledTimes(0); - userEvent.click(screen.getByRole('button', { name: 'add-item' })); + userEvent.click(screen.getByRole('button', { name: 'plus-large' })); expect(props.onChange).toBeCalledWith([{ key: 'hrYAZ5iBH' }, undefined]); }); @@ -121,7 +121,7 @@ test('Should have remove button', () => { test('Should have SortableDragger icon', () => { const props = createProps(); render(); - expect(screen.getByRole('img')).toBeVisible(); + expect(screen.getByLabelText('drag')).toBeVisible(); }); test('Should call Control component', () => { diff --git a/superset-frontend/src/explore/components/controls/CollectionControl/index.jsx b/superset-frontend/src/explore/components/controls/CollectionControl/index.jsx index 7f49d71b1..c6e712840 100644 --- a/superset-frontend/src/explore/components/controls/CollectionControl/index.jsx +++ b/superset-frontend/src/explore/components/controls/CollectionControl/index.jsx @@ -18,19 +18,24 @@ */ import React from 'react'; import PropTypes from 'prop-types'; -import { ListGroup, ListGroupItem } from 'react-bootstrap'; +import { List } from 'src/common/components'; import shortid from 'shortid'; +import { t, withTheme } from '@superset-ui/core'; import { SortableContainer, SortableHandle, SortableElement, arrayMove, } from 'react-sortable-hoc'; - +import Icon from 'src/components/Icon'; +import { + HeaderContainer, + AddIconButton, +} from 'src/explore/components/controls/OptionControls'; import { InfoTooltipWithTrigger } from '@superset-ui/chart-controls'; import ControlHeader from 'src/explore/components/ControlHeader'; +import CustomListItem from 'src/explore/components/controls/CustomListItem'; import controlMap from '..'; -import './CollectionControl.less'; const propTypes = { name: PropTypes.string.isRequired, @@ -51,23 +56,24 @@ const defaultProps = { label: null, description: null, onChange: () => {}, - placeholder: 'Empty collection', + placeholder: t('Empty collection'), itemGenerator: () => ({ key: shortid.generate() }), keyAccessor: o => o.key, value: [], - addTooltip: 'Add an item', + addTooltip: t('Add an item'), }; -const SortableListGroupItem = SortableElement(ListGroupItem); -const SortableListGroup = SortableContainer(ListGroup); +const SortableListItem = SortableElement(CustomListItem); +const SortableList = SortableContainer(List); const SortableDragger = SortableHandle(() => ( )); -export default class CollectionControl extends React.Component { +class CollectionControl extends React.Component { constructor(props) { super(props); this.onAdd = this.onAdd.bind(this); @@ -96,59 +102,69 @@ export default class CollectionControl extends React.Component { } const Control = controlMap[this.props.controlName]; return ( - ({ + borderRadius: theme.gridUnit, + })} > {this.props.value.map((o, i) => { // label relevant only for header, not here const { label, ...commonProps } = this.props; return ( - -
- -
-
+ +
({ + flex: 1, + marginLeft: theme.gridUnit * 2, + marginRight: theme.gridUnit * 2, + })} + >
-
- -
- + + ); })} - + ); } render() { + const { theme } = this.props; return (
- + + + + + + {this.renderList()} -
); } @@ -156,3 +172,5 @@ export default class CollectionControl extends React.Component { CollectionControl.propTypes = propTypes; CollectionControl.defaultProps = defaultProps; + +export default withTheme(CollectionControl); diff --git a/superset-frontend/src/explore/components/controls/CustomListItem/index.tsx b/superset-frontend/src/explore/components/controls/CustomListItem/index.tsx new file mode 100644 index 000000000..e83552c9d --- /dev/null +++ b/superset-frontend/src/explore/components/controls/CustomListItem/index.tsx @@ -0,0 +1,56 @@ +/** + * 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 { useTheme } from '@superset-ui/core'; +import { List, ListItemProps } from 'src/common/components'; + +export interface CustomListItemProps extends ListItemProps { + selectable: boolean; +} + +export default function CustomListItem(props: CustomListItemProps) { + const { selectable, children, ...rest } = props; + const theme = useTheme(); + const css = { + '&.ant-list-item': { + padding: `${theme.gridUnit + 2}px ${theme.gridUnit * 3}px`, + ':first-of-type': { + borderTopLeftRadius: theme.gridUnit, + borderTopRightRadius: theme.gridUnit, + }, + ':last-of-type': { + borderBottomLeftRadius: theme.gridUnit, + borderBottomRightRadius: theme.gridUnit, + }, + }, + }; + + if (selectable) { + css['&:hover'] = { + cursor: 'pointer', + backgroundColor: theme.colors.grayscale.light4, + }; + } + + return ( + + {children} + + ); +}