mirror of https://github.com/apache/superset.git
[explore] Improved time filters controls (#3371)
* Improved time filters controls * lint * Fix coverage * Allow empty dates
This commit is contained in:
parent
aff7a82664
commit
a47a512808
|
@ -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>
|
||||||
|
{info &&
|
||||||
|
<InfoTooltipWithTrigger
|
||||||
|
tooltip={info}
|
||||||
|
label="date-free-tooltip"
|
||||||
|
/>}
|
||||||
|
|
||||||
|
<i className={isSelected ? 'fa fa-check text-primary' : ''} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
PopoverSection.propTypes = propTypes;
|
|
@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
|
||||||
import BoundsControl from './controls/BoundsControl';
|
import BoundsControl from './controls/BoundsControl';
|
||||||
import CheckboxControl from './controls/CheckboxControl';
|
import CheckboxControl from './controls/CheckboxControl';
|
||||||
import DatasourceControl from './controls/DatasourceControl';
|
import DatasourceControl from './controls/DatasourceControl';
|
||||||
|
import DateFilterControl from './controls/DateFilterControl';
|
||||||
import FilterControl from './controls/FilterControl';
|
import FilterControl from './controls/FilterControl';
|
||||||
import HiddenControl from './controls/HiddenControl';
|
import HiddenControl from './controls/HiddenControl';
|
||||||
import SelectControl from './controls/SelectControl';
|
import SelectControl from './controls/SelectControl';
|
||||||
|
@ -16,6 +17,7 @@ const controlMap = {
|
||||||
BoundsControl,
|
BoundsControl,
|
||||||
CheckboxControl,
|
CheckboxControl,
|
||||||
DatasourceControl,
|
DatasourceControl,
|
||||||
|
DateFilterControl,
|
||||||
FilterControl,
|
FilterControl,
|
||||||
HiddenControl,
|
HiddenControl,
|
||||||
SelectControl,
|
SelectControl,
|
||||||
|
|
|
@ -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;
|
|
@ -86,3 +86,8 @@
|
||||||
.control-panel-section span.label {
|
.control-panel-section span.label {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
.input-inline {
|
||||||
|
float: left;
|
||||||
|
display: inline-block;
|
||||||
|
padding-right: 3px;
|
||||||
|
}
|
||||||
|
|
|
@ -557,37 +557,17 @@ export const controls = {
|
||||||
},
|
},
|
||||||
|
|
||||||
since: {
|
since: {
|
||||||
type: 'SelectControl',
|
type: 'DateFilterControl',
|
||||||
freeForm: true,
|
freeForm: true,
|
||||||
label: 'Since',
|
label: 'Until',
|
||||||
default: '7 days ago',
|
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: {
|
until: {
|
||||||
type: 'SelectControl',
|
type: 'DateFilterControl',
|
||||||
freeForm: true,
|
freeForm: true,
|
||||||
label: 'Until',
|
label: 'Until',
|
||||||
default: 'now',
|
default: 'now',
|
||||||
choices: formatSelectOptions([
|
|
||||||
'now',
|
|
||||||
'1 day ago',
|
|
||||||
'7 days ago',
|
|
||||||
'28 days ago',
|
|
||||||
'90 days ago',
|
|
||||||
'1 year ago',
|
|
||||||
]),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
max_bubble_size: {
|
max_bubble_size: {
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "mocha --require ignore-styles --compilers js:babel-core/register --require spec/helpers/browser.js --recursive spec/**/*_spec.*",
|
"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": "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",
|
"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",
|
"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-alert": "^1.0.14",
|
||||||
"react-bootstrap": "^0.31.2",
|
"react-bootstrap": "^0.31.2",
|
||||||
"react-bootstrap-table": "^3.1.7",
|
"react-bootstrap-table": "^3.1.7",
|
||||||
|
"react-datetime": "^2.9.0",
|
||||||
"react-dom": "^15.5.1",
|
"react-dom": "^15.5.1",
|
||||||
"react-gravatar": "^2.6.1",
|
"react-gravatar": "^2.6.1",
|
||||||
"react-grid-layout": "^0.14.4",
|
"react-grid-layout": "^0.14.4",
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -341,3 +341,18 @@ iframe {
|
||||||
.Select--multi .Select-value {
|
.Select--multi .Select-value {
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
.dimmed {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
.pointer {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.PopoverSection {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
.float-left {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
.float-right {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
|
@ -18,8 +18,8 @@ const config = {
|
||||||
theme: APP_DIR + '/javascripts/theme.js',
|
theme: APP_DIR + '/javascripts/theme.js',
|
||||||
common: APP_DIR + '/javascripts/common.js',
|
common: APP_DIR + '/javascripts/common.js',
|
||||||
addSlice: ['babel-polyfill', APP_DIR + '/javascripts/addSlice/index.jsx'],
|
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'],
|
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'],
|
sqllab: ['babel-polyfill', APP_DIR + '/javascripts/SqlLab/index.jsx'],
|
||||||
welcome: ['babel-polyfill', APP_DIR + '/javascripts/welcome.js'],
|
welcome: ['babel-polyfill', APP_DIR + '/javascripts/welcome.js'],
|
||||||
profile: ['babel-polyfill', APP_DIR + '/javascripts/profile/index.jsx'],
|
profile: ['babel-polyfill', APP_DIR + '/javascripts/profile/index.jsx'],
|
||||||
|
|
|
@ -61,10 +61,12 @@ class TableColumn(Model, BaseColumn):
|
||||||
|
|
||||||
def get_time_filter(self, start_dttm, end_dttm):
|
def get_time_filter(self, start_dttm, end_dttm):
|
||||||
col = self.sqla_col.label('__time')
|
col = self.sqla_col.label('__time')
|
||||||
return and_(
|
l = []
|
||||||
col >= text(self.dttm_sql_literal(start_dttm)),
|
if start_dttm:
|
||||||
col <= text(self.dttm_sql_literal(end_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):
|
def get_timestamp_expression(self, time_grain):
|
||||||
"""Getting the time component of the query"""
|
"""Getting the time component of the query"""
|
||||||
|
@ -364,7 +366,6 @@ class SqlaTable(Model, BaseDatasource):
|
||||||
columns=None,
|
columns=None,
|
||||||
form_data=None):
|
form_data=None):
|
||||||
"""Querying any sqla table from this common interface"""
|
"""Querying any sqla table from this common interface"""
|
||||||
|
|
||||||
template_kwargs = {
|
template_kwargs = {
|
||||||
'from_dttm': from_dttm,
|
'from_dttm': from_dttm,
|
||||||
'groupby': groupby,
|
'groupby': groupby,
|
||||||
|
|
|
@ -198,6 +198,8 @@ def parse_human_datetime(s):
|
||||||
>>> year_ago_1 == year_ago_2
|
>>> year_ago_1 == year_ago_2
|
||||||
True
|
True
|
||||||
"""
|
"""
|
||||||
|
if not s:
|
||||||
|
return None
|
||||||
try:
|
try:
|
||||||
dttm = parse(s)
|
dttm = parse(s)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|
|
@ -143,18 +143,14 @@ class BaseViz(object):
|
||||||
# potential conflicts with column that would be named `from` or `to`
|
# potential conflicts with column that would be named `from` or `to`
|
||||||
since = (
|
since = (
|
||||||
extra_filters.get('__from') or
|
extra_filters.get('__from') or
|
||||||
form_data.get("since") or
|
form_data.get("since")
|
||||||
config.get("SUPERSET_DEFAULT_SINCE", "1 year ago")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
from_dttm = utils.parse_human_datetime(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")
|
until = extra_filters.get('__to') or form_data.get("until", "now")
|
||||||
to_dttm = utils.parse_human_datetime(until)
|
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"))
|
raise Exception(_("From date cannot be larger than to date"))
|
||||||
|
|
||||||
# extras are used to query elements specific to a datasource type
|
# extras are used to query elements specific to a datasource type
|
||||||
|
|
Loading…
Reference in New Issue