[WIP] [explorev2] Refactor filter into FieldSet (#1981)

* [explorev2] Refactor filter into FieldSet

* Fixed tests

* Added tests

* Modifications based on comments
This commit is contained in:
vera-liu
2017-01-24 13:32:40 -08:00
committed by GitHub
parent 2b7673ad5d
commit 1c338ba742
19 changed files with 274 additions and 324 deletions

View File

@@ -78,41 +78,6 @@ export function saveFaveStar(sliceId, isStarred) {
};
}
export const ADD_FILTER = 'ADD_FILTER';
export function addFilter(filter) {
return { type: ADD_FILTER, filter };
}
export const REMOVE_FILTER = 'REMOVE_FILTER';
export function removeFilter(filter) {
return { type: REMOVE_FILTER, filter };
}
export const CHANGE_FILTER = 'CHANGE_FILTER';
export function changeFilter(filter, field, value) {
return { type: CHANGE_FILTER, filter, field, value };
}
export function fetchFilterValues(datasource_type, datasource_id, filter, col) {
return function (dispatch) {
$.ajax({
type: 'GET',
url: `/superset/filter/${datasource_type}/${datasource_id}/${col}/`,
success: (data) => {
dispatch(changeFilter(
filter,
'choices',
Object.keys(data).map((k) => ([`'${data[k]}'`, `'${data[k]}'`]))
)
);
},
error() {
dispatch(changeFilter(filter, 'choices', []));
},
});
};
}
export const SET_FIELD_VALUE = 'SET_FIELD_VALUE';
export function setFieldValue(fieldName, value, validationErrors) {
return { type: SET_FIELD_VALUE, fieldName, value, validationErrors };

View File

@@ -4,11 +4,10 @@ import { bindActionCreators } from 'redux';
import * as actions from '../actions/exploreActions';
import { connect } from 'react-redux';
import { Panel, Alert } from 'react-bootstrap';
import visTypes, { sectionsToRender, commonControlPanelSections } from '../stores/visTypes';
import visTypes, { sectionsToRender } from '../stores/visTypes';
import ControlPanelSection from './ControlPanelSection';
import FieldSetRow from './FieldSetRow';
import FieldSet from './FieldSet';
import Filters from './Filters';
const propTypes = {
datasource_type: PropTypes.string.isRequired,
@@ -58,11 +57,6 @@ class ControlPanelsContainer extends React.Component {
sectionsToRender() {
return sectionsToRender(this.props.form_data.viz_type, this.props.datasource_type);
}
filterSectionsToRender() {
const filterSections = this.props.datasource_type === 'table' ?
[commonControlPanelSections.filters[0]] : commonControlPanelSections.filters;
return filterSections;
}
fieldOverrides() {
const viz = visTypes[this.props.form_data.viz_type];
return viz.fieldOverrides || {};
@@ -100,6 +94,7 @@ class ControlPanelsContainer extends React.Component {
value={this.props.form_data[fieldName]}
validationErrors={this.props.fields[fieldName].validationErrors}
actions={this.props.actions}
prefix={section.prefix}
{...this.getFieldData(fieldName)}
/>
))}
@@ -107,21 +102,6 @@ class ControlPanelsContainer extends React.Component {
))}
</ControlPanelSection>
))}
{this.filterSectionsToRender().map((section) => (
<ControlPanelSection
key={section.label}
label={section.label}
tooltip={section.description}
>
<Filters
filterColumnOpts={[]}
filters={this.props.form_data.filters}
actions={this.props.actions}
prefix={section.prefix}
datasource_id={this.props.form_data.datasource}
/>
</ControlPanelSection>
))}
</Panel>
</div>
);

View File

