[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

@@ -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);