[adhoc-filters] Adding adhoc-filters to all viz types (#5206)

This commit is contained in:
John Bodley
2018-06-18 15:43:18 -07:00
committed by GitHub
parent 1fc4ee0d3c
commit d483ed121c
28 changed files with 1012 additions and 980 deletions

View File

@@ -33,18 +33,9 @@ const columns = [
{ type: 'DOUBLE', column_name: 'value' },
];
const legacyFilter = { col: 'value', op: '>', val: '5' };
const legacyHavingFilter = { col: 'SUM(value)', op: '>', val: '10' };
const whereFilterText = 'target in (\'alpha\')';
const havingFilterText = 'SUM(value) < 20';
const formData = {
filters: [legacyFilter],
having: havingFilterText,
having_filters: [legacyHavingFilter],
metric: undefined,
metrics: [sumValueAdhocMetric, savedMetric.saved_metric_name],
where: whereFilterText,
};
function setup(overrides) {
@@ -68,49 +59,6 @@ describe('AdhocFilterControl', () => {
expect(wrapper.find(OnPasteSelect)).to.have.lengthOf(1);
});
it('will translate legacy filters into adhoc filters if no adhoc filters are present', () => {
const { wrapper } = setup({ value: undefined });
expect(wrapper.state('values')).to.have.lengthOf(4);
expect(wrapper.state('values')[0].equals((
new AdhocFilter({
expressionType: EXPRESSION_TYPES.SIMPLE,
subject: 'value',
operator: '>',
comparator: '5',
clause: CLAUSES.WHERE,
})
))).to.be.true;
expect(wrapper.state('values')[1].equals((
new AdhocFilter({
expressionType: EXPRESSION_TYPES.SIMPLE,
subject: 'SUM(value)',
operator: '>',
comparator: '10',
clause: CLAUSES.HAVING,
})
))).to.be.true;
expect(wrapper.state('values')[2].equals((
new AdhocFilter({
expressionType: EXPRESSION_TYPES.SQL,
sqlExpression: 'target in (\'alpha\')',
clause: CLAUSES.WHERE,
})
))).to.be.true;
expect(wrapper.state('values')[3].equals((
new AdhocFilter({
expressionType: EXPRESSION_TYPES.SQL,
sqlExpression: 'SUM(value) < 20',
clause: CLAUSES.HAVING,
})
))).to.be.true;
});
it('will ignore legacy filters if adhoc filters are present', () => {
const { wrapper } = setup();
expect(wrapper.state('values')).to.have.lengthOf(1);
expect(wrapper.state('values')[0]).to.equal(simpleAdhocFilter);
});
it('handles saved metrics being selected to filter on', () => {
const { wrapper, onChange } = setup({ value: [] });
const select = wrapper.find(OnPasteSelect);

View File

@@ -26,6 +26,6 @@ describe('ControlPanelsContainer', () => {
});
it('renders ControlPanelSections', () => {
expect(wrapper.find(ControlPanelSection)).to.have.lengthOf(7);
expect(wrapper.find(ControlPanelSection)).to.have.lengthOf(6);
});
});

View File

@@ -1,248 +0,0 @@
/* eslint-disable no-unused-expressions */
import React from 'react';
import { Button } from 'react-bootstrap';
import sinon from 'sinon';
import { expect } from 'chai';
import { describe, it, beforeEach } from 'mocha';
import { shallow } from 'enzyme';
import FilterControl from '../../../../src/explore/components/controls/FilterControl';
import Filter from '../../../../src/explore/components/controls/Filter';
const $ = window.$ = require('jquery');
const defaultProps = {
name: 'not_having_filters',
onChange: sinon.spy(),
value: [
{
col: 'col1',
op: 'in',
val: ['a', 'b', 'd'],
},
{
col: 'col2',
op: '==',
val: 'Z',
},
],
datasource: {
id: 1,
type: 'qtable',
filter_select: true,
filterable_cols: [['col1', 'col2']],
metrics_combo: [
['m1', 'v1'],
['m2', 'v2'],
],
},
};
describe('FilterControl', () => {
let wrapper;
beforeEach(() => {
wrapper = shallow(<FilterControl {...defaultProps} />);
wrapper.setState({
filters: [
{
valuesLoading: false,
valueChoices: ['a', 'b', 'c', 'd', 'e', 'f'],
},
{
valuesLoading: false,
valueChoices: ['X', 'Y', 'Z'],
},
// Need a duplicate since onChange calls are not changing props
{
valuesLoading: false,
valueChoices: ['X', 'Y', 'Z'],
},
],
});
});
it('renders Filters', () => {
expect(
React.isValidElement(<FilterControl {...defaultProps} />),
).to.equal(true);
});
it('renders one button and two filters', () => {
expect(wrapper.find(Filter)).to.have.lengthOf(2);
expect(wrapper.find(Button)).to.have.lengthOf(1);
});
it('adds filter when clicking Add Filter', () => {
const addButton = wrapper.find('#add-button');
expect(addButton).to.have.lengthOf(1);
addButton.simulate('click');
expect(defaultProps.onChange).to.have.property('callCount', 1);
expect(defaultProps.onChange.getCall(0).args[0]).to.deep.equal([
{
col: 'col1',
op: 'in',
val: ['a', 'b', 'd'],
},
{
col: 'col2',
op: '==',
val: 'Z',
},
{
col: 'col1',
op: 'in',
val: [],
},
]);
});
it('removes a the second filter when its delete button is clicked', () => {
expect(wrapper.find(Filter)).to.have.lengthOf(2);
wrapper.instance().removeFilter(1);
expect(defaultProps.onChange).to.have.property('callCount', 2);
expect(defaultProps.onChange.getCall(1).args[0]).to.deep.equal([
{
col: 'col1',
op: 'in',
val: ['a', 'b', 'd'],
},
]);
});
before(() => {
sinon.stub($, 'ajax');
});
after(() => {
$.ajax.restore();
});
it('makes a GET request to retrieve value choices', () => {
wrapper.instance().fetchFilterValues(0, 'col1');
expect($.ajax.getCall(0).args[0].type).to.deep.equal('GET');
expect($.ajax.getCall(0).args[0].url).to.deep.equal('/superset/filter/qtable/1/col1/');
});
it('changes filter values when one is removed', () => {
wrapper.instance().changeFilter(0, 'val', ['a', 'b']);
expect(defaultProps.onChange).to.have.property('callCount', 3);
expect(defaultProps.onChange.getCall(2).args[0]).to.deep.equal([
{
col: 'col1',
op: 'in',
val: ['a', 'b'],
},
{
col: 'col2',
op: '==',
val: 'Z',
},
]);
});
it('changes filter values when one is added', () => {
wrapper.instance().changeFilter(0, 'val', ['a', 'b', 'd', 'e']);
expect(defaultProps.onChange).to.have.property('callCount', 4);
expect(defaultProps.onChange.getCall(3).args[0]).to.deep.equal([
{
col: 'col1',
op: 'in',
val: ['a', 'b', 'd', 'e'],
},
{
col: 'col2',
op: '==',
val: 'Z',
},
]);
});
it('changes op and transforms values', () => {
wrapper.instance().changeFilter(0, ['val', 'op'], ['a', '==']);
wrapper.instance().changeFilter(1, ['val', 'op'], [['Z'], 'in']);
expect(defaultProps.onChange).to.have.property('callCount', 6);
expect(defaultProps.onChange.getCall(4).args[0]).to.deep.equal([
{
col: 'col1',
op: '==',
val: 'a',
},
{
col: 'col2',
op: '==',
val: 'Z',
},
]);
expect(defaultProps.onChange.getCall(5).args[0]).to.deep.equal([
{
col: 'col1',
op: 'in',
val: ['a', 'b', 'd'],
},
{
col: 'col2',
op: 'in',
val: ['Z'],
},
]);
});
it('changes column and clears invalid values', () => {
wrapper.instance().changeFilter(0, 'col', 'col2');
expect(defaultProps.onChange).to.have.property('callCount', 7);
expect(defaultProps.onChange.getCall(6).args[0]).to.deep.equal([
{
col: 'col2',
op: 'in',
val: [],
},
{
col: 'col2',
op: '==',
val: 'Z',
},
]);
wrapper.instance().changeFilter(1, 'col', 'col1');
expect(defaultProps.onChange).to.have.property('callCount', 8);
expect(defaultProps.onChange.getCall(7).args[0]).to.deep.equal([
{
col: 'col1',
op: 'in',
val: ['a', 'b', 'd'],
},
{
col: 'col1',
op: '==',
val: '',
},
]);
});
it('tracks an active filter select ajax request', () => {
const spyReq = sinon.spy();
$.ajax.reset();
$.ajax.onFirstCall().returns(spyReq);
wrapper.instance().fetchFilterValues(0, 'col1');
expect(wrapper.state().activeRequest).to.equal(spyReq);
// Sets active to null after success
$.ajax.getCall(0).args[0].success(['opt1', 'opt2', null, '']);
expect(wrapper.state().filters[0].valuesLoading).to.equal(false);
expect(wrapper.state().filters[0].valueChoices).to.deep.equal(['opt1', 'opt2', null, '']);
expect(wrapper.state().activeRequest).to.equal(null);
});
it('cancels active request if another is submitted', () => {
const spyReq = sinon.spy();
spyReq.abort = sinon.spy();
$.ajax.reset();
$.ajax.onFirstCall().returns(spyReq);
wrapper.instance().fetchFilterValues(0, 'col1');
expect(wrapper.state().activeRequest).to.equal(spyReq);
const spyReq1 = sinon.spy();
$.ajax.onSecondCall().returns(spyReq1);
wrapper.instance().fetchFilterValues(1, 'col2');
expect(spyReq.abort.called).to.equal(true);
expect(wrapper.state().activeRequest).to.equal(spyReq1);
});
});

View File

@@ -1,115 +0,0 @@
/* eslint-disable no-unused-expressions */
import React from 'react';
import Select from 'react-select';
import { Button } from 'react-bootstrap';
import sinon from 'sinon';
import { expect } from 'chai';
import { describe, it, beforeEach } from 'mocha';
import { shallow } from 'enzyme';
import Filter from '../../../../src/explore/components/controls/Filter';
import SelectControl from '../../../../src/explore/components/controls/SelectControl';
const defaultProps = {
changeFilter: sinon.spy(),
removeFilter: () => {},
filter: {
col: null,
op: 'in',
value: ['val'],
},
datasource: {
id: 1,
type: 'qtable',
filter_select: false,
filterable_cols: ['col1', 'col2'],
metrics_combo: [
['m1', 'v1'],
['m2', 'v2'],
],
},
};
describe('Filter', () => {
let wrapper;
beforeEach(() => {
wrapper = shallow(<Filter {...defaultProps} />);
});
it('renders Filters', () => {
expect(
React.isValidElement(<Filter {...defaultProps} />),
).to.equal(true);
});
it('renders two selects, one button and one input', () => {
expect(wrapper.find(Select)).to.have.lengthOf(2);
expect(wrapper.find(Button)).to.have.lengthOf(1);
expect(wrapper.find(SelectControl)).to.have.lengthOf(1);
expect(wrapper.find('#select-op').prop('options')).to.have.lengthOf(10);
});
it('renders five op choices for table datasource', () => {
const props = Object.assign({}, defaultProps);
props.datasource = {
id: 1,
type: 'druid',
filter_select: false,
filterable_cols: ['country_name'],
};
const druidWrapper = shallow(<Filter {...props} />);
expect(druidWrapper.find('#select-op').prop('options')).to.have.lengthOf(11);
});
it('renders six op choices for having filter', () => {
const props = Object.assign({}, defaultProps);
props.having = true;
const havingWrapper = shallow(<Filter {...props} />);
expect(havingWrapper.find('#select-op').prop('options')).to.have.lengthOf(6);
});
it('calls changeFilter when select is changed', () => {
const selectCol = wrapper.find('#select-col');
selectCol.simulate('change', { value: 'col' });
const selectOp = wrapper.find('#select-op');
selectOp.simulate('change', { value: 'in' });
const selectVal = wrapper.find(SelectControl);
selectVal.simulate('change', { value: 'x' });
expect(defaultProps.changeFilter).to.have.property('callCount', 3);
});
it('renders input for regex filters', () => {
const props = Object.assign({}, defaultProps);
props.filter = {
col: null,
op: 'regex',
value: 'val',
};
const regexWrapper = shallow(<Filter {...props} />);
expect(regexWrapper.find('input')).to.have.lengthOf(1);
});
it('renders `input` for text filters', () => {
const props = Object.assign({}, defaultProps);
['>=', '>', '<=', '<'].forEach((op) => {
props.filter = {
col: 'col1',
op,
value: 'val',
};
wrapper = shallow(<Filter {...props} />);
expect(wrapper.find('input')).to.have.lengthOf(1);
});
});
it('replaces null filter values with empty string in `input`', () => {
const props = Object.assign({}, defaultProps);
props.filter = {
col: 'col1',
op: '>=',
value: null,
};
wrapper = shallow(<Filter {...props} />);
expect(wrapper.find('input').props().value).to.equal('');
});
});