Skip to content

Commit

Permalink
[explore] improved filters (#2330)
Browse files Browse the repository at this point in the history
* Support more filter operators

* more filter operators [>, <, >=, <=, ==, !=, LIKE]
* Fix need to escape/double `%` in LIKE clauses
* spinner while loading values when changing column
* datasource config elements to allow to applying predicates when
  fetching filter values
* refactor

* Removing doubling parens

* rebasing

* Merging migrations
  • Loading branch information
mistercrunch authored Mar 21, 2017
1 parent 82bc907 commit 8042ac8
Show file tree
Hide file tree
Showing 15 changed files with 245 additions and 243 deletions.
1 change: 0 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ def get_git_sha():
'sqlparse==0.1.19',
'thrift>=0.9.3',
'thrift-sasl>=0.2.1',
'werkzeug==0.11.15',
],
extras_require={
'cors': ['Flask-Cors>=2.0.0'],
Expand Down
149 changes: 76 additions & 73 deletions superset/assets/javascripts/explorev2/components/controls/Filter.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,32 @@ import Select from 'react-select';
import { Button, Row, Col } from 'react-bootstrap';
import SelectControl from './SelectControl';

const arrayFilterOps = ['in', 'not in'];
const strFilterOps = ['==', '!=', '>', '<', '>=', '<=', 'regex'];
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 },
{ val: '!=', type: 'string', useSelect: true, multi: false },
{ val: '>=', type: 'string' },
{ val: '<=', type: 'string' },
{ val: '>', type: 'string' },
{ val: '<', type: 'string' },
{ val: 'regex', type: 'string', datasourceTypes: ['druid'] },
{ val: 'LIKE', type: 'string', datasourceTypes: ['table'] },
];
const operators = {};
operatorsArr.forEach(op => {
operators[op.val] = op;
});

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

