Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feature: added datepicker and time granularity options to dashboard f…
Browse files Browse the repository at this point in the history
…ilter
Mogball committed Sep 21, 2017
1 parent 1cf634a commit 089cad4
Showing 9 changed files with 182 additions and 63 deletions.
5 changes: 3 additions & 2 deletions superset/assets/javascripts/dashboard/Dashboard.jsx
Original file line number Diff line number Diff line change
@@ -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;
}

@@ -205,7 +205,8 @@ export function dashboardContainer(dashboard, datasources, userid) {
return f;
},
addFilter(sliceId, col, vals, merge = true, refresh = true) {
if (this.getSlice(sliceId) && (col === '__from' || col === '__to' ||
if (this.getSlice(sliceId) &&
(col === '__from' || col === '__to' || col === '__time_col' || col === '__time_grain' ||
this.getSlice(sliceId).formData.groupby.indexOf(col) !== -1)) {
if (!(sliceId in this.filters)) {
this.filters[sliceId] = {};
5 changes: 0 additions & 5 deletions superset/assets/javascripts/explore/stores/visTypes.js
Original file line number Diff line number Diff line change
@@ -870,11 +870,6 @@ export const visTypes = {
controlSetRows: [
['groupby'],
['metric'],
],
},
{
label: 'Options',
controlSetRows: [
['date_filter', 'instant_filtering'],
],
},
3 changes: 1 addition & 2 deletions superset/assets/javascripts/modules/superset.js
Original file line number Diff line number Diff line change
@@ -204,8 +204,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();
6 changes: 6 additions & 0 deletions superset/assets/visualizations/filter_box.css
Original file line number Diff line number Diff line change
@@ -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;
94 changes: 74 additions & 20 deletions superset/assets/visualizations/filter_box.jsx
Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@ 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 './filter_box.css';

const propTypes = {
@@ -17,7 +17,6 @@ const propTypes = {
showDateFilter: PropTypes.bool,
datasource: PropTypes.object.isRequired,
};

const defaultProps = {
origSelectedValues: {},
onChange: () => {},
@@ -31,6 +30,8 @@ class FilterBox extends React.Component {
this.state = {
selectedValues: props.origSelectedValues,
hasChanged: false,
timeColumnOptions: props.datasource.granularity_sqla,
timeGrainOptions: props.datasource.time_grain_sqla,
};
}
clickApply() {
@@ -42,37 +43,88 @@ class FilterBox extends React.Component {
if (options) {
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;
this.setState({ selectedValues, hasChanged: true });
this.props.onChange(filter, vals, false, this.props.instantFiltering);
}
newColumnOption(option) {
const newColumnOptions = this.state.timeColumnOptions.slice();
newColumnOptions.push(option.value);
this.setState({ timeColumnOptions: newColumnOptions });
}
newGrainOption(option) {
const newGrainOptions = this.state.timeGrainOptions.slice();
newGrainOptions.push(option.value);
this.setState({ timeGrainOptions: newGrainOptions });
}
render() {
let dateFilter;
let timeColumnFilter;
let timeGrainFilter;
const since = '__from';
const until = '__to';
const timeCol = '__time_col';
const timeGrain = '__time_grain';
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>
);
timeColumnFilter = (
<div className="m-b-5">
Time Column
<Select.Creatable
placeholder={`Select (${this.state.timeColumnOptions.length})`}
key={timeCol}
options={this.state.timeColumnOptions.map(option =>
({ value: option[0], label: option[0] }),
)}
onChange={this.changeFilter.bind(this, timeCol)}
value={this.state.selectedValues[timeCol]}
onNewOptionClick={this.newColumnOption.bind(this)}
/>
</div>
);
timeGrainFilter = (
<div className="m-b-5">
Time Grain
<Select.Creatable
placeholder={`Select (${this.state.timeGrainOptions.length})`}
key={timeGrain}
options={this.state.timeGrainOptions.map(option =>
({ value: option[0], label: option[0] }),
)}
onChange={this.changeFilter.bind(this, timeGrain)}
value={this.state.selectedValues[timeGrain]}
onNewOptionClick={this.newGrainOption.bind(this)}
/>
</div>
);
}
// Add created options to filtersChoices, even though it doesn't exist,
// or these options will exist in query sql but invisible to end user.
@@ -126,6 +178,8 @@ class FilterBox extends React.Component {
return (
<div>
{dateFilter}
{timeColumnFilter}
{timeGrainFilter}
{filters}
{!this.props.instantFiltering &&
<Button
26 changes: 26 additions & 0 deletions superset/utils.py
Original file line number Diff line number Diff line change
@@ -665,3 +665,29 @@ 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',
}
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']
6 changes: 5 additions & 1 deletion superset/views/core.py
Original file line number Diff line number Diff line change
@@ -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
@@ -1078,6 +1078,10 @@ def explore(self, datasource_type, 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,
40 changes: 9 additions & 31 deletions superset/viz.py
Original file line number Diff line number Diff line change
@@ -29,7 +29,7 @@
from dateutil import relativedelta as rdelta

from superset import app, utils, cache, get_manifest_file
from superset.utils import DTTM_ALIAS
from superset.utils import DTTM_ALIAS, merge_extra_filters

config = app.config
stats_logger = config.get('STATS_LOGGER')
@@ -105,10 +105,6 @@ def get_df(self, query_obj=None):
df = df.fillna(0)
return df

def get_extra_filters(self):
extra_filters = self.form_data.get('extra_filters', [])
return {f['col']: f['val'] for f in extra_filters}

def query_obj(self):
"""Building a query object"""
form_data = self.form_data
@@ -125,29 +121,22 @@ def query_obj(self):
groupby.remove(DTTM_ALIAS)
is_timeseries = True

# 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
extra_filters = self.get_extra_filters()
# Add extra filters into the query form data
merge_extra_filters(form_data)

granularity = (
form_data.get("granularity") or form_data.get("granularity_sqla")
form_data.get("granularity") or
form_data.get("granularity_sqla")
)
limit = int(form_data.get("limit") or 0)
timeseries_limit_metric = form_data.get("timeseries_limit_metric")
row_limit = int(
form_data.get("row_limit") or config.get("ROW_LIMIT"))
row_limit = int(form_data.get("row_limit") or config.get("ROW_LIMIT"))

# default order direction
order_desc = form_data.get("order_desc", True)

# __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`
since = (
extra_filters.get('__from') or
form_data.get("since") or ''
)
since = form_data.get("since", "")
until = form_data.get("until", "now")

# Backward compatibility hack
since_words = since.split(' ')
@@ -157,7 +146,6 @@ def query_obj(self):

from_dttm = utils.parse_human_datetime(since)

until = extra_filters.get('__to') or form_data.get("until", "now")
to_dttm = utils.parse_human_datetime(until)
if from_dttm and to_dttm and from_dttm > to_dttm:
raise Exception(_("From date cannot be larger than to date"))
@@ -172,16 +160,6 @@ def query_obj(self):
'druid_time_origin': form_data.get("druid_time_origin", ''),
}
filters = form_data.get('filters', [])
for col, vals in self.get_extra_filters().items():
if not (col and vals) or col.startswith('__'):
continue
elif col in self.datasource.filterable_column_names:
# Quote values with comma to avoid conflict
filters += [{
'col': col,
'op': 'in',
'val': vals,
}]
d = {
'granularity': granularity,
'from_dttm': from_dttm,
60 changes: 58 additions & 2 deletions tests/utils_tests.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
from datetime import datetime, date, timedelta, time
from decimal import Decimal
from superset.utils import (
json_int_dttm_ser, json_iso_dttm_ser, base_json_conv, parse_human_timedelta, zlib_compress, zlib_decompress_to_string
json_int_dttm_ser,
json_iso_dttm_ser,
base_json_conv,
parse_human_timedelta,
zlib_compress,
zlib_decompress_to_string,
merge_extra_filters,
)
import unittest
import uuid

from mock import Mock, patch
from mock import patch
import numpy


@@ -52,3 +58,53 @@ def test_zlib_compression(self):
got_str = zlib_decompress_to_string(blob)
self.assertEquals(json_str, got_str)

def test_merge_extra_filters(self):
# does nothing if no extra filters
form_data = {'A': 1, 'B': 2, 'c': 'test'}
expected = {'A': 1, 'B': 2, 'c': 'test'}
merge_extra_filters(form_data)
self.assertEquals(form_data, expected)
# does nothing if empty extra_filters
form_data = {'A': 1, 'B': 2, 'c': 'test', 'extra_filters': []}
expected = {'A': 1, 'B': 2, 'c': 'test', 'extra_filters': []}
merge_extra_filters(form_data)
self.assertEquals(form_data, expected)
# copy over extra filters into empty filters
form_data = {'extra_filters': [
{'col': 'a', 'op': 'in', 'val': 'someval'},
{'col': 'B', 'op': '==', 'val': ['c1', 'c2']}
]}
expected = {'filters': [
{'col': 'a', 'op': 'in', 'val': 'someval'},
{'col': 'B', 'op': '==', 'val': ['c1', 'c2']}
]}
merge_extra_filters(form_data)
self.assertEquals(form_data, expected)
# adds extra filters to existing filters
form_data = {'extra_filters': [
{'col': 'a', 'op': 'in', 'val': 'someval'},
{'col': 'B', 'op': '==', 'val': ['c1', 'c2']}
], 'filters': [{'col': 'D', 'op': '!=', 'val': ['G1', 'g2']}]}
expected = {'filters': [
{'col': 'D', 'op': '!=', 'val': ['G1', 'g2']},
{'col': 'a', 'op': 'in', 'val': 'someval'},
{'col': 'B', 'op': '==', 'val': ['c1', 'c2']},
]}
merge_extra_filters(form_data)
self.assertEquals(form_data, expected)
# adds extra filters to existing filters and sets time options
form_data = {'extra_filters': [
{'col': '__from', 'op': 'in', 'val': '1 year ago'},
{'col': '__to', 'op': 'in', 'val': None},
{'col': '__time_col', 'op': 'in', 'val': 'birth_year'},
{'col': '__time_grain', 'op': 'in', 'val': 'years'},
{'col': 'A', 'op': 'like', 'val': 'hello'}
]}
expected = {
'filters': [{'col': 'A', 'op': 'like', 'val': 'hello'}],
'since': '1 year ago',
'granularity_sqla': 'birth_year',
'time_grain_sqla': 'years',
}
merge_extra_filters(form_data)
self.assertEquals(form_data, expected)

0 comments on commit 089cad4

Please sign in to comment.