Filter Box实现级联
根据选择实现级联操作
Filter Box.jsx
/**
* 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 VirtualizedSelect from 'react-virtualized-select';
import {Creatable} from 'react-select';
import {Button} from 'react-bootstrap';
import {t} from '@superset-ui/translation';
import DateFilterControl from '../../explore/components/controls/DateFilterControl';
import ControlRow from '../../explore/components/ControlRow';
import Control from '../../explore/components/Control';
import controls from '../../explore/controls';
import OnPasteSelect from '../../components/OnPasteSelect';
import VirtualizedRendererWrap from '../../components/VirtualizedRendererWrap';
import './FilterBox.css';
// maps control names to their key in extra_filters
const TIME_FILTER_MAP = {
time_range: '__time_range',
granularity_sqla: '__time_col',
time_grain_sqla: '__time_grain',
druid_time_origin: '__time_origin',
granularity: '__granularity',
};
const TIME_RANGE = '__time_range';
const propTypes = {
origSelectedValues: PropTypes.object,
datasource: PropTypes.object.isRequired,
instantFiltering: PropTypes.bool,
filtersFields: PropTypes.arrayOf(PropTypes.shape({
field: PropTypes.string,
label: PropTypes.string,
})),
filtersChoices: PropTypes.objectOf(PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string,
text: PropTypes.string,
filter: PropTypes.string,
metric: PropTypes.number,
}))),
onChange: PropTypes.func,
showDateFilter: PropTypes.bool,
showSqlaTimeGrain: PropTypes.bool,
showSqlaTimeColumn: PropTypes.bool,
showDruidTimeGrain: PropTypes.bool,
showDruidTimeOrigin: PropTypes.bool,
};
const defaultProps = {
origSelectedValues: {},
onChange: () => {
},
showDateFilter: false,
showSqlaTimeGrain: false,
showSqlaTimeColumn: false,
showDruidTimeGrain: false,
showDruidTimeOrigin: false,
instantFiltering: true,
};
class FilterBox extends React.Component {
constructor(props) {
super(props);
this.state = {
selectedValues: props.origSelectedValues,
hasChanged: false,
};
this.changeFilter = this.changeFilter.bind(this);
}
getControlData(controlName) {
const {selectedValues} = this.state;
const control = Object.assign({}, controls[controlName], {
name: controlName,
key: `control-${controlName}`,
value: selectedValues[TIME_FILTER_MAP[controlName]],
actions: {setControlValue: this.changeFilter},
});
const mapFunc = control.mapStateToProps;
return mapFunc
? Object.assign({}, control, mapFunc(this.props))
: control;
}
clickApply() {
const {selectedValues} = this.state;
Object.keys(selectedValues).forEach((fltr, i, arr) => {
let refresh = false;
if (i === arr.length - 1) {
refresh = true;
}
this.props.onChange(fltr, selectedValues[fltr], false, refresh);
});
this.setState({hasChanged: false});
}
changeFilter(filter, options) {
const fltr = TIME_FILTER_MAP[filter] || filter;
let vals = null;
if (options !== null) {
if (Array.isArray(options)) {
vals = options.map(opt => opt.value);
} else if (options.value) {
vals = options.value;
} else {
vals = options;
}
}
const selectedValues = Object.assign({}, this.state.selectedValues);
selectedValues[fltr] = vals;
this.setState({selectedValues, hasChanged: true});
if (this.props.instantFiltering) {
this.props.onChange(fltr, vals, false, true);
}
}
renderDateFilter() {
const {showDateFilter} = this.props;
if (showDateFilter) {
return (
<div className="row space-1">
<div className="col-lg-12 col-xs-12">
<DateFilterControl
name={TIME_RANGE}
label={t('Time range')}
description={t('Select start and end date')}
onChange={(...args) => {
this.changeFilter(TIME_RANGE, ...args);
}}
value={this.state.selectedValues[TIME_RANGE] || 'No filter'}
/>
</div>
</div>
);
}
return null;
}
renderDatasourceFilters() {
const {
showSqlaTimeGrain,
showSqlaTimeColumn,
showDruidTimeGrain,
showDruidTimeOrigin,
} = this.props;
const datasourceFilters = [];
const sqlaFilters = [];
const druidFilters = [];
if (showSqlaTimeGrain) sqlaFilters.push('time_grain_sqla');
if (showSqlaTimeColumn) sqlaFilters.push('granularity_sqla');
if (showDruidTimeGrain) druidFilters.push('granularity');
if (showDruidTimeOrigin) druidFilters.push('druid_time_origin');
if (sqlaFilters.length) {
datasourceFilters.push(
<ControlRow
key="sqla-filters"
className="control-row"
controls={sqlaFilters.map(control => (
<Control {...this.getControlData(control)} />
))}
/>,
);
}
if (druidFilters.length) {
datasourceFilters.push(
<ControlRow
key="druid-filters"
className="control-row"
controls={druidFilters.map(control => (
<Control {...this.getControlData(control)} />
))}
/>,
);
}
return datasourceFilters;
}
renderSelect(filterConfig) {
const {filtersChoices} = this.props;
const {selectedValues} = this.state;
// Add created options to filtersChoices, even though it doesn't exist,
// or these options will exist in query sql but invisible to end user.
Object.keys(selectedValues)
.filter(key => selectedValues.hasOwnProperty(key) && (key in filtersChoices))
.forEach((key) => {
const choices = filtersChoices[key] || [];
const choiceIds = new Set(choices.map(f => f.id));
const selectedValuesForKey = Array.isArray(selectedValues[key])
? selectedValues[key]
: [selectedValues[key]];
selectedValuesForKey
.filter(value => !choiceIds.has(value))
.forEach((value) => {
choices.unshift({
filter: key,
id: value,
text: value,
metric: 0,
});
});
});
const {key, label} = filterConfig;
const data = this.props.filtersChoices[key];
let datax = [];
let midKey = 'xx';
let flag = true;
if (JSON.stringify(selectedValues) === '{}') {
datax = data;
} else {
for (var xkey in selectedValues) {
if (xkey == key) {
break;
}
midKey = selectedValues[xkey];
flag = false;
}
}
let setx = new Set();
if (flag) {
datax = data;
} else {
data.map(objx => {
if(JSON.stringify(midKey).indexOf('[') != -1){
midKey.map(opt => {
if (objx['dep'] == opt) {
setx.add(objx['id']);
}
});
}else{
if (objx['dep'] == midKey) {
setx.add(objx['id']);
}
}
});
for(let xvalue of setx){
datax.push({'id': xvalue,'text': xvalue});
}
}
const max = Math.max(...data.map(d => d.metric));
let value = selectedValues[key] || null;
// Assign default value if required
if (!value && filterConfig.defaultValue) {
if (filterConfig.multiple) {
// Support for semicolon-delimited multiple values
value = filterConfig.defaultValue.split(';');
} else {
value = filterConfig.defaultValue;
}
}
return (
<OnPasteSelect
placeholder={t('Select [%s]', label)}
key={key}
multi={filterConfig.multiple}
clearable={filterConfig.clearable}
value={value}
options={datax.map((opt) => {
const perc = Math.round((opt.metric / max) * 100);
const backgroundImage = (
'linear-gradient(to right, lightgrey, ' +
`lightgrey ${perc}%, rgba(0,0,0,0) ${perc}%`
);
const style = {
backgroundImage,
padding: '2px 5px',
};
return {value: opt.id, label: opt.id, style};
})}
onChange={(...args) => {
this.changeFilter(key, ...args);
}}
selectComponent={Creatable}
selectWrap={VirtualizedSelect}
optionRenderer={VirtualizedRendererWrap(opt => opt.label)}
noResultsText={t('No results found')}
/>);
}
renderFilters() {
const {filtersFields} = this.props;
return filtersFields.map((filterConfig) => {
const {label, key} = filterConfig;
return (
<div key={key} className="m-b-5">
{label}
{this.renderSelect(filterConfig)}
</div>
);
});
}
render() {
const {instantFiltering} = this.props;
return (
<div className="scrollbar-container">
<div className="scrollbar-content">
{this.renderDateFilter()}
{this.renderDatasourceFilters()}
{this.renderFilters()}
{!instantFiltering &&
<Button
bsSize="small"
bsStyle="primary"
onClick={this.clickApply.bind(this)}
disabled={!this.state.hasChanged}
>
{t('Apply')}
</Button>
}
</div>
</div>
);
}
}
FilterBox.propTypes = propTypes;
FilterBox.defaultProps = defaultProps;
export default FilterBox;