Skip to content

Commit

Permalink
[FilterBox] dashboard date range filtering (apache#1165)
Browse files Browse the repository at this point in the history
* [FilterBox] dashboard date range filtering

* Addressing comments
  • Loading branch information
mistercrunch authored Sep 22, 2016
1 parent 8cb0bea commit 7115c54
Show file tree
Hide file tree
Showing 8 changed files with 121 additions and 52 deletions.
13 changes: 8 additions & 5 deletions caravel/assets/.eslintignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
node_modules/*
vendor/*
**/*{.,-}min.js
**/*.sh
coverage/**
dist/*
stylesheets/*
images/*
node_modules/*
node_modules*/*
spec/*
coverage/**
**/*{.,-}min.js
stylesheets/*
vendor/*
2 changes: 1 addition & 1 deletion caravel/assets/javascripts/modules/caravel.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ const px = function () {
const containerId = data.token + '_con';
const selector = '#' + containerId;
const container = $(selector);
const sliceId = data.sliceId;
const sliceId = data.slice_id;
let dttm = 0;
const stopwatch = function () {
dttm += 10;
Expand Down
16 changes: 7 additions & 9 deletions caravel/assets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@
"cover": "babel-node ./node_modules/.bin/istanbul cover _mocha -- --require spec/helpers/browser.js --recursive spec/**/*_spec.*",
"dev": "NODE_ENV=dev webpack -d --watch --colors --progress",
"prod": "NODE_ENV=production webpack -p --colors --progress",
"lint": "npm run --silent lint:js",
"lint:js": "eslint --ignore-path=.eslintignore --ext .js,.jsx ."
"lint": "eslint --ignore-path=.eslintignore --ext .js,.jsx ."
},
"repository": {
"type": "git",
Expand All @@ -39,6 +38,7 @@
"dependencies": {
"autobind-decorator": "^1.3.3",
"babel-cli": "^6.14.0",
"babel-preset-es2015": "^6.14.0",
"bootstrap": "^3.3.6",
"bootstrap-datepicker": "^1.6.0",
"brace": "^0.7.0",
Expand All @@ -62,11 +62,11 @@
"moments": "0.0.2",
"mustache": "^2.2.1",
"nvd3": "1.8.4",
"react": "^15.2.1",
"react": "^15.3.2",
"react-ace": "^3.4.1",
"react-bootstrap": "^0.30.3",
"react-bootstrap-table": "^2.3.8",
"react-dom": "^0.14.8",
"react-dom": "^15.3.2",
"react-draggable": "^2.1.2",
"react-grid-layout": "^0.13.1",
"react-map-gl": "^1.0.0-beta-10",
Expand All @@ -89,8 +89,7 @@
"babel": "^6.3.26",
"babel-core": "^6.10.4",
"babel-loader": "^6.2.4",
"babel-preset-airbnb": "^1.1.1",
"babel-preset-es2015": "^6.9.0",
"babel-preset-airbnb": "^2.0.0",
"babel-preset-react": "^6.11.1",
"chai": "^3.5.0",
"codeclimate-test-reporter": "^0.3.3",
Expand All @@ -99,7 +98,7 @@
"eslint": "^2.13.1",
"eslint-config-airbnb": "^9.0.1",
"eslint-plugin-import": "^1.11.1",
"eslint-plugin-jsx-a11y": "^2.0.1",
"eslint-plugin-jsx-a11y": "^1.2.0",
"eslint-plugin-react": "^5.2.2",
"exports-loader": "^0.6.3",
"istanbul": "^1.0.0-alpha",
Expand All @@ -110,8 +109,7 @@
"less": "^2.6.1",
"less-loader": "^2.2.2",
"mocha": "^2.4.5",
"react-addons-test-utils": "^0.14.8",
"react-dom": "^0.14.8",
"react-addons-test-utils": "^15.3.2",
"style-loader": "^0.13.0",
"transform-loader": "^0.2.3",
"url-loader": "^0.5.7",
Expand Down
10 changes: 10 additions & 0 deletions caravel/assets/visualizations/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export const TIME_CHOICES = [
'1 hour ago',
'12 hours ago',
'1 day ago',
'7 days ago',
'28 days ago',
'90 days ago',
'1 year ago',
];

3 changes: 3 additions & 0 deletions caravel/assets/visualizations/filter_box.css
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,6 @@ ul.select2-results div.filter_box{
.filter_box:hover {
z-index: 1000;
}
.m-b-5 {
margin-bottom: 5px;
}
79 changes: 57 additions & 22 deletions caravel/assets/visualizations/filter_box.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,21 @@ import Select from 'react-select';
import '../stylesheets/react-select/select.less';

import './filter_box.css';
import { TIME_CHOICES } from './constants.js';

const propTypes = {
filtersChoices: React.PropTypes.object,
onChange: React.PropTypes.func,
origSelectedValues: React.PropTypes.object,
showDateFilter: React.PropTypes.bool,
};

const defaultProps = {
filtersChoices: {},
onChange: () => {},
origSelectedValues: {},
showDateFilter: false,
};

class FilterBox extends React.Component {
constructor(props) {
Expand All @@ -17,15 +32,50 @@ class FilterBox extends React.Component {
selectedValues: props.origSelectedValues,
};
}
changeFilter(filter, options) {
let vals = null;
if (options) {
if (Array.isArray(options)) {
vals = options.map((opt) => opt.value);
} else {
vals = options.value;
}
}
const selectedValues = Object.assign({}, this.state.selectedValues);
selectedValues[filter] = vals;
this.setState({ selectedValues });
this.props.onChange(filter, vals);
}
render() {
let dateFilter;
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">
{field.replace('__', '')}
<Select.Creatable
options={options}
value={this.state.selectedValues[field]}
onChange={this.changeFilter.bind(this, field)}
/>
</div>
);
});
}
const filters = Object.keys(this.props.filtersChoices).map((filter) => {
const data = this.props.filtersChoices[filter];
const maxes = {};
maxes[filter] = d3.max(data, function (d) {
return d.metric;
});
return (
<div>
<div key={filter} className="m-b-5">
{filter}
<Select
placeholder={`Select [${filter}]`}
Expand All @@ -44,38 +94,21 @@ class FilterBox extends React.Component {
};
return { value: opt.id, label: opt.id, style };
})}
onChange={(selectedOptions) => {
let vals;
if (selectedOptions) {
vals = selectedOptions.map((opt) => opt.value);
} else {
vals = null;
}
const selectedValues = this.state.selectedValues;
selectedValues[filter] = vals;
this.setState({ selectedValues });
this.props.onChange(filter, vals);
}}
onChange={this.changeFilter.bind(this, filter)}
/>
</div>
);
});
return (
<div>
{dateFilter}
{filters}
</div>
);
}
}
FilterBox.propTypes = {
origSelectedValues: React.PropTypes.objectOf(React.PropTypes.array),
filtersChoices: React.PropTypes.objectOf(React.PropTypes.array),
onChange: React.PropTypes.function,
};
FilterBox.defaultProps = {
origSelectedValues: {},
onChange() {},
};
FilterBox.propTypes = propTypes;
FilterBox.defaultProps = defaultProps;

