Skip to content

Commit

Permalink
[AmericanExpress] PR Funnel/Step Visualization
Browse files Browse the repository at this point in the history
  • Loading branch information
TheLastSultan committed Oct 11, 2019
1 parent c09e3e7 commit dce602c
Show file tree
Hide file tree
Showing 18 changed files with 1,235 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/**
* 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.
*/
/* eslint-disable no-unused-expressions */
import React from 'react';
import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { shallow } from 'enzyme';
import { ListGroupItem } from 'react-bootstrap';

import chartQueries from '../../dashboard/fixtures/mockChartQueries';
import StepsControl from '../../../../src/explore/components/controls/StepsControl';
import Button from '../../../../src/components/Button';

describe('StepsControl', () => {
const middlewares = [thunk];
const mockStore = configureStore(middlewares);
const initialState = {
charts: { 0: {} },
explore: {
can_overwrite: null,
user_id: '1',
datasource: {},
slice: null,
controls: {
viz_type: {
value: 'funnel',
},
},
},
selectedValues: {},
};
const store = mockStore(initialState);

const defaultProps = {
name: 'steps',
label: 'Steps',
value: {},
origSelectedValues: {},
vizType: '',
annotationError: {},
annotationQuery: {},
onChange: () => {},
charts: chartQueries,
};

const getWrapper = () =>
shallow(<StepsControl {...defaultProps} />, {
context: { store },
}).dive();

it('renders Add Step button and Absolute filter', () => {
const wrapper = getWrapper();
expect(wrapper.find(ListGroupItem)).toHaveLength(1);
});

it('add/remove Step', () => {
const wrapper = getWrapper();
const label = wrapper.find(ListGroupItem).first();
label.simulate('click');
setTimeout(() => {
expect(wrapper.find('.list-group')).toHaveLength(1);
expect(wrapper.find('.metrics-select')).toHaveLength(2);
expect(wrapper.find(Button)).toHaveLength(1);
expect(wrapper.find(Button)).first().simulate('click');
setTimeout(() => {
expect(wrapper.find('list-group')).toHaveLength(0);
}, 10);
}, 10);
});

it('onChange', () => {
const wrapper = getWrapper();

wrapper.instance().onChange(0, 'testControl', { test: true });
expect(wrapper.state().selectedValues).toMatchObject({ 0: { testControl: { test: true } } });

});
});
204 changes: 204 additions & 0 deletions superset/assets/src/explore/components/controls/Filter.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
/**
* 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 PropTypes from 'prop-types';
import Select from 'react-select';
import { Button, Row, Col } from 'react-bootstrap';
import { t } from '@superset-ui/translation';
import SelectControl from './SelectControl';

const operatorsArr = [
{ val: 'in', type: 'array', useSelect: true, multi: true },
{ val: 'not in', type: 'array', useSelect: true, multi: true },
{ val: '==', type: 'string', useSelect: true, multi: false, havingOnly: true },
{ val: '!=', type: 'string', useSelect: true, multi: false, havingOnly: true },
{ val: '>=', type: 'string', havingOnly: true },
{ val: '<=', type: 'string', havingOnly: true },
{ val: '>', type: 'string', havingOnly: true },
{ val: '<', type: 'string', havingOnly: true },
{ val: 'regex', type: 'string', datasourceTypes: ['druid'] },
{ val: 'LIKE', type: 'string', datasourceTypes: ['table'] },
{ val: 'IS NULL', type: null },
{ val: 'IS NOT NULL', type: null },
];
const operators = {};
operatorsArr.forEach((op) => {
operators[op.val] = op;
});

const propTypes = {
changeFilter: PropTypes.func,
removeFilter: PropTypes.func,
filter: PropTypes.object.isRequired,
datasource: PropTypes.object,
having: PropTypes.bool,
valuesLoading: PropTypes.bool,
valueChoices: PropTypes.array,
};

const defaultProps = {
changeFilter: () => {},
removeFilter: () => {},
datasource: null,
having: false,
valuesLoading: false,
valueChoices: [],
};

export default class Filter extends React.Component {

switchFilterValue(prevOp, nextOp) {
if (operators[prevOp].type !== operators[nextOp].type) {
// Switch from array to string or vice versa
const val = this.props.filter.val;
let newVal;
if (operators[nextOp].type === 'string') {
if (!val || !val.length) {
newVal = '';
} else {
newVal = val[0];
}
} else if (operators[nextOp].type === 'array') {
if (!val || !val.length) {
newVal = [];
} else {
newVal = [val];
}
}
this.props.changeFilter(['val', 'op'], [newVal, nextOp]);
} else {
// No value type change
this.props.changeFilter('op', nextOp);
}
}

changeText(event) {
this.props.changeFilter('val', event.target.value);
}

changeSelect(value) {
this.props.changeFilter('val', value);
}

changeColumn(event) {
this.props.changeFilter('col', event.value);
}

changeOp(event) {
this.switchFilterValue(this.props.filter.op, event.value);
}

removeFilter(filter) {
this.props.removeFilter(filter);
}

renderFilterFormControl(filter) {
const operator = operators[filter.op];
if (operator.type === null) {
// IS NULL or IS NOT NULL
return null;
}
if (operator.useSelect && !this.props.having) {
// TODO should use a simple Select, not a control here...
return (
<SelectControl
multi={operator.multi}
freeForm
name="filter-value"
value={filter.val}
isLoading={this.props.valuesLoading}
choices={this.props.valueChoices}
onChange={this.changeSelect}
showHeader={false}
/>
);
}
return (
<input
type="text"
onChange={this.changeText.bind(this)}
value={filter.val || ''}
className="form-control input-sm"
placeholder={t('Filter value')}
/>
);
}
render() {
const { datasource, filter } = this.props;
const opsChoices = operatorsArr
.filter((o) => {
if (this.props.having) {
return !!o.havingOnly;
}
return (!o.datasourceTypes || o.datasourceTypes.indexOf(datasource.type) >= 0);
})
.map(o => ({ value: o.val, label: o.val }));
let colChoices;
if (datasource) {
if (this.props.having) {
colChoices = datasource.metrics_combo.map(c => ({ value: c[0], label: c[1] }));
} else {
colChoices = datasource.filterable_cols.map(c => ({ value: c[0], label: c[1] }));
}
}
return (
<div>
<Row className="space-1">
<Col md={12}>
<Select
id="select-col"
placeholder={this.props.having ? t('Select metric') : t('Select column')}
clearable={false}
options={colChoices}
value={filter.col}
onChange={this.changeColumn.bind(this)}
/>
</Col>
</Row>
<Row className="space-1">
<Col md={3}>
<Select
id="select-op"
placeholder={t('Select operator')}
options={opsChoices}
clearable={false}
value={filter.op}
onChange={this.changeOp.bind(this)}
/>
</Col>
<Col md={7}>
{this.renderFilterFormControl(filter)}
</Col>
<Col md={2}>
<Button
id="remove-button"
bsSize="small"
onClick={this.removeFilter.bind(this)}
>
<i className="fa fa-minus" />
</Button>
</Col>
</Row>
</div>
);
}
}

Filter.propTypes = propTypes;
Filter.defaultProps = defaultProps;
Loading

0 comments on commit dce602c

Please sign in to comment.