superset-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From maximebeauche...@apache.org
Subject [incubator-superset] branch master updated: [Feature/Bugfix] Datepicker and time granularity options to dashboard filters (#3508)
Date Wed, 04 Oct 2017 19:43:31 GMT
This is an automated email from the ASF dual-hosted git repository.

maximebeauchemin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 7c936e7  [Feature/Bugfix] Datepicker and time granularity options to dashboard filters
(#3508)
7c936e7 is described below

commit 7c936e7f60a49fef05a0b77e966b63d527d983dd
Author: Jeff Niu <jeffniu22@gmail.com>
AuthorDate: Wed Oct 4 12:43:29 2017 -0700

    [Feature/Bugfix] Datepicker and time granularity options to dashboard filters (#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
---
 .../assets/javascripts/dashboard/Dashboard.jsx     |  11 +-
 .../assets/javascripts/explore/stores/controls.jsx |  29 ++++
 .../assets/javascripts/explore/stores/visTypes.js  |   7 +-
 superset/assets/javascripts/modules/superset.js    |   3 +-
 superset/assets/visualizations/filter_box.css      |   6 +
 superset/assets/visualizations/filter_box.jsx      | 150 ++++++++++++++++-----
 superset/utils.py                                  |  28 ++++
 superset/views/core.py                             |   7 +-
 superset/viz.py                                    |  40 ++----
 tests/utils_tests.py                               |  58 +++++++-
 10 files changed, 260 insertions(+), 79 deletions(-)

diff --git a/superset/assets/javascripts/dashboard/Dashboard.jsx b/superset/assets/javascripts/dashboard/Dashboard.jsx
index 5d150a2..f133424 100644
--- a/superset/assets/javascripts/dashboard/Dashboard.jsx
+++ b/superset/assets/javascripts/dashboard/Dashboard.jsx
@@ -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,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] = {};
         }
diff --git a/superset/assets/javascripts/explore/stores/controls.jsx b/superset/assets/javascripts/explore/stores/controls.jsx
index 57222a6..cd5d38a 100644
--- a/superset/assets/javascripts/explore/stores/controls.jsx
+++ b/superset/assets/javascripts/explore/stores/controls.jsx
@@ -562,6 +562,7 @@ export const controls = {
     mapStateToProps: state => ({
       choices: (state.datasource) ? state.datasource.granularity_sqla : [],
     }),
+    freeForm: true,
   },
 
   time_grain_sqla: {
@@ -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'),
diff --git a/superset/assets/javascripts/explore/stores/visTypes.js b/superset/assets/javascripts/explore/stores/visTypes.js
index 1d4d79b..ca8a63f 100644
--- a/superset/assets/javascripts/explore/stores/visTypes.js
+++ b/superset/assets/javascripts/explore/stores/visTypes.js
@@ -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'],
         ],
       },
     ],
diff --git a/superset/assets/javascripts/modules/superset.js b/superset/assets/javascripts/modules/superset.js
index 6570aba..9d58f02 100644
--- a/superset/assets/javascripts/modules/superset.js
+++ b/superset/assets/javascripts/modules/superset.js
@@ -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();
diff --git a/superset/assets/visualizations/filter_box.css b/superset/assets/visualizations/filter_box.css
index e1b72f3..b6938e8 100644
--- a/superset/assets/visualizations/filter_box.css
+++ b/superset/assets/visualizations/filter_box.css
@@ -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;
diff --git a/superset/assets/visualizations/filter_box.jsx b/superset/assets/visualizations/filter_box.jsx
index e465af5..9605830 100644
--- a/superset/assets/visualizations/filter_box.jsx
+++ b/superset/assets/visualizations/filter_box.jsx
@@ -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,
 };
 
@@ -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.
@@ -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>
     );
   }
@@ -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}
diff --git a/superset/utils.py b/superset/utils.py
index 956575b..fa72971 100644
--- a/superset/utils.py
+++ b/superset/utils.py
@@ -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']
diff --git a/superset/views/core.py b/superset/views/core.py
index 8cd3aac..57710bb 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -35,7 +35,7 @@ from superset import (
     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
@@ -1087,6 +1087,11 @@ class Superset(BaseSupersetView):
                 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,
diff --git a/superset/viz.py b/superset/viz.py
index 2283e8d..953d815 100644
--- a/superset/viz.py
+++ b/superset/viz.py
@@ -30,7 +30,7 @@ from six import string_types, PY3
 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')
@@ -124,10 +124,6 @@ class BaseViz(object):
             df = df.fillna(fillna)
         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
@@ -144,29 +140,22 @@ class BaseViz(object):
             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(' ')
@@ -176,7 +165,6 @@ class BaseViz(object):
 
         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"))
@@ -195,16 +183,6 @@ class BaseViz(object):
             '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,
diff --git a/tests/utils_tests.py b/tests/utils_tests.py
index 1e40d92..b642c43 100644
--- a/tests/utils_tests.py
+++ b/tests/utils_tests.py
@@ -7,6 +7,7 @@ from superset.utils import (
     parse_human_timedelta,
     zlib_compress,
     zlib_decompress_to_string,
+    merge_extra_filters,
     datetime_f,
     JSONEncodedDict,
     validate_json,
@@ -15,7 +16,7 @@ from superset.utils import (
 import unittest
 import uuid
 
-from mock import Mock, patch
+from mock import patch
 import numpy
 
 
@@ -61,6 +62,61 @@ class UtilsTestCase(unittest.TestCase):
         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'},
+            {'col': '__time_origin', 'op': 'in', 'val': 'now'},
+            {'col': '__granularity', 'op': 'in', 'val': '90 seconds'},
+        ]}
+        expected = {
+            'filters': [{'col': 'A', 'op': 'like', 'val': 'hello'}],
+            'since': '1 year ago',
+            'granularity_sqla': 'birth_year',
+            'time_grain_sqla': 'years',
+            'granularity': '90 seconds',
+            'druid_time_origin': 'now',
+        }
+        merge_extra_filters(form_data)
+        self.assertEquals(form_data, expected)
+
     def test_datetime_f(self):
         self.assertEquals(datetime_f(datetime(1990, 9, 21, 19, 11, 19, 626096)),
             '<nobr>1990-09-21T19:11:19.626096</nobr>')

-- 
To stop receiving notification emails like this one, please contact
['"commits@superset.apache.org" <commits@superset.apache.org>'].

Mime
View raw message