function filterBox(slice) {
const d3token = d3.select(slice.selector);
Expand All @@ -86,6 +119,7 @@ function filterBox(slice) {
// filter box should ignore the dashboard's filters
const url = slice.jsonEndpoint({ extraFilters: false });
$.getJSON(url, (payload) => {
const fd = payload.form_data;
const filtersChoices = {};
// Making sure the ordering of the fields matches the setting in the
// dropdown as it may have been shuffled while serialized to json
Expand All @@ -96,6 +130,7 @@ function filterBox(slice) {
<FilterBox
filtersChoices={filtersChoices}
onChange={slice.setFilter}
showDateFilter={fd.date_filter}
origSelectedValues={slice.getFilters() || {}}
/>,
document.getElementById(slice.containerId)
Expand Down
5 changes: 5 additions & 0 deletions caravel/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -749,6 +749,11 @@ def __init__(self, viz):
"description": _(
"Whether to display the time range interactive selector")
}),
'date_filter': (BetterBooleanField, {
"label": _("Date Filter"),
"default": False,
"description": _("Whether to include a time filter")
}),
'show_datatable': (BetterBooleanField, {
"label": _("Data Table"),
"default": False,
Expand Down
45 changes: 30 additions & 15 deletions caravel/viz.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,17 @@ def form(self):
def form_class(self):
return FormFactory(self).get_form()

def get_extra_filters(self):
extra_filters = self.form_data.get('extra_filters')
if not extra_filters:
return {}
extra_filters = json.loads(extra_filters)
# removing per-slice details
summary = {}
for flt in extra_filters.values():
summary.update(flt)
return summary

def query_filters(self, is_having_filter=False):
"""Processes the filters for the query"""
form_data = self.form_data
Expand All @@ -205,36 +216,39 @@ def query_filters(self, is_having_filter=False):
if col and op and eq is not None:
filters.append((col, op, eq))

if is_having_filter:
return filters

# Extra filters (coming from dashboard)
extra_filters = form_data.get('extra_filters')
if extra_filters and not is_having_filter:
extra_filters = json.loads(extra_filters)
for slice_filters in extra_filters.values():
for col, vals in slice_filters.items():
if not (col and vals):
continue
elif col in self.datasource.filterable_column_names:
# Quote values with comma to avoid conflict
vals = ["'%s'" % x if "," in x else x for x in vals]
filters += [(col, 'in', ",".join(vals))]
for col, vals in self.get_extra_filters().items():
if not (col and vals):
continue
elif col in self.datasource.filterable_column_names:
# Quote values with comma to avoid conflict
vals = ["'{}'".format(x) if "," in x else x for x in vals]
filters += [(col, 'in', ",".join(vals))]
return filters

def query_obj(self):
"""Building a query object"""
form_data = self.form_data
groupby = form_data.get("groupby") or []
metrics = form_data.get("metrics") or ['count']
granularity = \
extra_filters = self.get_extra_filters()
granularity = (
form_data.get("granularity") or form_data.get("granularity_sqla")
)
limit = int(form_data.get("limit", 0))
row_limit = int(
form_data.get("row_limit", config.get("ROW_LIMIT")))
since = form_data.get("since", "1 year ago")
since = (
extra_filters.get('__from') or form_data.get("since", "1 year ago")
)
from_dttm = utils.parse_human_datetime(since)
now = datetime.now()
if from_dttm > now:
from_dttm = now - (from_dttm - now)
until = form_data.get("until", "now")
until = extra_filters.get('__to') or form_data.get("until", "now")
to_dttm = utils.parse_human_datetime(until)
if from_dttm > to_dttm:
flasher("The date range doesn't seem right.", "danger")
Expand All @@ -245,7 +259,7 @@ def query_obj(self):
extras = {
'where': form_data.get("where", ''),
'having': form_data.get("having", ''),
'having_druid': self.query_filters(True),
'having_druid': self.query_filters(is_having_filter=True),
'time_grain_sqla': form_data.get("time_grain_sqla", ''),
'druid_time_origin': form_data.get("druid_time_origin", ''),
}
Expand Down Expand Up @@ -1634,6 +1648,7 @@ class FilterBoxViz(BaseViz):
fieldsets = ({
'label': None,
'fields': (
('date_filter', None),
'groupby',
'metric',
)
Expand Down

0 comments on commit 7115c54

Please sign in to comment.