@@ -3,7 +3,7 @@ import TextField from './TextField';
import CheckboxField from './CheckboxField';
import TextAreaField from './TextAreaField';
import SelectField from './SelectField';
import FilterField from './FilterField';
import ControlHeader from './ControlHeader';
const fieldMap = {
@@ -11,6 +11,7 @@ const fieldMap = {
CheckboxField,
TextAreaField,
SelectField,
FilterField,
};
const fieldTypes = Object.keys(fieldMap);

View File

@@ -1,124 +1,125 @@
import React from 'react';
const $ = window.$ = require('jquery');
import React, { PropTypes } from 'react';
import Select from 'react-select';
import { Button } from 'react-bootstrap';
import { Button, Row, Col } from 'react-bootstrap';
import SelectField from './SelectField';
const propTypes = {
actions: React.PropTypes.object.isRequired,
filterColumnOpts: React.PropTypes.array,
prefix: React.PropTypes.string,
filter: React.PropTypes.object.isRequired,
renderFilterSelect: React.PropTypes.bool,
datasource_type: React.PropTypes.string.isRequired,
datasource_id: React.PropTypes.number.isRequired,
choices: PropTypes.array,
opChoices: PropTypes.array,
changeFilter: PropTypes.func,
removeFilter: PropTypes.func,
filter: PropTypes.object.isRequired,
datasource: PropTypes.object,
};
const defaultProps = {
filterColumnOpts: [],
prefix: 'flt',
changeFilter: () => {},
removeFilter: () => {},
choices: [],
datasource: null,
};
export default class Filter extends React.Component {
constructor(props) {
super(props);
const opChoices = this.props.prefix === 'flt' ?
['in', 'not in'] : ['==', '!=', '>', '<', '>=', '<='];
this.state = {
opChoices,
};
}
componentWillMount() {
if (this.props.filter.col) {
this.props.actions.fetchFilterValues(
this.props.datasource_type,
this.props.datasource_id,
this.props.filter,
this.props.filter.col);
fetchFilterValues(col) {
if (!this.props.datasource) {
return;
}
const datasource = this.props.datasource;
let choices = [];
if (col) {
$.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);
},
});
}
}
changeCol(filter, colOpt) {
const val = (colOpt) ? colOpt.value : null;
this.props.actions.changeFilter(filter, 'col', val);
if (val) {
this.props.actions.fetchFilterValues(
this.props.datasource_type, this.props.datasource_id, filter, val);
changeFilter(field, event) {
let value = event;
if (event && event.target) {
value = event.target.value;
}
if (event && event.value) {
value = event.value;
}
this.props.changeFilter(field, value);
if (field === 'col' && value !== null && this.props.datasource.filter_select) {
this.fetchFilterValues(value);
}
}
changeOp(filter, opOpt) {
const val = (opOpt) ? opOpt.value : null;
this.props.actions.changeFilter(filter, 'op', val);
}
changeValue(filter, event) {
this.props.actions.changeFilter(filter, 'value', event.target.value);
}
changeSelectValue(filter, name, value) {
this.props.actions.changeFilter(filter, 'value', value);
}
removeFilter(filter) {
this.props.actions.removeFilter(filter);
this.props.removeFilter(filter);
}
renderFilterFormField() {
if (this.props.renderFilterSelect) {
renderFilterFormField(filter) {
const datasource = this.props.datasource;
if (datasource && datasource.filter_select) {
if (!filter.choices) {
this.fetchFilterValues(filter.col);
}
return (
<SelectField
multi
freeForm
name="filter-value"
value={this.props.filter.value}
choices={this.props.filter.choices ? this.props.filter.choices : []}
onChange={this.changeSelectValue.bind(this, this.props.filter)}
value={filter.value}
choices={filter.choices}
onChange={this.changeFilter.bind(this, 'value')}
/>
);
}
return (
<input
type="text"
onChange={this.changeValue.bind(this, this.props.filter)}
value={this.props.filter.value}
onChange={this.changeFilter.bind(this, 'value')}
value={filter.value}
className="form-control input-sm"
placeholder="Filter value"
/>
);
}
render() {
const filter = this.props.filter;
return (
<div>
<div className="row space-1">
<Select
className="col-lg-12"
multi={false}
name="select-column"
placeholder="Select column"
options={this.props.filterColumnOpts.map((o) => ({ value: o, label: o }))}
value={this.props.filter.col}
autosize={false}
onChange={this.changeCol.bind(this, this.props.filter)}
/>
</div>
<div className="row space-1">
<Select
className="col-lg-4"
multi={false}
name="select-op"
placeholder="Select operator"
options={this.state.opChoices.map((o) => ({ value: o, label: o }))}
value={this.props.filter.op}
autosize={false}
onChange={this.changeOp.bind(this, this.props.filter)}
/>
<div className="col-lg-6">
{this.renderFilterFormField()}
</div>
<div className="col-lg-2">
<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] }))}
value={filter.col}
onChange={this.changeFilter.bind(this, 'col')}
/>
</Col>
</Row>
<Row className="space-1">
<Col md={3}>
<Select
id="select-op"
placeholder="Select operator"
options={this.props.opChoices.map((o) => ({ value: o, label: o }))}
value={filter.op}
onChange={this.changeFilter.bind(this, 'op')}
/>
</Col>
<Col md={7}>
{this.renderFilterFormField(filter)}
</Col>
<Col md={2}>
<Button
id="remove-button"
bsSize="small"
onClick={this.removeFilter.bind(this, this.props.filter)}
onClick={this.removeFilter.bind(this)}
>
<i className="fa fa-minus" />
</Button>
</div>
</div>
</Col>
</Row>
</div>
);
}

