Skip to content

Commit

Permalink
[Feature/Bugfix] Datepicker and time granularity options to dashboard…
Browse files Browse the repository at this point in the history
… filters (apache#3508)

* Feature: added datepicker and time granularity options to dashboard filter

* Added option for Druid datasource time filters

* added more checkbox control over dashboard time filters
  • Loading branch information
Mogball authored and michellethomas committed May 23, 2018
1 parent f75934c commit 1fdcf10
Show file tree
Hide file tree
Showing 10 changed files with 260 additions and 79 deletions.
11 changes: 8 additions & 3 deletions superset/assets/javascripts/dashboard/Dashboard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ export function dashboardContainer(dashboard, datasources, userid) {
const f = [];
const immuneSlices = this.metadata.filter_immune_slices || [];
if (sliceId && immuneSlices.includes(sliceId)) {
// The slice is immune to dashboard fiterls
// The slice is immune to dashboard filters
return f;
}

Expand Down Expand Up @@ -205,8 +205,13 @@ export function dashboardContainer(dashboard, datasources, userid) {
return f;
},
addFilter(sliceId, col, vals, merge = true, refresh = true) {
if (this.getSlice(sliceId) && (col === '__from' || col === '__to' ||
this.getSlice(sliceId).formData.groupby.indexOf(col) !== -1)) {
if (
this.getSlice(sliceId) && (
['__from', '__to', '__time_col', '__time_grain', '__time_origin', '__granularity']
.indexOf(col) >= 0 ||
this.getSlice(sliceId).formData.groupby.indexOf(col) !== -1
)
) {
if (!(sliceId in this.filters)) {
this.filters[sliceId] = {};
}
Expand Down
29 changes: 29 additions & 0 deletions superset/assets/javascripts/explore/stores/controls.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,7 @@ export const controls = {
mapStateToProps: state => ({
choices: (state.datasource) ? state.datasource.granularity_sqla : [],
}),
freeForm: true,
},

time_grain_sqla: {
Expand Down Expand Up @@ -1020,6 +1021,34 @@ export const controls = {
description: t('Whether to include a time filter'),
},

show_sqla_time_granularity: {
type: 'CheckboxControl',
label: 'Show SQL Granularity Dropdown',
default: false,
description: 'Check to include SQL Granularity dropdown',
},

show_sqla_time_column: {
type: 'CheckboxControl',
label: 'Show SQL Time Column',
default: false,
description: 'Check to include Time Column dropdown',
},

show_druid_time_granularity: {
type: 'CheckboxControl',
label: 'Show Druid Granularity Dropdown',
default: false,
description: 'Check to include Druid Granularity dropdown',
},

show_druid_time_origin: {
type: 'CheckboxControl',
label: 'Show Druid Time Origin',
default: false,
description: 'Check to include Time Origin dropdown',
},

show_datatable: {
type: 'CheckboxControl',
label: t('Data Table'),
Expand Down
7 changes: 2 additions & 5 deletions superset/assets/javascripts/explore/stores/visTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -902,12 +902,9 @@ export const visTypes = {
controlSetRows: [
['groupby'],
['metric'],
],
},
{
label: 'Options',
controlSetRows: [
['date_filter', 'instant_filtering'],
['show_sqla_time_granularity', 'show_sqla_time_column'],
['show_druid_time_granularity', 'show_druid_time_origin'],
],
},
],
Expand Down
3 changes: 1 addition & 2 deletions superset/assets/javascripts/modules/superset.js
Original file line number Diff line number Diff line change
Expand Up @@ -208,8 +208,7 @@ const px = function (state) {
this.force = force;
}
const formDataExtra = Object.assign({}, formData);
const extraFilters = controller.effectiveExtraFilters(sliceId);
formDataExtra.filters = formDataExtra.filters.concat(extraFilters);
formDataExtra.extra_filters = controller.effectiveExtraFilters(sliceId);
controls.find('a.exploreChart').attr('href', getExploreUrl(formDataExtra));
controls.find('a.exportCSV').attr('href', getExploreUrl(formDataExtra, 'csv'));
token.find('img.loading').show();
Expand Down
6 changes: 6 additions & 0 deletions superset/assets/visualizations/filter_box.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
padding-top: 0;
}

.input-inline {
float: left;
display: inline-block;
padding-right: 3px;
}

ul.select2-results li.select2-highlighted div.filter_box{
color: black;
border-width: 1px;
Expand Down
150 changes: 114 additions & 36 deletions superset/assets/visualizations/filter_box.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,42 @@ import ReactDOM from 'react-dom';
import Select from 'react-select';
import { Button } from 'react-bootstrap';

import { TIME_CHOICES } from './constants';
import DateFilterControl from '../javascripts/explore/components/controls/DateFilterControl';
import ControlRow from '../javascripts/explore/components/ControlRow';
import Control from '../javascripts/explore/components/Control';
import controls from '../javascripts/explore/stores/controls';
import './filter_box.css';
import { t } from '../javascripts/locales';

// maps control names to their key in extra_filters
const timeFilterMap = {
since: '__from',
until: '__to',
granularity_sqla: '__time_col',
time_grain_sqla: '__time_grain',
druid_time_origin: '__time_origin',
granularity: '__granularity',
};
const propTypes = {
origSelectedValues: PropTypes.object,
instantFiltering: PropTypes.bool,
filtersChoices: PropTypes.object,
onChange: PropTypes.func,
showDateFilter: PropTypes.bool,
showSqlaTimeGrain: PropTypes.bool,
showSqlaTimeColumn: PropTypes.bool,
showDruidTimeGrain: PropTypes.bool,
showDruidTimeOrigin: PropTypes.bool,
datasource: PropTypes.object.isRequired,
};

const defaultProps = {
origSelectedValues: {},
onChange: () => {},
showDateFilter: false,
showSqlaTimeGrain: false,
showSqlaTimeColumn: false,
showDruidTimeGrain: false,
showDruidTimeOrigin: false,
instantFiltering: true,
};

Expand All @@ -34,46 +53,98 @@ class FilterBox extends React.Component {
hasChanged: false,
};
}
getControlData(controlName) {
const control = Object.assign({}, controls[controlName]);
const controlData = {
name: controlName,
key: `control-${controlName}`,
value: this.state.selectedValues[timeFilterMap[controlName]],
actions: { setControlValue: this.changeFilter.bind(this) },
};
Object.assign(control, controlData);
const mapFunc = control.mapStateToProps;
if (mapFunc) {
return Object.assign({}, control, mapFunc(this.props));
}
return control;
}
clickApply() {
this.props.onChange(Object.keys(this.state.selectedValues)[0], [], true, true);
this.setState({ hasChanged: false });
}
changeFilter(filter, options) {
const fltr = timeFilterMap[filter] || filter;
let vals = null;
if (options) {
if (options !== null) {
if (Array.isArray(options)) {
vals = options.map(opt => opt.value);
} else {
} else if (options.value) {
vals = options.value;
} else {
vals = options;
}
}
const selectedValues = Object.assign({}, this.state.selectedValues);
selectedValues[filter] = vals;
selectedValues[fltr] = vals;
this.setState({ selectedValues, hasChanged: true });
this.props.onChange(filter, vals, false, this.props.instantFiltering);
this.props.onChange(fltr, vals, false, this.props.instantFiltering);
}
render() {
let dateFilter;
const since = '__from';
const until = '__to';
if (this.props.showDateFilter) {
dateFilter = ['__from', '__to'].map((field) => {
const val = this.state.selectedValues[field];
const choices = TIME_CHOICES.slice();
if (!choices.includes(val)) {
choices.push(val);
}
const options = choices.map(s => ({ value: s, label: s }));
return (
<div className="m-b-5" key={field}>
{field.replace('__', '')}
<Select.Creatable
placeholder="Select"
options={options}
value={this.state.selectedValues[field]}
onChange={this.changeFilter.bind(this, field)}
dateFilter = (
<div className="row space-1">
<div className="col-lg-6 col-xs-12">
<DateFilterControl
name={since}
label="Since"
description="Select starting date"
onChange={this.changeFilter.bind(this, since)}
value={this.state.selectedValues[since]}
/>
</div>
);
});
<div className="col-lg-6 col-xs-12">
<DateFilterControl
name={until}
label="Until"
description="Select end date"
onChange={this.changeFilter.bind(this, until)}
value={this.state.selectedValues[until]}
/>
</div>
</div>
);
}
const datasourceFilters = [];
const sqlaFilters = [];
const druidFilters = [];
if (this.props.showSqlaTimeGrain) sqlaFilters.push('time_grain_sqla');
if (this.props.showSqlaTimeColumn) sqlaFilters.push('granularity_sqla');
if (this.props.showDruidTimeGrain) druidFilters.push('granularity');
if (this.props.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)} />
))}
/>,
);
}
// Add created options to filtersChoices, even though it doesn't exist,
// or these options will exist in query sql but invisible to end user.
Expand Down Expand Up @@ -126,19 +197,22 @@ class FilterBox extends React.Component {
);
});
return (
<div>
{dateFilter}
{filters}
{!this.props.instantFiltering &&
<Button
bsSize="small"
bsStyle="primary"
onClick={this.clickApply.bind(this)}
disabled={!this.state.hasChanged}
>
Apply
</Button>
}
<div className="scrollbar-container">
<div className="scrollbar-content">
{dateFilter}
{datasourceFilters}
{filters}
{!this.props.instantFiltering &&
<Button
bsSize="small"
bsStyle="primary"
onClick={this.clickApply.bind(this)}
disabled={!this.state.hasChanged}
>
Apply
</Button>
}
</div>
</div>
);
}
Expand All @@ -164,6 +238,10 @@ function filterBox(slice, payload) {
filtersChoices={filtersChoices}
onChange={slice.addFilter}
showDateFilter={fd.date_filter}
showSqlaTimeGrain={fd.show_sqla_time_granularity}
showSqlaTimeColumn={fd.show_sqla_time_column}
showDruidTimeGrain={fd.show_druid_time_granularity}
showDruidTimeOrigin={fd.show_druid_time_origin}
datasource={slice.datasource}
origSelectedValues={slice.getFilters() || {}}
instantFiltering={fd.instant_filtering}
Expand Down
28 changes: 28 additions & 0 deletions superset/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -665,3 +665,31 @@ def get_celery_app(config):
return _celery_app
_celery_app = celery.Celery(config_source=config.get('CELERY_CONFIG'))
return _celery_app


def merge_extra_filters(form_data):
# extra_filters are temporary/contextual filters that are external
# to the slice definition. We use those for dynamic interactive
# filters like the ones emitted by the "Filter Box" visualization
if form_data.get('extra_filters'):
# __form and __to are special extra_filters that target time
# boundaries. The rest of extra_filters are simple
# [column_name in list_of_values]. `__` prefix is there to avoid
# potential conflicts with column that would be named `from` or `to`
if 'filters' not in form_data:
form_data['filters'] = []
date_options = {
'__from': 'since',
'__to': 'until',
'__time_col': 'granularity_sqla',
'__time_grain': 'time_grain_sqla',
'__time_origin': 'druid_time_origin',
'__granularity': 'granularity',
}
for filtr in form_data['extra_filters']:
if date_options.get(filtr['col']): # merge date options
if filtr.get('val'):
form_data[date_options[filtr['col']]] = filtr['val']
else:
form_data['filters'] += [filtr] # merge col filters
del form_data['extra_filters']
7 changes: 6 additions & 1 deletion superset/views/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
sm, sql_lab, results_backend, security,
)
from superset.legacy import cast_form_data
from superset.utils import has_access, QueryStatus
from superset.utils import has_access, QueryStatus, merge_extra_filters
from superset.connectors.connector_registry import ConnectorRegistry
import superset.models.core as models
from superset.models.sql_lab import Query
Expand Down Expand Up @@ -1087,6 +1087,11 @@ def explore(self, datasource_type, datasource_id):
datasource_id,
datasource_type)

form_data['datasource'] = str(datasource_id) + '__' + datasource_type

# On explore, merge extra filters into the form data
merge_extra_filters(form_data)

standalone = request.args.get("standalone") == "true"
bootstrap_data = {
"can_add": slice_add_perm,
Expand Down
Loading

0 comments on commit 1fdcf10

Please sign in to comment.