const defaultProps = {
having: false,
changeFilter: () => {},
removeFilter: () => {},
choices: [],
Expand All @@ -27,112 +39,102 @@ const defaultProps = {
export default class Filter extends React.Component {
constructor(props) {
super(props);
const filterOps = props.datasource.type === 'table' ?
['in', 'not in'] : ['==', '!=', 'in', 'not in', 'regex'];
this.opChoices = this.props.having ? ['==', '!=', '>', '<', '>=', '<=']
: filterOps;
this.state = {
valuesLoading: false,
};
}
componentDidMount() {
this.fetchFilterValues(this.props.filter.col);
}
fetchFilterValues(col) {
if (!this.props.datasource) {
return;
}
const datasource = this.props.datasource;
let choices = [];
if (col) {
if (col && this.props.datasource && this.props.datasource.filter_select) {
this.setState({ valuesLoading: true });
$.ajax({
type: 'GET',
url: `/superset/filter/${datasource.type}/${datasource.id}/${col}/`,
success: (data) => {
choices = Object.keys(data).map((k) =>
([`'${data[k]}'`, `'${data[k]}'`]));
this.props.changeFilter('choices', choices);
this.props.changeFilter('choices', data);
this.setState({ valuesLoading: false });
},
});
}
}
switchFilterValue(prevFilter, nextOp) {
const prevOp = prevFilter.op;
let newVal = null;
if (arrayFilterOps.indexOf(prevOp) !== -1
&& strFilterOps.indexOf(nextOp) !== -1) {
switchFilterValue(prevOp, nextOp) {
if (operators[prevOp].type !== operators[nextOp].type) {
const val = this.props.filter.value;
let newVal;
// switch from array to string
newVal = this.props.filter.val.length > 0 ? this.props.filter.val[0] : '';
}
if (strFilterOps.indexOf(prevOp) !== -1
&& arrayFilterOps.indexOf(nextOp) !== -1) {
// switch from string to array
newVal = this.props.filter.val === '' ? [] : [this.props.filter.val];
}
return newVal;
}
changeFilter(control, event) {
let value = event;
if (event && event.target) {
value = event.target.value;
}
if (event && event.value) {
value = event.value;
}
if (control === 'op') {
const newVal = this.switchFilterValue(this.props.filter, value);
if (newVal) {
this.props.changeFilter(['op', 'val'], [value, newVal]);
} else {
this.props.changeFilter(control, value);
if (operators[nextOp].type === 'string' && val && val.length > 0) {
newVal = val[0];
} else if (operators[nextOp].type === 'string' && val) {
newVal = [val];
}
} else {
this.props.changeFilter(control, value);
}
if (control === 'col' && value !== null && this.props.datasource.filter_select) {
this.fetchFilterValues(value);
this.props.changeFilter('val', newVal);
}
}
changeText(event) {
this.props.changeFilter('val', event.target.value);
}
changeSelect(value) {
this.props.changeFilter('val', value);
}
changeColumn(event) {
this.props.changeFilter('col', event.value);
this.fetchFilterValues(event.value);
}
changeOp(event) {
this.switchFilterValue(this.props.filter.op, event.value);
this.props.changeFilter('op', event.value);
}
removeFilter(filter) {
this.props.removeFilter(filter);
}
renderFilterFormControl(filter) {
const datasource = this.props.datasource;
if (datasource && datasource.filter_select) {
if (!filter.choices) {
this.fetchFilterValues(filter.col);
}
}
// switching filter value between array/string when needed
if (strFilterOps.indexOf(filter.op) !== -1) {
// druid having filter or regex/==/!= filters
const operator = operators[filter.op];
if (operator.useSelect) {
return (
<input
type="text"
onChange={this.changeFilter.bind(this, 'val')}
<SelectControl
multi={operator.multi}
freeForm
name="filter-value"
value={filter.val}
className="form-control input-sm"
placeholder="Filter value"
isLoading={this.state.valuesLoading}
choices={filter.choices}
onChange={this.changeSelect.bind(this)}
/>
);
}
return (
<SelectControl
multi
freeForm
name="filter-value"
<input
type="text"
onChange={this.changeText.bind(this)}
value={filter.val}
choices={filter.choices || []}
onChange={this.changeFilter.bind(this, 'val')}
className="form-control input-sm"
placeholder="Filter value"
/>
);
}
render() {
const datasource = this.props.datasource;
const filter = this.props.filter;
const opsChoices = operatorsArr
.filter(o => !o.datasourceTypes || o.datasourceTypes.indexOf(datasource.type) >= 0)
.map(o => ({ value: o.val, label: o.val }));
const colChoices = datasource ?
datasource.filterable_cols.map(c => ({ value: c[0], label: c[1] })) :
null;
return (
<div>
<Row className="space-1">
<Col md={12}>
<Select
id="select-col"
placeholder="Select column"
options={this.props.choices.map((c) => ({ value: c[0], label: c[1] }))}
clearable={false}
options={colChoices}
value={filter.col}
onChange={this.changeFilter.bind(this, 'col')}
onChange={this.changeColumn.bind(this)}
/>
</Col>
</Row>
Expand All @@ -141,9 +143,10 @@ export default class Filter extends React.Component {
<Select
id="select-op"
placeholder="Select operator"
options={this.opChoices.map((o) => ({ value: o, label: o }))}
options={opsChoices}
clearable={false}
value={filter.op}
onChange={this.changeFilter.bind(this, 'op')}
onChange={this.changeOp.bind(this)}
/>
</Col>
<Col md={7}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,24 @@ import Filter from './Filter';

const propTypes = {
name: PropTypes.string,
choices: PropTypes.array,
onChange: PropTypes.func,
value: PropTypes.array,
datasource: PropTypes.object,
};

const defaultProps = {
choices: [],
onChange: () => {},
value: [],
};

export default class FilterControl extends React.Component {
addFilter() {
const newFilters = Object.assign([], this.props.value);
const col = this.props.datasource && this.props.datasource.filterable_cols.length > 0 ?
this.props.datasource.filterable_cols[0][0] :
null;
newFilters.push({
col: null,
col,
op: 'in',
val: this.props.datasource.filter_select ? [] : '',
});
Expand All @@ -43,22 +44,17 @@ export default class FilterControl extends React.Component {
this.props.onChange(this.props.value.filter((f, i) => i !== index));
}
render() {
const filters = [];
this.props.value.forEach((filter, i) => {
const filterBox = (
<div key={i}>
<Filter
having={this.props.name === 'having_filters'}
filter={filter}
choices={this.props.choices}
datasource={this.props.datasource}
removeFilter={this.removeFilter.bind(this, i)}
changeFilter={this.changeFilter.bind(this, i)}
/>
</div>
);
filters.push(filterBox);
});
const filters = this.props.value.map((filter, i) => (
<div key={i}>
<Filter
having={this.props.name === 'having_filters'}
filter={filter}
datasource={this.props.datasource}
removeFilter={this.removeFilter.bind(this, i)}
changeFilter={this.changeFilter.bind(this, i)}
/>
</div>
));
return (
<div>
{filters}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,30 +47,39 @@ export default class SelectControl extends React.PureComponent {
this.props.onChange(optionValue);
}
getOptions(props) {
const options = props.choices.map((c) => {
const label = c.length > 1 ? c[1] : c[0];
const newOptions = {
value: c[0],
label,
};
if (c[2]) newOptions.imgSrc = c[2];
return newOptions;
// Accepts different formats of input
const options = props.choices.map(c => {
let option;
if (Array.isArray(c)) {
const label = c.length > 1 ? c[1] : c[0];
option = {
value: c[0],
label,
};
if (c[2]) option.imgSrc = c[2];
} else if (Object.is(c)) {
option = c;
} else {
option = {
value: c,
label: c,
};
}
return option;
});
if (props.freeForm) {
// For FreeFormSelect, insert value into options if not exist
const values = props.choices.map((c) => c[0]);
const values = options.map(c => c.value);
if (props.value) {
if (typeof props.value === 'object') {
props.value.forEach((v) => {
if (values.indexOf(v) === -1) {
options.push({ value: v, label: v });
}
});
} else {
if (values.indexOf(props.value) === -1) {
options.push({ value: props.value, label: props.value });
}
let valuesToAdd = props.value;
if (!Array.isArray(valuesToAdd)) {
valuesToAdd = [valuesToAdd];
}
valuesToAdd.forEach(v => {
if (values.indexOf(v) < 0) {
options.push({ value: v, label: v });
}
});
}
}
return options;
Expand Down
1 change: 0 additions & 1 deletion superset/assets/javascripts/explorev2/stores/controls.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -1177,7 +1177,6 @@ export const controls = {
default: [],
description: '',
mapStateToProps: (state) => ({
choices: (state.datasource) ? state.datasource.filterable_cols : [],
datasource: state.datasource,
}),
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const defaultProps = {
id: 1,
type: 'table',
filter_select: false,
filterable_cols: ['country_name'],
},
};

Expand Down
Loading

0 comments on commit 8042ac8

Please sign in to comment.