View File

@@ -0,0 +1,86 @@
import React, { PropTypes } from 'react';
import { Button, Row, Col } from 'react-bootstrap';
import Filter from './Filter';
const propTypes = {
prefix: PropTypes.string,
choices: PropTypes.array,
onChange: PropTypes.func,
value: PropTypes.array,
datasource: PropTypes.object,
};
const defaultProps = {
prefix: 'flt',
choices: [],
onChange: () => {},
value: [],
};
export default class FilterField extends React.Component {
constructor(props) {
super(props);
this.opChoices = props.prefix === 'flt' ?
['in', 'not in'] : ['==', '!=', '>', '<', '>=', '<='];
}
addFilter() {
const newFilters = Object.assign([], this.props.value);
newFilters.push({
prefix: this.props.prefix,
col: null,
op: 'in',
value: this.props.datasource.filter_select ? [] : '',
});
this.props.onChange(newFilters);
}
changeFilter(index, field, value) {
const newFilters = Object.assign([], this.props.value);
const modifiedFilter = Object.assign({}, newFilters[index]);
modifiedFilter[field] = value;
newFilters.splice(index, 1, modifiedFilter);
this.props.onChange(newFilters);
}
removeFilter(index) {
this.props.onChange(this.props.value.filter((f, i) => i !== index));
}
render() {
const filters = [];
this.props.value.forEach((filter, i) => {
// only display filters with current prefix
if (filter.prefix === this.props.prefix) {
const filterBox = (
<div key={i}>
<Filter
filter={filter}
choices={this.props.choices}
opChoices={this.opChoices}
datasource={this.props.datasource}
removeFilter={this.removeFilter.bind(this, i)}
changeFilter={this.changeFilter.bind(this, i)}
/>
</div>
);
filters.push(filterBox);
}
});
return (
<div>
{filters}
<Row className="space-2">
<Col md={2}>
<Button
id="add-button"
bsSize="sm"
onClick={this.addFilter.bind(this)}
>
<i className="fa fa-plus" /> &nbsp; Add Filter
</Button>
</Col>
</Row>
</div>
);
}
}
FilterField.propTypes = propTypes;
FilterField.defaultProps = defaultProps;

View File

@@ -1,87 +0,0 @@
import React from 'react';
// import { Tab, Row, Col, Nav, NavItem } from 'react-bootstrap';
import Filter from './Filter';
import { Button } from 'react-bootstrap';
import { connect } from 'react-redux';
import shortid from 'shortid';
const propTypes = {
actions: React.PropTypes.object.isRequired,
datasource_type: React.PropTypes.string.isRequired,
datasource_id: React.PropTypes.number.isRequired,
filterColumnOpts: React.PropTypes.array,
filters: React.PropTypes.array,
prefix: React.PropTypes.string,
renderFilterSelect: React.PropTypes.bool,
};
const defaultProps = {
filterColumnOpts: [],
filters: [],
prefix: 'flt',
};
class Filters extends React.Component {
addFilter() {
this.props.actions.addFilter({
id: shortid.generate(),
prefix: this.props.prefix,
col: null,
op: null,
value: null,
});
}
render() {
const filters = [];
let i = 0;
this.props.filters.forEach((filter) => {
// only display filters with current prefix
i++;
if (filter.prefix === this.props.prefix) {
filters.push(
<Filter
key={i}
filterColumnOpts={this.props.filterColumnOpts}
actions={this.props.actions}
prefix={this.props.prefix}
filter={filter}
renderFilterSelect={this.props.renderFilterSelect}
datasource_type={this.props.datasource_type}
datasource_id={this.props.datasource_id}
/>
);
}
});
return (
<div>
{filters}
<div className="row space-2">
<div className="col-lg-2">
<Button
id="add-button"
bsSize="sm"
onClick={this.addFilter.bind(this)}
>
<i className="fa fa-plus" /> &nbsp; Add Filter
</Button>
</div>
</div>
</div>
);
}
}
Filters.propTypes = propTypes;
Filters.defaultProps = defaultProps;
function mapStateToProps(state) {
return {
datasource_type: state.datasource_type,
filterColumnOpts: state.filterColumnOpts,
filters: state.viz.form_data.filters,
renderFilterSelect: state.filter_select,
};
}
export { Filters };
export default connect(mapStateToProps, () => ({}))(Filters);

View File

@@ -8,7 +8,11 @@ function formatFilters(filters) {
const filter = filters[i];
params[`${filter.prefix}_col_${i + 1}`] = filter.col;
params[`${filter.prefix}_op_${i + 1}`] = filter.op;
params[`${filter.prefix}_eq_${i + 1}`] = filter.value;
if (filter.value.constructor === Array) {
params[`${filter.prefix}_eq_${i + 1}`] = filter.value.join(',');
} else {
params[`${filter.prefix}_eq_${i + 1}`] = filter.value;
}
}
return params;
}

