fix: Explore popovers issues (#11428)

* Fix spaces and comas not working in filter popover

* Fix popup not opening automatically

* Add e2e test

* Remove only from test

* Remove redundant test, add checking label content

* Add comments to e2e test

* Fix unit test

* Use destructuring

* Always open popup for functions and saved metrics, too

* Fix popover for adhoc metrics too

* Small refactor to consistency

* Refactor for consistency

* Remove redundant functions

* Test fix

Co-authored-by: Jesse Yang <jesse.yang@airbnb.com>
This commit is contained in:
Kamil Gabryjelski 2020-10-28 17:38:52 +01:00 committed by GitHub
parent 88e5e9855d
commit b2636f01bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 120 additions and 112 deletions

View File

@ -50,13 +50,17 @@ describe('AdhocFilters', () => {
}); });
it('Set simple adhoc filter', () => { it('Set simple adhoc filter', () => {
cy.get('#filter-edit-popover').within(() => { cy.get('[data-test=adhoc-filter-simple-value] .Select__control').click();
cy.get('[data-test=adhoc-filter-simple-value]').within(() => { cy.get('[data-test=adhoc-filter-simple-value] input[type=text]')
cy.get('.Select__control').click(); .focus()
cy.get('input[type=text]').focus().type('Any{enter}'); .type('Jack{enter}', { delay: 20 });
});
cy.get('button').contains('Save').click(); cy.get('[data-test="adhoc-filter-edit-popover-save-button"]').click();
});
cy.get(
'[data-test=adhoc_filters] .Select__control span.option-label',
).contains('name = Jack');
cy.get('button[data-test="run-query-button"]').click(); cy.get('button[data-test="run-query-button"]').click();
cy.verifySliceSuccess({ cy.verifySliceSuccess({
waitAlias: '@postJson', waitAlias: '@postJson',
@ -65,26 +69,35 @@ describe('AdhocFilters', () => {
}); });
it('Set custom adhoc filter', () => { it('Set custom adhoc filter', () => {
cy.visitChartByName('Num Births Trend'); const filterType = 'name';
cy.verifySliceSuccess({ waitAlias: '@postJson' }); const filterContent = "'Amy' OR name = 'Donald'";
cy.get('[data-test=adhoc_filters] .Select__control') cy.get('[data-test=adhoc_filters] .Select__control')
.scrollIntoView() .scrollIntoView()
.click(); .click();
// remove previous input
cy.get('[data-test=adhoc_filters] input[type=text]') cy.get('[data-test=adhoc_filters] input[type=text]')
.focus() .focus()
.type('name{enter}', { delay: 20 }); .type('{backspace}');
cy.get('[data-test="adhoc_filters"]').within(() => {
cy.contains('name = ').should('be.visible').click(); cy.get('[data-test=adhoc_filters] input[type=text]')
}); .focus()
.type(`${filterType}{enter}`);
cy.wait('@filterValues'); cy.wait('@filterValues');
// selecting a new filter should auto-open the popup,
// so the tabshould be visible by now
cy.get('#filter-edit-popover #adhoc-filter-edit-tabs-tab-SQL').click(); cy.get('#filter-edit-popover #adhoc-filter-edit-tabs-tab-SQL').click();
cy.get('#filter-edit-popover .ace_content').click(); cy.get('#filter-edit-popover .ace_content').click();
cy.get('#filter-edit-popover .ace_text-input').type( cy.get('#filter-edit-popover .ace_text-input').type(filterContent);
"'Amy' OR name = 'Bob'", cy.get('[data-test="adhoc-filter-edit-popover-save-button"]').click();
);
cy.get('#filter-edit-popover button').contains('Save').click(); // check if the filter was saved correctly
cy.get(
'[data-test=adhoc_filters] .Select__control span.option-label',
).contains(`${filterType} = ${filterContent}`);
cy.get('button[data-test="run-query-button"]').click(); cy.get('button[data-test="run-query-button"]').click();
cy.verifySliceSuccess({ cy.verifySliceSuccess({

View File

@ -42,7 +42,6 @@ describe('AdhocMetrics', () => {
.trigger('mousedown') .trigger('mousedown')
.click(); .click();
cy.get('[data-test="option-label"]').first().click();
cy.get('[data-test="AdhocMetricEditTitle#trigger"]').click(); cy.get('[data-test="AdhocMetricEditTitle#trigger"]').click();
cy.get('[data-test="AdhocMetricEditTitle#input"]').type(metricName); cy.get('[data-test="AdhocMetricEditTitle#input"]').type(metricName);
cy.get('[data-test="AdhocMetricEdit#save"]').contains('Save').click(); cy.get('[data-test="AdhocMetricEdit#save"]').contains('Save').click();

View File

@ -19,7 +19,7 @@
/* eslint-disable no-unused-expressions */ /* eslint-disable no-unused-expressions */
import React from 'react'; import React from 'react';
import sinon from 'sinon'; import sinon from 'sinon';
import { styledShallow as shallow } from 'spec/helpers/theming'; import { shallow } from 'enzyme';
import Popover from 'src/common/components/Popover'; import Popover from 'src/common/components/Popover';
import Label from 'src/components/Label'; import Label from 'src/components/Label';
@ -46,7 +46,7 @@ function setup(overrides) {
datasource: {}, datasource: {},
...overrides, ...overrides,
}; };
const wrapper = shallow(<AdhocFilterOption {...props} />).dive(); const wrapper = shallow(<AdhocFilterOption {...props} />);
return { wrapper }; return { wrapper };
} }

View File

@ -19,7 +19,7 @@
/* eslint-disable no-unused-expressions */ /* eslint-disable no-unused-expressions */
import React from 'react'; import React from 'react';
import sinon from 'sinon'; import sinon from 'sinon';
import { styledShallow as shallow } from 'spec/helpers/theming'; import { shallow } from 'enzyme';
import Popover from 'src/common/components/Popover'; import Popover from 'src/common/components/Popover';
import Label from 'src/components/Label'; import Label from 'src/components/Label';
@ -46,7 +46,7 @@ function setup(overrides) {
columns, columns,
...overrides, ...overrides,
}; };
const wrapper = shallow(<AdhocMetricOption {...props} />).dive(); const wrapper = shallow(<AdhocMetricOption {...props} />);
return { wrapper, onMetricEdit }; return { wrapper, onMetricEdit };
} }
@ -73,11 +73,13 @@ describe('AdhocMetricOption', () => {
it('returns to default labels when the custom label is cleared', () => { it('returns to default labels when the custom label is cleared', () => {
const { wrapper } = setup(); const { wrapper } = setup();
expect(wrapper.state('title').label).toBe('SUM(value)');
wrapper.instance().onLabelChange({ target: { value: 'new label' } }); wrapper.instance().onLabelChange({ target: { value: 'new label' } });
expect(wrapper.state('title').label).toBe('new label');
wrapper.instance().onLabelChange({ target: { value: '' } }); wrapper.instance().onLabelChange({ target: { value: '' } });
// close and open the popover
wrapper.instance().closeMetricEditOverlay();
wrapper.instance().onOverlayEntered();
expect(wrapper.state('title').label).toBe('SUM(value)'); expect(wrapper.state('title').label).toBe('SUM(value)');
expect(wrapper.state('title').hasCustomLabel).toBe(false); expect(wrapper.state('title').hasCustomLabel).toBe(false);
}); });

View File

@ -17,8 +17,7 @@
* under the License. * under the License.
*/ */
import { Popover as AntdPopover } from 'src/common/components'; import { Popover as AntdPopover } from 'src/common/components';
import { styled } from '@superset-ui/core';
const SupersetPopover = styled(AntdPopover)``; const SupersetPopover = AntdPopover;
export default SupersetPopover; export default SupersetPopover;

View File

@ -18,10 +18,10 @@
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Popover from 'src/common/components/Popover'; import { t } from '@superset-ui/core';
import { t, withTheme } from '@superset-ui/core';
import { InfoTooltipWithTrigger } from '@superset-ui/chart-controls'; import { InfoTooltipWithTrigger } from '@superset-ui/chart-controls';
import Popover from 'src/common/components/Popover';
import Label from 'src/components/Label'; import Label from 'src/components/Label';
import AdhocFilterEditPopover from './AdhocFilterEditPopover'; import AdhocFilterEditPopover from './AdhocFilterEditPopover';
import AdhocFilter from '../AdhocFilter'; import AdhocFilter from '../AdhocFilter';
@ -44,57 +44,63 @@ const propTypes = {
class AdhocFilterOption extends React.PureComponent { class AdhocFilterOption extends React.PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this.closeFilterEditOverlay = this.closeFilterEditOverlay.bind(this);
this.onPopoverResize = this.onPopoverResize.bind(this); this.onPopoverResize = this.onPopoverResize.bind(this);
this.onOverlayEntered = this.onOverlayEntered.bind(this); this.closePopover = this.closePopover.bind(this);
this.onOverlayExited = this.onOverlayExited.bind(this); this.togglePopover = this.togglePopover.bind(this);
this.handleVisibleChange = this.handleVisibleChange.bind(this); this.state = {
this.state = { overlayShown: false }; // automatically open the popover the the metric is new
popoverVisible: !!props.adhocFilter.isNew,
};
}
componentDidMount() {
const { adhocFilter } = this.props;
// isNew is used to auto-open the popup. Once popup is opened, it's not
// considered new anymore.
// put behind setTimeout so in case consequetive re-renderings are triggered
// for some reason, the popup can still show up.
setTimeout(() => {
adhocFilter.isNew = false;
});
} }
onPopoverResize() { onPopoverResize() {
this.forceUpdate(); this.forceUpdate();
} }
onOverlayEntered() { closePopover() {
// isNew is used to indicate whether to automatically open the overlay this.setState({ popoverVisible: false });
// once the overlay has been opened, the metric/filter will never be
// considered new again.
this.props.adhocFilter.isNew = false;
this.setState({ overlayShown: true });
} }
onOverlayExited() { togglePopover(visible) {
this.setState({ overlayShown: false }); this.setState(({ popoverVisible }) => {
} return {
popoverVisible: visible === undefined ? !popoverVisible : visible,
closeFilterEditOverlay() { };
this.setState({ overlayShown: false }); });
}
handleVisibleChange(visible) {
if (visible) {
this.onOverlayEntered();
} else {
this.onOverlayExited();
}
} }
render() { render() {
const { adhocFilter } = this.props; const { adhocFilter } = this.props;
const content = ( const overlayContent = (
<AdhocFilterEditPopover <AdhocFilterEditPopover
onResize={this.onPopoverResize}
adhocFilter={adhocFilter} adhocFilter={adhocFilter}
onChange={this.props.onFilterEdit}
onClose={this.closeFilterEditOverlay}
options={this.props.options} options={this.props.options}
datasource={this.props.datasource} datasource={this.props.datasource}
partitionColumn={this.props.partitionColumn} partitionColumn={this.props.partitionColumn}
onResize={this.onPopoverResize}
onClose={this.closePopover}
onChange={this.props.onFilterEdit}
/> />
); );
return ( return (
<div role="button" tabIndex={0} onMouseDown={e => e.stopPropagation()}> <div
role="button"
tabIndex={0}
onMouseDown={e => e.stopPropagation()}
onKeyDown={e => e.stopPropagation()}
>
{adhocFilter.isExtra && ( {adhocFilter.isExtra && (
<InfoTooltipWithTrigger <InfoTooltipWithTrigger
icon="exclamation-triangle" icon="exclamation-triangle"
@ -109,19 +115,14 @@ class AdhocFilterOption extends React.PureComponent {
<Popover <Popover
placement="right" placement="right"
trigger="click" trigger="click"
disabled content={overlayContent}
content={content}
defaultVisible={adhocFilter.isNew} defaultVisible={adhocFilter.isNew}
visible={this.state.overlayShown} visible={this.state.popoverVisible}
onVisibleChange={this.handleVisibleChange} onVisibleChange={this.togglePopover}
> >
<Label className="option-label adhoc-option adhoc-filter-option"> <Label className="option-label adhoc-option adhoc-filter-option">
{adhocFilter.getDefaultLabel()} {adhocFilter.getDefaultLabel()}
<i <i className="fa fa-caret-right adhoc-label-arrow" />
className={`fa fa-caret-${
this.state.overlayShown ? 'left' : 'right'
} adhoc-label-arrow`}
/>
</Label> </Label>
</Popover> </Popover>
</div> </div>
@ -129,6 +130,6 @@ class AdhocFilterOption extends React.PureComponent {
} }
} }
export default withTheme(AdhocFilterOption); export default AdhocFilterOption;
AdhocFilterOption.propTypes = propTypes; AdhocFilterOption.propTypes = propTypes;

View File

@ -18,7 +18,6 @@
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { withTheme } from '@superset-ui/core';
import Popover from 'src/common/components/Popover'; import Popover from 'src/common/components/Popover';
import Label from 'src/components/Label'; import Label from 'src/components/Label';
@ -38,13 +37,12 @@ const propTypes = {
class AdhocMetricOption extends React.PureComponent { class AdhocMetricOption extends React.PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this.closeMetricEditOverlay = this.closeMetricEditOverlay.bind(this);
this.onOverlayEntered = this.onOverlayEntered.bind(this);
this.onPopoverResize = this.onPopoverResize.bind(this); this.onPopoverResize = this.onPopoverResize.bind(this);
this.handleVisibleChange = this.handleVisibleChange.bind(this);
this.onLabelChange = this.onLabelChange.bind(this); this.onLabelChange = this.onLabelChange.bind(this);
this.closePopover = this.closePopover.bind(this);
this.togglePopover = this.togglePopover.bind(this);
this.state = { this.state = {
overlayShown: false, popoverVisible: undefined,
title: { title: {
label: props.adhocMetric.label, label: props.adhocMetric.label,
hasCustomLabel: props.adhocMetric.hasCustomLabel, hasCustomLabel: props.adhocMetric.hasCustomLabel,
@ -52,12 +50,23 @@ class AdhocMetricOption extends React.PureComponent {
}; };
} }
componentDidMount() {
const { adhocMetric } = this.props;
// isNew is used to auto-open the popup. Once popup is opened, it's not
// considered new anymore.
// put behind setTimeout so in case consequetive re-renderings are triggered
// for some reason, the popup can still show up.
setTimeout(() => {
adhocMetric.isNew = false;
});
}
onLabelChange(e) { onLabelChange(e) {
const label = e.target.value; const label = e.target.value;
this.setState({ this.setState({
title: { title: {
label, label: label || this.props.adhocMetric.label,
hasCustomLabel: true, hasCustomLabel: !!label,
}, },
}); });
} }
@ -66,43 +75,30 @@ class AdhocMetricOption extends React.PureComponent {
this.forceUpdate(); this.forceUpdate();
} }
onOverlayEntered() { closePopover() {
// isNew is used to indicate whether to automatically open the overlay this.setState({ popoverVisible: false });
// once the overlay has been opened, the metric/filter will never be }
// considered new again.
this.props.adhocMetric.isNew = false; togglePopover(visible) {
this.setState({ this.setState(({ popoverVisible }) => {
overlayShown: true, return {
title: { popoverVisible: visible === undefined ? !popoverVisible : visible,
label: this.props.adhocMetric.label, };
hasCustomLabel: this.props.adhocMetric.hasCustomLabel,
},
}); });
} }
closeMetricEditOverlay() {
this.setState({ overlayShown: false });
}
handleVisibleChange(visible) {
if (visible) {
this.onOverlayEntered();
} else {
this.closeMetricEditOverlay();
}
}
render() { render() {
const { adhocMetric } = this.props; const { adhocMetric } = this.props;
const overlayContent = ( const overlayContent = (
<AdhocMetricEditPopover <AdhocMetricEditPopover
onResize={this.onPopoverResize}
adhocMetric={adhocMetric} adhocMetric={adhocMetric}
title={this.state.title} title={this.state.title}
onChange={this.props.onMetricEdit}
onClose={this.closeMetricEditOverlay}
columns={this.props.columns} columns={this.props.columns}
datasourceType={this.props.datasourceType} datasourceType={this.props.datasourceType}
onResize={this.onPopoverResize}
onClose={this.closePopover}
onChange={this.props.onMetricEdit}
/> />
); );
@ -129,17 +125,13 @@ class AdhocMetricOption extends React.PureComponent {
disabled disabled
content={overlayContent} content={overlayContent}
defaultVisible={adhocMetric.isNew} defaultVisible={adhocMetric.isNew}
onVisibleChange={this.handleVisibleChange} visible={this.state.popoverVisible}
visible={this.state.overlayShown} onVisibleChange={this.togglePopover}
title={popoverTitle} title={popoverTitle}
> >
<Label className="option-label adhoc-option" data-test="option-label"> <Label className="option-label adhoc-option" data-test="option-label">
{adhocMetric.label} {adhocMetric.label}
<i <i className="fa fa-caret-right adhoc-label-arrow" />
className={`fa fa-caret-${
this.state.overlayShown ? 'left' : 'right'
} adhoc-label-arrow`}
/>
</Label> </Label>
</Popover> </Popover>
</div> </div>
@ -147,6 +139,6 @@ class AdhocMetricOption extends React.PureComponent {
} }
} }
export default withTheme(AdhocMetricOption); export default AdhocMetricOption;
AdhocMetricOption.propTypes = propTypes; AdhocMetricOption.propTypes = propTypes;

View File

@ -180,9 +180,10 @@ export default class AdhocFilterControl extends React.Component {
operator: OPERATORS['>'], operator: OPERATORS['>'],
comparator: 0, comparator: 0,
clause: CLAUSES.HAVING, clause: CLAUSES.HAVING,
isNew: true,
}); });
} }
// has a custom label // has a custom label, meaning it's custom column
if (option.label) { if (option.label) {
return new AdhocFilter({ return new AdhocFilter({
expressionType: expressionType:
@ -196,6 +197,7 @@ export default class AdhocFilterControl extends React.Component {
operator: OPERATORS['>'], operator: OPERATORS['>'],
comparator: 0, comparator: 0,
clause: CLAUSES.HAVING, clause: CLAUSES.HAVING,
isNew: true,
}); });
} }
// add a new filter item // add a new filter item
@ -262,7 +264,7 @@ export default class AdhocFilterControl extends React.Component {
render() { render() {
return ( return (
<div className="metrics-select"> <div className="metrics-select" data-test="adhoc-filter-control">
<ControlHeader {...this.props} /> <ControlHeader {...this.props} />
<OnPasteSelect <OnPasteSelect
isMulti isMulti