mirror of
https://github.com/apache/superset.git
synced 2026-04-18 07:35:09 +00:00
[adhoc-filters] Adding adhoc-filters to all viz types (#5206)
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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('');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user