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: [explore] Improved time filters controls (#3371)
Date Mon, 28 Aug 2017 16:16:29 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 a47a512  [explore] Improved time filters controls (#3371)
a47a512 is described below

commit a47a512808bb4032351942a2f9ebff25a083149f
Author: Maxime Beauchemin <maximebeauchemin@gmail.com>
AuthorDate: Mon Aug 28 09:16:23 2017 -0700

    [explore] Improved time filters controls (#3371)
    
    * Improved time filters controls
    
    * lint
    
    * Fix coverage
    
    * Allow empty dates
---
 .../javascripts/components/PopoverSection.jsx      |  31 +++
 .../javascripts/explore/components/Control.jsx     |   2 +
 .../components/controls/DateFilterControl.jsx      | 218 +++++++++++++++++++++
 superset/assets/javascripts/explore/main.css       |   5 +
 .../assets/javascripts/explore/stores/controls.jsx |  26 +--
 superset/assets/package.json                       |   3 +-
 .../javascripts/components/PopoverSection_spec.jsx |  34 ++++
 .../explore/components/DateFilterControl_spec.jsx  |  63 ++++++
 superset/assets/stylesheets/superset.less          |  15 ++
 superset/assets/webpack.config.js                  |   2 +-
 superset/connectors/sqla/models.py                 |  11 +-
 superset/utils.py                                  |   2 +
 superset/viz.py                                    |   8 +-
 13 files changed, 384 insertions(+), 36 deletions(-)

diff --git a/superset/assets/javascripts/components/PopoverSection.jsx b/superset/assets/javascripts/components/PopoverSection.jsx
new file mode 100644
index 0000000..1490366
--- /dev/null
+++ b/superset/assets/javascripts/components/PopoverSection.jsx
@@ -0,0 +1,31 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import InfoTooltipWithTrigger from './InfoTooltipWithTrigger';
+
+const propTypes = {
+  title: PropTypes.string.isRequired,
+  isSelected: PropTypes.bool.isRequired,
+  onSelect: PropTypes.func.isRequired,
+  info: PropTypes.string,
+  children: PropTypes.node.isRequired,
+};
+
+export default function PopoverSection({ title, isSelected, children, onSelect, info }) {
+  return (
+    <div className={'PopoverSection ' + (!isSelected ? 'dimmed' : '')}>
+      <div onClick={onSelect} className="pointer">
+        <strong>{title}</strong> &nbsp;
+        {info &&
+          <InfoTooltipWithTrigger
+            tooltip={info}
+            label="date-free-tooltip"
+          />}
+        &nbsp;
+        <i className={isSelected ? 'fa fa-check text-primary' : ''} />
+      </div>
+      <div>
+        {children}
+      </div>
+    </div>);
+}
+PopoverSection.propTypes = propTypes;
diff --git a/superset/assets/javascripts/explore/components/Control.jsx b/superset/assets/javascripts/explore/components/Control.jsx
index 4ff5af0..5c644c3 100644
--- a/superset/assets/javascripts/explore/components/Control.jsx
+++ b/superset/assets/javascripts/explore/components/Control.jsx
@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
 import BoundsControl from './controls/BoundsControl';
 import CheckboxControl from './controls/CheckboxControl';
 import DatasourceControl from './controls/DatasourceControl';
+import DateFilterControl from './controls/DateFilterControl';
 import FilterControl from './controls/FilterControl';
 import HiddenControl from './controls/HiddenControl';
 import SelectControl from './controls/SelectControl';
@@ -16,6 +17,7 @@ const controlMap = {
   BoundsControl,
   CheckboxControl,
   DatasourceControl,
+  DateFilterControl,
   FilterControl,
   HiddenControl,
   SelectControl,
diff --git a/superset/assets/javascripts/explore/components/controls/DateFilterControl.jsx
b/superset/assets/javascripts/explore/components/controls/DateFilterControl.jsx
new file mode 100644
index 0000000..d1bc335
--- /dev/null
+++ b/superset/assets/javascripts/explore/components/controls/DateFilterControl.jsx
@@ -0,0 +1,218 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import {
+  Button, ButtonGroup, FormControl, InputGroup,
+  Label, OverlayTrigger, Popover, Glyphicon,
+} from 'react-bootstrap';
+import Select from 'react-select';
+import Datetime from 'react-datetime';
+import 'react-datetime/css/react-datetime.css';
+import moment from 'moment';
+
+import ControlHeader from '../ControlHeader';
+import PopoverSection from '../../../components/PopoverSection';
+
+const RELATIVE_TIME_OPTIONS = ['ago', 'from now'];
+const TIME_GRAIN_OPTIONS = ['seconds', 'minutes', 'days', 'weeks', 'months', 'years'];
+
+const propTypes = {
+  animation: PropTypes.bool,
+  name: PropTypes.string.isRequired,
+  label: PropTypes.string,
+  description: PropTypes.string,
+  onChange: PropTypes.func,
+  value: PropTypes.string.isRequired,
+  height: PropTypes.number,
+};
+
+const defaultProps = {
+  animation: true,
+  onChange: () => {},
+  value: null,
+};
+
+export default class DateFilterControl extends React.Component {
+  constructor(props) {
+    super(props);
+    const words = props.value.split(' ');
+    this.state = {
+      num: '7',
+      grain: 'days',
+      rel: 'ago',
+      dttm: '',
+      type: 'free',
+      free: '',
+    };
+    if (words.length >= 3 && RELATIVE_TIME_OPTIONS.indexOf(words[2]) >= 0)
{
+      this.state.num = words[0];
+      this.state.grain = words[1];
+      this.state.rel = words[2];
+      this.state.type = 'rel';
+    } else if (moment(props.value).isValid()) {
+      this.state.dttm = props.value;
+      this.state.type = 'fix';
+    } else {
+      this.state.free = props.value;
+      this.state.type = 'free';
+    }
+  }
+  onControlChange(target, opt) {
+    this.setState({ [target]: opt.value }, this.onChange);
+  }
+  onNumberChange(event) {
+    this.setState({ num: event.target.value }, this.onChange);
+  }
+  onChange() {
+    let val;
+    if (this.state.type === 'rel') {
+      val = `${this.state.num} ${this.state.grain} ${this.state.rel}`;
+    } else if (this.state.type === 'fix') {
+      val = this.state.dttm;
+    } else if (this.state.type === 'free') {
+      val = this.state.free;
+    }
+    this.props.onChange(val);
+  }
+  onFreeChange(event) {
+    this.setState({ free: event.target.value }, this.onChange);
+  }
+  setType(type) {
+    this.setState({ type }, this.onChange);
+  }
+  setValue(val) {
+    this.setState({ type: 'free', free: val }, this.onChange);
+    this.close();
+  }
+  setDatetime(dttm) {
+    this.setState({ dttm: dttm.format().substring(0, 19) }, this.onChange);
+  }
+  close() {
+    this.refs.trigger.hide();
+  }
+  renderPopover() {
+    return (
+      <Popover id="filter-popover">
+        <div style={{ width: '240px' }}>
+          <PopoverSection
+            title="Fixed"
+            isSelected={this.state.type === 'fix'}
+            onSelect={this.setType.bind(this, 'fix')}
+          >
+            <InputGroup bsSize="small">
+              <InputGroup.Addon>
+                <Glyphicon glyph="calendar" />
+              </InputGroup.Addon>
+              <Datetime
+                inputProps={{ className: 'form-control input-sm' }}
+                dateFormat="YYYY-MM-DD"
+                defaultValue={this.state.dttm}
+                onFocus={this.setType.bind(this, 'fix')}
+                onChange={this.setDatetime.bind(this)}
+                timeFormat="h:mm:ss"
+              />
+            </InputGroup>
+          </PopoverSection>
+          <PopoverSection
+            title="Relative"
+            isSelected={this.state.type === 'rel'}
+            onSelect={this.setType.bind(this, 'rel')}
+          >
+            <div className="clearfix">
+              <div style={{ width: '50px' }} className="input-inline">
+                <FormControl
+                  onFocus={this.setType.bind(this, 'rel')}
+                  value={this.state.num}
+                  onChange={this.onNumberChange.bind(this)}
+                  bsSize="small"
+                />
+              </div>
+              <div style={{ width: '95px' }} className="input-inline">
+                <Select
+                  onFocus={this.setType.bind(this, 'rel')}
+                  value={this.state.grain}
+                  clearable={false}
+                  options={TIME_GRAIN_OPTIONS.map(s => ({ label: s, value: s }))}
+                  onChange={this.onControlChange.bind(this, 'grain')}
+                />
+              </div>
+              <div style={{ width: '95px' }} className="input-inline">
+                <Select
+                  value={this.state.rel}
+                  onFocus={this.setType.bind(this, 'rel')}
+                  clearable={false}
+                  options={RELATIVE_TIME_OPTIONS.map(s => ({ label: s, value: s }))}
+                  onChange={this.onControlChange.bind(this, 'rel')}
+                />
+              </div>
+            </div>
+          </PopoverSection>
+          <PopoverSection
+            title="Free form"
+            isSelected={this.state.type === 'free'}
+            onSelect={this.setType.bind(this, 'free')}
+            info={
+              'Superset supports smart date parsing. Strings like `last sunday` or ' +
+              '`last october` can be used.'
+            }
+          >
+            <FormControl
+              onFocus={this.setType.bind(this, 'free')}
+              value={this.state.free}
+              onChange={this.onFreeChange.bind(this)}
+              bsSize="small"
+            />
+          </PopoverSection>
+          <div className="clearfix">
+            <Button
+              bsSize="small"
+              className="float-left ok"
+              bsStyle="primary"
+              onClick={this.close.bind(this)}
+            >
+              Ok
+            </Button>
+            <ButtonGroup
+              className="float-right"
+            >
+              <Button
+                bsSize="small"
+                onClick={this.setValue.bind(this, 'now')}
+              >
+                now
+              </Button>
+              <Button
+                bsSize="small"
+                onClick={this.setValue.bind(this, '')}
+              >
+                clear
+              </Button>
+            </ButtonGroup>
+          </div>
+        </div>
+      </Popover>
+    );
+  }
+  render() {
+    return (
+      <div>
+        <ControlHeader {...this.props} />
+        <OverlayTrigger
+          animation={this.props.animation}
+          container={document.body}
+          trigger="click"
+          rootClose
+          ref="trigger"
+          placement="right"
+          overlay={this.renderPopover()}
+        >
+          <Label style={{ cursor: 'pointer' }}>
+            {this.props.value.replace('T00:00:00', '') || '∞'}
+          </Label>
+        </OverlayTrigger>
+      </div>
+    );
+  }
+}
+
+DateFilterControl.propTypes = propTypes;
+DateFilterControl.defaultProps = defaultProps;
diff --git a/superset/assets/javascripts/explore/main.css b/superset/assets/javascripts/explore/main.css
index 6d1a7bd..b068497 100644
--- a/superset/assets/javascripts/explore/main.css
+++ b/superset/assets/javascripts/explore/main.css
@@ -86,3 +86,8 @@
 .control-panel-section span.label {
   display: inline-block;
 }
+.input-inline {
+  float: left;
+  display: inline-block;
+  padding-right: 3px;
+}
diff --git a/superset/assets/javascripts/explore/stores/controls.jsx b/superset/assets/javascripts/explore/stores/controls.jsx
index ce5ee43..554554c 100644
--- a/superset/assets/javascripts/explore/stores/controls.jsx
+++ b/superset/assets/javascripts/explore/stores/controls.jsx
@@ -557,37 +557,17 @@ export const controls = {
   },
 
   since: {
-    type: 'SelectControl',
+    type: 'DateFilterControl',
     freeForm: true,
-    label: 'Since',
+    label: 'Until',
     default: '7 days ago',
-    choices: formatSelectOptions([
-      '1 hour ago',
-      '12 hours ago',
-      '1 day ago',
-      '7 days ago',
-      '28 days ago',
-      '90 days ago',
-      '1 year ago',
-      '100 year ago',
-    ]),
-    description: 'Timestamp from filter. This supports free form typing and ' +
-    'natural language as in `1 day ago`, `28 days` or `3 years`',
   },
 
   until: {
-    type: 'SelectControl',
+    type: 'DateFilterControl',
     freeForm: true,
     label: 'Until',
     default: 'now',
-    choices: formatSelectOptions([
-      'now',
-      '1 day ago',
-      '7 days ago',
-      '28 days ago',
-      '90 days ago',
-      '1 year ago',
-    ]),
   },
 
   max_bubble_size: {
diff --git a/superset/assets/package.json b/superset/assets/package.json
index 68b9e7c..3359006 100644
--- a/superset/assets/package.json
+++ b/superset/assets/package.json
@@ -9,7 +9,7 @@
   },
   "scripts": {
     "test": "mocha --require ignore-styles --compilers js:babel-core/register --require spec/helpers/browser.js
--recursive spec/**/*_spec.*",
-    "cover": "babel-node node_modules/.bin/babel-istanbul cover _mocha -- --require spec/helpers/browser.js
--recursive spec/**/*_spec.*",
+    "cover": "babel-node node_modules/.bin/babel-istanbul cover _mocha -- --require ignore-styles
spec/helpers/browser.js --recursive spec/**/*_spec.*",
     "dev": "NODE_ENV=dev webpack --watch --colors --progress --debug --output-pathinfo --devtool
inline-source-map",
     "dev-fast": "NODE_ENV=dev webpack --watch --colors --progress --debug --output-pathinfo
--devtool eval-cheap-source-map",
     "prod": "NODE_ENV=production node --max_old_space_size=4096 ./node_modules/webpack/bin/webpack.js
-p --colors --progress",
@@ -68,6 +68,7 @@
     "react-alert": "^1.0.14",
     "react-bootstrap": "^0.31.2",
     "react-bootstrap-table": "^3.1.7",
+    "react-datetime": "^2.9.0",
     "react-dom": "^15.5.1",
     "react-gravatar": "^2.6.1",
     "react-grid-layout": "^0.14.4",
diff --git a/superset/assets/spec/javascripts/components/PopoverSection_spec.jsx b/superset/assets/spec/javascripts/components/PopoverSection_spec.jsx
new file mode 100644
index 0000000..b9a638c
--- /dev/null
+++ b/superset/assets/spec/javascripts/components/PopoverSection_spec.jsx
@@ -0,0 +1,34 @@
+import React from 'react';
+import { expect } from 'chai';
+import { describe, it } from 'mocha';
+import { shallow } from 'enzyme';
+
+import PopoverSection from '../../../javascripts/components/PopoverSection';
+
+describe('PopoverSection', () => {
+  const defaultProps = {
+    title: 'Section Title',
+    isSelected: true,
+    onSelect: () => {},
+    info: 'info section',
+    children: <div />,
+  };
+
+  let wrapper;
+  const factory = (overrideProps) => {
+    const props = Object.assign({}, defaultProps, overrideProps || {});
+    return shallow(<PopoverSection {...props} />);
+  };
+  beforeEach(() => {
+    wrapper = factory();
+  });
+  it('renders', () => {
+    expect(React.isValidElement(<PopoverSection {...defaultProps} />)).to.equal(true);
+  });
+  it('is show an icon when selected', () => {
+    expect(wrapper.find('.fa-check')).to.have.length(1);
+  });
+  it('is show no icon when not selected', () => {
+    expect(factory({ isSelected: false }).find('.fa-check')).to.have.length(0);
+  });
+});
diff --git a/superset/assets/spec/javascripts/explore/components/DateFilterControl_spec.jsx
b/superset/assets/spec/javascripts/explore/components/DateFilterControl_spec.jsx
new file mode 100644
index 0000000..e15356e
--- /dev/null
+++ b/superset/assets/spec/javascripts/explore/components/DateFilterControl_spec.jsx
@@ -0,0 +1,63 @@
+/* eslint-disable no-unused-expressions */
+import React from 'react';
+import sinon from 'sinon';
+import { expect } from 'chai';
+import { describe, it, beforeEach } from 'mocha';
+import { shallow } from 'enzyme';
+import { Button } from 'react-bootstrap';
+
+import DateFilterControl from '../../../../javascripts/explore/components/controls/DateFilterControl';
+import ControlHeader from '../../../../javascripts/explore/components/ControlHeader';
+
+const defaultProps = {
+  animation: false,
+  name: 'date',
+  onChange: sinon.spy(),
+  value: '90 days ago',
+  label: 'date',
+};
+
+describe('DateFilterControl', () => {
+  let wrapper;
+
+  beforeEach(() => {
+    wrapper = shallow(<DateFilterControl {...defaultProps} />);
+  });
+
+  it('renders a ControlHeader', () => {
+    const controlHeader = wrapper.find(ControlHeader);
+    expect(controlHeader).to.have.lengthOf(1);
+  });
+  it('renders 3 Buttons', () => {
+    const label = wrapper.find('.label').first();
+    label.simulate('click');
+    setTimeout(() => {
+      expect(wrapper.find(Button)).to.have.length(3);
+    }, 10);
+  });
+  it('loads the right state', () => {
+    const label = wrapper.find('.label').first();
+    label.simulate('click');
+    setTimeout(() => {
+      expect(wrapper.state().num).to.equal('90');
+    }, 10);
+  });
+  it('renders 2 dimmed sections', () => {
+    const label = wrapper.find('.label').first();
+    label.simulate('click');
+    setTimeout(() => {
+      expect(wrapper.find(Button)).to.have.length(3);
+    }, 10);
+  });
+  it('opens and closes', () => {
+    const label = wrapper.find('.label').first();
+    label.simulate('click');
+    setTimeout(() => {
+      expect(wrapper.find('.popover')).to.have.length(1);
+      expect(wrapper.find('.ok')).first().simulate('click');
+      setTimeout(() => {
+        expect(wrapper.find('.popover')).to.have.length(0);
+      }, 10);
+    }, 10);
+  });
+});
diff --git a/superset/assets/stylesheets/superset.less b/superset/assets/stylesheets/superset.less
index 5c0c4d6..89b5cbc 100644
--- a/superset/assets/stylesheets/superset.less
+++ b/superset/assets/stylesheets/superset.less
@@ -341,3 +341,18 @@ iframe {
 .Select--multi .Select-value {
   line-height: 1.2;
 }
+.dimmed {
+  opacity: 0.5;
+}
+.pointer {
+  cursor: pointer;
+}
+.PopoverSection {
+  padding-bottom: 10px;
+}
+.float-left {
+  float: left;
+}
+.float-right {
+  float: right;
+}
diff --git a/superset/assets/webpack.config.js b/superset/assets/webpack.config.js
index 10e41c9..8e3177a 100644
--- a/superset/assets/webpack.config.js
+++ b/superset/assets/webpack.config.js
@@ -18,8 +18,8 @@ const config = {
     theme: APP_DIR + '/javascripts/theme.js',
     common: APP_DIR + '/javascripts/common.js',
     addSlice: ['babel-polyfill', APP_DIR + '/javascripts/addSlice/index.jsx'],
-    dashboard: ['babel-polyfill', APP_DIR + '/javascripts/dashboard/Dashboard.jsx'],
     explore: ['babel-polyfill', APP_DIR + '/javascripts/explore/index.jsx'],
+    dashboard: ['babel-polyfill', APP_DIR + '/javascripts/dashboard/Dashboard.jsx'],
     sqllab: ['babel-polyfill', APP_DIR + '/javascripts/SqlLab/index.jsx'],
     welcome: ['babel-polyfill', APP_DIR + '/javascripts/welcome.js'],
     profile: ['babel-polyfill', APP_DIR + '/javascripts/profile/index.jsx'],
diff --git a/superset/connectors/sqla/models.py b/superset/connectors/sqla/models.py
index 6dccdf8..96ef575 100644
--- a/superset/connectors/sqla/models.py
+++ b/superset/connectors/sqla/models.py
@@ -61,10 +61,12 @@ class TableColumn(Model, BaseColumn):
 
     def get_time_filter(self, start_dttm, end_dttm):
         col = self.sqla_col.label('__time')
-        return and_(
-            col >= text(self.dttm_sql_literal(start_dttm)),
-            col <= text(self.dttm_sql_literal(end_dttm)),
-        )
+        l = []
+        if start_dttm:
+            l.append(col >= text(self.dttm_sql_literal(start_dttm)))
+        if end_dttm:
+            l.append(col <= text(self.dttm_sql_literal(end_dttm)))
+        return and_(*l)
 
     def get_timestamp_expression(self, time_grain):
         """Getting the time component of the query"""
@@ -364,7 +366,6 @@ class SqlaTable(Model, BaseDatasource):
             columns=None,
             form_data=None):
         """Querying any sqla table from this common interface"""
-
         template_kwargs = {
             'from_dttm': from_dttm,
             'groupby': groupby,
diff --git a/superset/utils.py b/superset/utils.py
index cd70e0c..fe43bc4 100644
--- a/superset/utils.py
+++ b/superset/utils.py
@@ -198,6 +198,8 @@ def parse_human_datetime(s):
     >>> year_ago_1 == year_ago_2
     True
     """
+    if not s:
+        return None
     try:
         dttm = parse(s)
     except Exception:
diff --git a/superset/viz.py b/superset/viz.py
index 7c88aa1..b93f874 100644
--- a/superset/viz.py
+++ b/superset/viz.py
@@ -143,18 +143,14 @@ class BaseViz(object):
         # potential conflicts with column that would be named `from` or `to`
         since = (
             extra_filters.get('__from') or
-            form_data.get("since") or
-            config.get("SUPERSET_DEFAULT_SINCE", "1 year ago")
+            form_data.get("since")
         )
 
         from_dttm = utils.parse_human_datetime(since)
-        now = datetime.now()
-        if from_dttm > now:
-            from_dttm = now - (from_dttm - now)
 
         until = extra_filters.get('__to') or form_data.get("until", "now")
         to_dttm = utils.parse_human_datetime(until)
-        if from_dttm > to_dttm:
+        if from_dttm and to_dttm and from_dttm > to_dttm:
             raise Exception(_("From date cannot be larger than to date"))
 
         # extras are used to query elements specific to a datasource type

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

Mime
View raw message