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-css-transition-group": "^15.6.0",
"react-addons-shallow-compare": "^15.4.2", "react-addons-shallow-compare": "^15.4.2",
"react-bootstrap": "^0.31.5", "react-bootstrap": "^0.31.5",
"react-bootstrap-datetimepicker": "0.0.22",
"react-bootstrap-dialog": "^0.10.0", "react-bootstrap-dialog": "^0.10.0",
"react-bootstrap-slider": "2.1.5", "react-bootstrap-slider": "2.1.5",
"react-bootstrap-table": "^4.3.1", "react-bootstrap-table": "^4.3.1",
"react-color": "^2.13.8", "react-color": "^2.13.8",
"react-datetime": "2.14.0", "react-datetime": "^2.14.0",
"react-dnd": "^2.5.4", "react-dnd": "^2.5.4",
"react-dnd-html5-backend": "^2.5.4", "react-dnd-html5-backend": "^2.5.4",
"react-dom": "^15.6.2", "react-dom": "^15.6.2",

View File

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

View File

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

View File

@ -7,6 +7,7 @@ const propTypes = {
value: PropTypes.oneOfType([ value: PropTypes.oneOfType([
PropTypes.string, PropTypes.string,
PropTypes.number, 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" mkdirp "^0.5.1"
source-map-support "^0.4.15" 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: 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" version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
@ -8107,7 +8101,7 @@ moment-timezone@0.5.5:
dependencies: dependencies:
moment ">= 2.6.0" 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" version "2.22.2"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66" 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" version "15.6.2"
resolved "https://registry.yarnpkg.com/react-addons-test-utils/-/react-addons-test-utils-15.6.2.tgz#c12b6efdc2247c10da7b8770d185080a7b047156" 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: react-bootstrap-dialog@^0.10.0:
version "0.10.0" version "0.10.0"
resolved "https://registry.yarnpkg.com/react-bootstrap-dialog/-/react-bootstrap-dialog-0.10.0.tgz#fca5c84804ea2b6debe3833c6d4b7480bcff0175" 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" reactcss "^1.2.0"
tinycolor2 "^1.4.1" tinycolor2 "^1.4.1"
react-datetime@2.14.0: react-datetime@^2.14.0:
version "2.14.0" version "2.15.0"
resolved "https://registry.yarnpkg.com/react-datetime/-/react-datetime-2.14.0.tgz#c7859c5b765275d7980f1cca27c03a727ff9ccef" resolved "https://registry.yarnpkg.com/react-datetime/-/react-datetime-2.15.0.tgz#a8f7da6c58b6b45dbeea32d4e8485db17614e12c"
dependencies: dependencies:
create-react-class "^15.5.2" create-react-class "^15.5.2"
object-assign "^3.0.0" object-assign "^3.0.0"