View File

@@ -25,7 +25,6 @@ const bootstrappedState = Object.assign(
initialState(bootstrapData.viz.form_data.viz_type, bootstrapData.datasource_type), {
can_edit: bootstrapData.can_edit,
can_download: bootstrapData.can_download,
filter_select: bootstrapData.filter_select,
datasources: bootstrapData.datasources,
datasource_type: bootstrapData.datasource_type,
viz: bootstrapData.viz,
@@ -40,7 +39,7 @@ bootstrappedState.viz.form_data.datasource_name = bootstrapData.datasource_name;
function parseFilters(form_data, prefix = 'flt') {
const filters = [];
for (let i = 0; i < 10; i++) {
for (let i = 0; i <= 10; i++) {
if (form_data[`${prefix}_col_${i}`] && form_data[`${prefix}_op_${i}`]) {
filters.push({
prefix,

View File

@@ -1,7 +1,6 @@
/* eslint camelcase: 0 */
import { defaultFormData } from '../stores/store';
import * as actions from '../actions/exploreActions';
import { addToArr, removeFromArr, alterInArr } from '../../../utils/reducerUtils';
import { now } from '../../modules/dates';
import { getExploreUrl } from '../exploreUtils';
@@ -41,37 +40,6 @@ export const exploreReducer = function (state, action) {
[actions.SET_DATASOURCE]() {
return Object.assign({}, state, { datasource: action.datasource });
},
[actions.SET_FILTER_COLUMN_OPTS]() {
return Object.assign({}, state, { filterColumnOpts: action.filterColumnOpts });
},
[actions.ADD_FILTER]() {
const newFormData = addToArr(state.viz.form_data, 'filters', action.filter);
const newState = Object.assign(
{},
state,
{ viz: Object.assign({}, state.viz, { form_data: newFormData }) }
);
return newState;
},
[actions.REMOVE_FILTER]() {
const newFormData = removeFromArr(state.viz.form_data, 'filters', action.filter);
return Object.assign(
{},
state,
{ viz: Object.assign({}, state.viz, { form_data: newFormData }) }
);
},
[actions.CHANGE_FILTER]() {
const changes = {};
changes[action.field] = action.value;
const newFormData = alterInArr(
state.viz.form_data, 'filters', action.filter, changes);
return Object.assign(
{},
state,
{ viz: Object.assign({}, state.viz, { form_data: newFormData }) }
);
},
[actions.SET_FIELD_VALUE]() {
let newFormData = Object.assign({}, state.viz.form_data);
if (action.fieldName === 'datasource') {

View File

@@ -1123,6 +1123,17 @@ export const fields = {
default: '',
description: 'Labels for the marker lines',
},
filters: {
type: 'FilterField',
label: '',
default: [],
description: '',
mapStateToProps: (state) => ({
choices: (state.datasource) ? state.datasource.filterable_cols : [],
datasource: state.datasource,
}),
},
};
export default fields;

View File

@@ -43,7 +43,6 @@ export function initialState(vizType = 'table', datasourceType = 'table') {
datasources: null,
datasource_type: null,
filterColumnOpts: [],
filter_select: false,
fields,
viz: defaultViz(vizType, datasourceType),
isStarred: false,

View File

@@ -60,11 +60,15 @@ export const commonControlPanelSections = {
'Leave the value field empty to filter empty strings or nulls' +
'For filters with comma in values, wrap them in single quotes' +
"as in <NY, 'Tahoe, CA', DC>",
prefix: 'flt',
fieldSetRows: [['filters']],
},
{
label: 'Result Filters',
description: 'The filters to apply after post-aggregation.' +
'Leave the value field empty to filter empty strings or nulls',
prefix: 'having',
fieldSetRows: [['filters']],
},
],
};
@@ -751,12 +755,15 @@ export function sectionsToRender(vizType, datasourceType) {
const viz = visTypes[vizType];
const timeSection = datasourceType === 'table' ?
commonControlPanelSections.sqlaTimeSeries : commonControlPanelSections.druidTimeSeries;
const { datasourceAndVizType, sqlClause } = commonControlPanelSections;
const { datasourceAndVizType, sqlClause, filters } = commonControlPanelSections;
const filtersToRender =
datasourceType === 'table' ? filters[0] : filters;
const sections = [].concat(
datasourceAndVizType,
timeSection,
viz.controlPanelSections,
sqlClause
sqlClause,
filtersToRender
);
return sections;
}