Update time filter to use `react-datetime` (#5819)

* Update time filter to use react-datetime

* Clean up code

* Many small fixes and improvements

* Fix small things
This commit is contained in:
Beto Dealmeida 2018-09-10 10:27:16 -07:00 committed by Maxime Beauchemin
parent 6d573724aa
commit 6c9be9d67b
6 changed files with 179 additions and 19625 deletions

File diff suppressed because it is too large Load Diff

View File

@ -92,12 +92,11 @@
"react-addons-css-transition-group": "^15.6.0",
"react-addons-shallow-compare": "^15.4.2",
"react-bootstrap": "^0.31.5",
"react-bootstrap-datetimepicker": "0.0.22",
"react-bootstrap-dialog": "^0.10.0",
"react-bootstrap-slider": "2.1.5",
"react-bootstrap-table": "^4.3.1",
"react-color": "^2.13.8",
"react-datetime": "2.14.0",
"react-datetime": "^2.14.0",
"react-dnd": "^2.5.4",
"react-dnd-html5-backend": "^2.5.4",
"react-dom": "^15.6.2",

View File

@ -0,0 +1,3 @@
.rdtPicker table {
font-size: 12;
}

View File

@ -5,6 +5,7 @@ import {
DropdownButton,
FormControl,
FormGroup,
Glyphicon,
InputGroup,
Label,
MenuItem,
@ -14,11 +15,11 @@ import {
Tab,
Tabs,
} from 'react-bootstrap';
import Datetime from 'react-datetime';
import 'react-datetime/css/react-datetime.css';
import DateTimeField from 'react-bootstrap-datetimepicker';
import 'react-bootstrap-datetimepicker/css/bootstrap-datetimepicker.min.css';
import moment from 'moment';
import './DateFilterControl.css';
import ControlHeader from '../ControlHeader';
import { t } from '../../../locales';
import PopoverSection from '../../../components/PopoverSection';
@ -45,7 +46,6 @@ const TIME_GRAIN_OPTIONS = ['seconds', 'minutes', 'hours', 'days', 'weeks', 'mon
const MOMENT_FORMAT = 'YYYY-MM-DD[T]HH:mm:ss';
const DEFAULT_SINCE = moment().startOf('day').subtract(7, 'days').format(MOMENT_FORMAT);
const DEFAULT_UNTIL = moment().startOf('day').format(MOMENT_FORMAT);
const INVALID_DATE_MESSAGE = 'Invalid date';
const SEPARATOR = ' : ';
const FREEFORM_TOOLTIP = t(
'Superset supports smart date parsing. Strings like `last sunday` or ' +
@ -68,19 +68,60 @@ const defaultProps = {
value: 'Last week',
};
function isFreeform(s) {
function isValidMoment(s) {
/* Moment sometimes consider invalid dates as valid, eg, "10 years ago" gets
* parsed as "Fri Jan 01 2010 00:00:00" local time. This function does a
* better check by comparing a string with a parse/format roundtrip.
*/
return (s !== moment(s, MOMENT_FORMAT).format(MOMENT_FORMAT));
return (s === moment(s, MOMENT_FORMAT).format(MOMENT_FORMAT));
}
function getStateFromSeparator(value) {
const [since, until] = value.split(SEPARATOR, 2);
return { since, until, type: TYPES.CUSTOM_START_END };
}
function getStateFromCommonTimeFrame(value) {
const units = value.split(' ')[1] + 's';
return {
type: TYPES.DEFAULTS,
common: value,
since: moment().startOf('day').subtract(1, units).format(MOMENT_FORMAT),
until: moment().startOf('day').format(MOMENT_FORMAT),
};
}
function getStateFromCustomRange(value) {
const [rel, num, grain] = value.split(' ', 3);
let since;
let until;
if (rel === RELATIVE_TIME_OPTIONS.LAST) {
until = moment().startOf('day').format(MOMENT_FORMAT);
since = moment()
.startOf('day')
.subtract(num, grain)
.format(MOMENT_FORMAT);
} else {
until = moment()
.startOf('day')
.add(num, grain)
.format(MOMENT_FORMAT);
since = moment().startOf('day').format(MOMENT_FORMAT);
}
return {
type: TYPES.CUSTOM_RANGE,
common: null,
rel,
num,
grain,
since,
until,
};
}
export default class DateFilterControl extends React.Component {
constructor(props) {
super(props);
const value = props.value || defaultProps.value;
this.state = {
type: TYPES.DEFAULTS,
@ -95,29 +136,36 @@ export default class DateFilterControl extends React.Component {
// distinct start/end values, either ISO or freeform
since: DEFAULT_SINCE,
until: DEFAULT_UNTIL,
freeformInputs: {},
// react-datetime has a `closeOnSelect` prop, but it's buggy... so we
// handle the calendar visibility here ourselves
showSinceCalendar: false,
showUntilCalendar: false,
sinceViewMode: 'days',
untilViewMode: 'days',
};
if (value.indexOf(SEPARATOR) >= 0) {
this.state.type = TYPES.CUSTOM_START_END;
[this.state.since, this.state.until] = value.split(SEPARATOR, 2);
} else {
this.state.type = TYPES.DEFAULTS;
if (COMMON_TIME_FRAMES.indexOf(value) >= 0) {
this.state.common = value;
} else {
this.state.common = null;
[this.state.rel, this.state.num, this.state.grain] = value.split(' ', 3);
}
}
this.state.freeformInputs.since = isFreeform(this.state.since);
this.state.freeformInputs.until = isFreeform(this.state.until);
// We need direct access to the state of the `DateTimeField` component
this.dateTimeFieldRefs = {};
this.close = this.close.bind(this);
this.handleClick = this.handleClick.bind(this);
this.isValidSince = this.isValidSince.bind(this);
this.isValidUntil = this.isValidUntil.bind(this);
this.onEnter = this.onEnter.bind(this);
this.renderInput = this.renderInput.bind(this);
this.setCustomRange = this.setCustomRange.bind(this);
this.setCustomStartEnd = this.setCustomStartEnd.bind(this);
this.setTypeCustomRange = this.setTypeCustomRange.bind(this);
this.setTypeCustomStartEnd = this.setTypeCustomStartEnd.bind(this);
this.toggleCalendar = this.toggleCalendar.bind(this);
}
componentDidMount() {
const value = this.props.value;
if (value.indexOf(SEPARATOR) >= 0) {
this.state = { ...this.state, ...getStateFromSeparator(value) };
} else if (COMMON_TIME_FRAMES.indexOf(value) >= 0) {
this.state = { ...this.state, ...getStateFromCommonTimeFrame(value) };
} else {
this.state = { ...this.state, ...getStateFromCustomRange(value) };
}
document.addEventListener('click', this.handleClick);
}
componentWillUnmount() {
@ -128,66 +176,37 @@ export default class DateFilterControl extends React.Component {
this.close();
}
}
setDefaults(timeFrame) {
const nextState = {
type: TYPES.DEFAULTS,
common: timeFrame,
until: moment().startOf('day').format(MOMENT_FORMAT),
};
const units = timeFrame.split(' ')[1] + 's';
nextState.since = moment().startOf('day').subtract(1, units).format(MOMENT_FORMAT);
this.setState(nextState, this.updateRefs);
}
setCustomRange(key, value) {
const nextState = { ...this.state, type: TYPES.CUSTOM_RANGE };
if (key !== undefined && value !== undefined) {
nextState[key] = value;
}
if (nextState.rel === RELATIVE_TIME_OPTIONS.LAST) {
nextState.until = moment().startOf('day').format(MOMENT_FORMAT);
nextState.since = moment()
.startOf('day')
.subtract(nextState.num, nextState.grain)
.format(MOMENT_FORMAT);
} else {
nextState.until = moment()
.startOf('day')
.add(nextState.num, nextState.grain)
.format(MOMENT_FORMAT);
nextState.since = moment().startOf('day').format(MOMENT_FORMAT);
}
this.setState(nextState, this.updateRefs);
const updatedState = { ...this.state, [key]: value };
const combinedValue = [updatedState.rel, updatedState.num, updatedState.grain].join(' ');
this.setState(getStateFromCustomRange(combinedValue));
}
setCustomStartEnd(key, value) {
const nextState = {
const closeCalendar = (
(key === 'since' && this.state.sinceViewMode === 'days') ||
(key === 'until' && this.state.untilViewMode === 'days')
);
this.setState({
type: TYPES.CUSTOM_START_END,
freeformInputs: { ...this.state.freeformInputs },
};
if (value === INVALID_DATE_MESSAGE) {
// the DateTimeField component will return `Invalid date` for freeform
// text, so we need to cheat and steal the value from the state
const freeformValue = this.dateTimeFieldRefs[key].state.inputValue;
nextState.freeformInputs[key] = true;
nextState[key] = freeformValue;
} else {
nextState.freeformInputs[key] = false;
nextState[key] = value;
}
this.setState(nextState, this.updateRefs);
[key]: typeof value === 'string' ? value : value.format(MOMENT_FORMAT),
showSinceCalendar: this.state.showSinceCalendar && !closeCalendar,
showUntilCalendar: this.state.showUntilCalendar && !closeCalendar,
sinceViewMode: closeCalendar ? 'days' : this.state.sinceViewMode,
untilViewMode: closeCalendar ? 'days' : this.state.untilViewMode,
});
}
setTypeCustomRange() {
this.setState({ type: TYPES.CUSTOM_RANGE });
}
setTypeCustomStartEnd() {
this.setState({ type: TYPES.CUSTOM_START_END });
}
handleClick(e) {
// switch to `TYPES.CUSTOM_START_END` when the calendar is clicked
if (this.startEndSectionRef && this.startEndSectionRef.contains(e.target)) {
this.setState({ type: TYPES.CUSTOM_START_END });
this.setTypeCustomStartEnd();
}
}
updateRefs() {
/* This is required because the <DateTimeField> component does not accept
* freeform dates as props, since they can't be parsed by `moment`.
*/
this.dateTimeFieldRefs.since.setState({ inputValue: this.state.since });
this.dateTimeFieldRefs.until.setState({ inputValue: this.state.until });
}
close() {
let val;
if (this.state.type === TYPES.DEFAULTS) {
@ -199,11 +218,53 @@ export default class DateFilterControl extends React.Component {
}
this.props.onChange(val);
this.refs.trigger.hide();
this.setState({ showSinceCalendar: false, showUntilCalendar: false });
}
isValidSince(date) {
return (!isValidMoment(this.state.until) || date <= moment(this.state.until, MOMENT_FORMAT));
}
isValidUntil(date) {
return (!isValidMoment(this.state.since) || date >= moment(this.state.since, MOMENT_FORMAT));
}
toggleCalendar(key) {
const nextState = {};
if (key === 'showSinceCalendar') {
nextState.showSinceCalendar = !this.state.showSinceCalendar;
if (!this.state.showSinceCalendar) {
nextState.showUntilCalendar = false;
}
} else if (key === 'showUntilCalendar') {
nextState.showUntilCalendar = !this.state.showUntilCalendar;
if (!this.state.showUntilCalendar) {
nextState.showSinceCalendar = false;
}
}
this.setState(nextState);
}
renderInput(props, key) {
return (
<FormGroup>
<InputGroup>
<FormControl
{...props}
type="text"
onKeyPress={this.onEnter}
onFocus={this.setTypeCustomStartEnd}
onClick={() => {}}
/>
<InputGroup.Button onClick={() => this.toggleCalendar(key)}>
<Button>
<Glyphicon glyph="calendar" style={{ padding: 3 }} />
</Button>
</InputGroup.Button>
</InputGroup>
</FormGroup>
);
}
renderPopover() {
const grainOptions = TIME_GRAIN_OPTIONS.map(grain => (
<MenuItem
onSelect={this.setCustomRange.bind(this, 'grain')}
onSelect={value => this.setCustomRange('grain', value)}
key={grain}
eventKey={grain}
active={grain === this.state.grain}
@ -215,7 +276,7 @@ export default class DateFilterControl extends React.Component {
<Radio
key={timeFrame.replace(' ', '').toLowerCase()}
checked={this.state.common === timeFrame}
onChange={this.setDefaults.bind(this, timeFrame)}
onChange={() => this.setState(getStateFromCommonTimeFrame(timeFrame))}
>
{timeFrame}
</Radio>
@ -236,7 +297,7 @@ export default class DateFilterControl extends React.Component {
<PopoverSection
title="Relative to today"
isSelected={this.state.type === TYPES.CUSTOM_RANGE}
onSelect={this.setCustomRange.bind(this)}
onSelect={this.setTypeCustomRange}
>
<div className="clearfix centered" style={{ marginTop: '12px' }}>
<div style={{ width: '60px', marginTop: '-4px' }} className="input-inline">
@ -245,17 +306,17 @@ export default class DateFilterControl extends React.Component {
componentClass={InputGroup.Button}
id="input-dropdown-rel"
title={this.state.rel}
onFocus={this.setCustomRange.bind(this)}
onFocus={this.setTypeCustomRange}
>
<MenuItem
onSelect={this.setCustomRange.bind(this, 'rel')}
onSelect={value => this.setCustomRange('rel', value)}
key={RELATIVE_TIME_OPTIONS.LAST}
eventKey={RELATIVE_TIME_OPTIONS.LAST}
active={this.state.rel === RELATIVE_TIME_OPTIONS.LAST}
>Last
</MenuItem>
<MenuItem
onSelect={this.setCustomRange.bind(this, 'rel')}
onSelect={value => this.setCustomRange('rel', value)}
key={RELATIVE_TIME_OPTIONS.NEXT}
eventKey={RELATIVE_TIME_OPTIONS.NEXT}
active={this.state.rel === RELATIVE_TIME_OPTIONS.NEXT}
@ -267,11 +328,9 @@ export default class DateFilterControl extends React.Component {
<FormControl
bsSize="small"
type="text"
onChange={event => (
this.setCustomRange.call(this, 'num', event.target.value)
)}
onFocus={this.setCustomRange.bind(this)}
onKeyPress={this.onEnter.bind(this)}
onChange={event => this.setCustomRange('num', event.target.value)}
onFocus={this.setTypeCustomRange}
onKeyPress={this.onEnter}
value={this.state.num}
style={{ height: '30px' }}
/>
@ -282,7 +341,7 @@ export default class DateFilterControl extends React.Component {
componentClass={InputGroup.Button}
id="input-dropdown-grain"
title={this.state.grain}
onFocus={this.setCustomRange.bind(this)}
onFocus={this.setTypeCustomRange}
>
{grainOptions}
</DropdownButton>
@ -292,48 +351,37 @@ export default class DateFilterControl extends React.Component {
<PopoverSection
title="Start / end"
isSelected={this.state.type === TYPES.CUSTOM_START_END}
onSelect={this.setCustomStartEnd.bind(this)}
onSelect={this.setTypeCustomStartEnd}
info={FREEFORM_TOOLTIP}
>
<div ref={(ref) => { this.startEndSectionRef = ref; }}>
<InputGroup>
<div style={{ margin: '5px 0' }}>
<DateTimeField
ref={(ref) => { this.dateTimeFieldRefs.since = ref; }}
dateTime={
this.state.freeformInputs.since ?
DEFAULT_SINCE :
this.state.since
}
defaultText={this.state.since}
onChange={this.setCustomStartEnd.bind(this, 'since')}
maxDate={moment(this.state.until, MOMENT_FORMAT)}
format={MOMENT_FORMAT}
inputFormat={MOMENT_FORMAT}
onClick={this.setCustomStartEnd.bind(this)}
inputProps={{
onKeyPress: this.onEnter.bind(this),
onFocus: this.setCustomStartEnd.bind(this),
}}
<Datetime
value={this.state.since}
defaultValue={this.state.since}
viewDate={this.state.since}
onChange={value => this.setCustomStartEnd('since', value)}
isValidDate={this.isValidSince}
onClick={this.setTypeCustomStartEnd}
renderInput={props => this.renderInput(props, 'showSinceCalendar')}
open={this.state.showSinceCalendar}
viewMode={this.state.sinceViewMode}
onViewModeChange={sinceViewMode => this.setState({ sinceViewMode })}
/>
</div>
<div style={{ margin: '5px 0' }}>
<DateTimeField
ref={(ref) => { this.dateTimeFieldRefs.until = ref; }}
dateTime={
this.state.freeformInputs.until ?
DEFAULT_UNTIL :
this.state.until
}
defaultText={this.state.until}
onChange={this.setCustomStartEnd.bind(this, 'until')}
minDate={moment(this.state.since, MOMENT_FORMAT).add(1, 'days')}
format={MOMENT_FORMAT}
inputFormat={MOMENT_FORMAT}
inputProps={{
onKeyPress: this.onEnter.bind(this),
onFocus: this.setCustomStartEnd.bind(this),
}}
<Datetime
value={this.state.until}
defaultValue={this.state.until}
viewDate={this.state.until}
onChange={value => this.setCustomStartEnd('until', value)}
isValidDate={this.isValidUntil}
onClick={this.setTypeCustomStartEnd}
renderInput={props => this.renderInput(props, 'showUntilCalendar')}
open={this.state.showUntilCalendar}
viewMode={this.state.untilViewMode}
onViewModeChange={untilViewMode => this.setState({ untilViewMode })}
/>
</div>
</InputGroup>
@ -347,7 +395,7 @@ export default class DateFilterControl extends React.Component {
bsSize="small"
className="float-right ok"
bsStyle="primary"
onClick={this.close.bind(this)}
onClick={this.close}
>
Ok
</Button>

View File

@ -7,6 +7,7 @@ const propTypes = {
value: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.object,
]),
};

View File

@ -2200,12 +2200,6 @@ babel-register@^6.24.1, babel-register@^6.26.0, babel-register@^6.9.0:
mkdirp "^0.5.1"
source-map-support "^0.4.15"
babel-runtime@^5.6.18:
version "5.8.38"
resolved "http://registry.npmjs.org/babel-runtime/-/babel-runtime-5.8.38.tgz#1c0b02eb63312f5f087ff20450827b425c9d4c19"
dependencies:
core-js "^1.0.0"
babel-runtime@^6.11.6, babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.23.0, babel-runtime@^6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
@ -8107,7 +8101,7 @@ moment-timezone@0.5.5:
dependencies:
moment ">= 2.6.0"
"moment@>= 2.6.0", moment@^2.20.1, moment@^2.8.2:
"moment@>= 2.6.0", moment@^2.20.1:
version "2.22.2"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66"
@ -10009,14 +10003,6 @@ react-addons-test-utils@^15.6.2:
version "15.6.2"
resolved "https://registry.yarnpkg.com/react-addons-test-utils/-/react-addons-test-utils-15.6.2.tgz#c12b6efdc2247c10da7b8770d185080a7b047156"
react-bootstrap-datetimepicker@0.0.22:
version "0.0.22"
resolved "https://registry.yarnpkg.com/react-bootstrap-datetimepicker/-/react-bootstrap-datetimepicker-0.0.22.tgz#07e448d993157d049ad0876d0f9a3c9c5029d9c5"
dependencies:
babel-runtime "^5.6.18"
classnames "^2.1.2"
moment "^2.8.2"
react-bootstrap-dialog@^0.10.0:
version "0.10.0"
resolved "https://registry.yarnpkg.com/react-bootstrap-dialog/-/react-bootstrap-dialog-0.10.0.tgz#fca5c84804ea2b6debe3833c6d4b7480bcff0175"
@ -10062,9 +10048,9 @@ react-color@^2.13.8:
reactcss "^1.2.0"
tinycolor2 "^1.4.1"
react-datetime@2.14.0:
version "2.14.0"
resolved "https://registry.yarnpkg.com/react-datetime/-/react-datetime-2.14.0.tgz#c7859c5b765275d7980f1cca27c03a727ff9ccef"
react-datetime@^2.14.0:
version "2.15.0"
resolved "https://registry.yarnpkg.com/react-datetime/-/react-datetime-2.15.0.tgz#a8f7da6c58b6b45dbeea32d4e8485db17614e12c"
dependencies:
create-react-class "^15.5.2"
object-assign "^3.0.0"