diff --git a/CHANGELOG.md b/CHANGELOG.md
index 19a20ee52cc..f334039e324 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,7 @@
## [`master`](https://github.com/elastic/eui/tree/master)
- Added `DisambiguateSet` and `ExclusiveUnion` utility types ([#1368](https://github.com/elastic/eui/pull/1368))
+- Added `EuiSuperDatePicker` component ([#1351](https://github.com/elastic/eui/pull/1351))
**Bug fixes**
diff --git a/package.json b/package.json
index a90f48f28b0..75d156b0970 100644
--- a/package.json
+++ b/package.json
@@ -69,6 +69,7 @@
"@babel/preset-env": "^7.1.0",
"@babel/preset-react": "^7.0.0",
"@babel/preset-typescript": "^7.1.0",
+ "@elastic/datemath": "^5.0.2",
"@elastic/eslint-config-kibana": "^0.15.0",
"@types/classnames": "^2.2.6",
"@types/enzyme": "^3.1.13",
@@ -170,6 +171,7 @@
"yo": "^2.0.0"
},
"peerDependencies": {
+ "@elastic/datemath": "^5.0.2",
"moment": "^2.13.0",
"prop-types": "^15.5.0",
"react": "^16.3",
diff --git a/src-docs/src/theme_dark.scss b/src-docs/src/theme_dark.scss
index 97d4ef0207c..f09a1aede5c 100644
--- a/src-docs/src/theme_dark.scss
+++ b/src-docs/src/theme_dark.scss
@@ -1,4 +1,3 @@
@import '../../src/theme_dark';
@import './components/guide_components';
@import './views/header/global_filter_group';
-@import './views/date_picker/global_date_picker';
diff --git a/src-docs/src/theme_k6_dark.scss b/src-docs/src/theme_k6_dark.scss
index a3738416b1d..99e708a00f1 100644
--- a/src-docs/src/theme_k6_dark.scss
+++ b/src-docs/src/theme_k6_dark.scss
@@ -1,4 +1,3 @@
@import '../../src/theme_k6_dark';
@import './components/guide_components';
@import './views/header/global_filter_group';
-@import './views/date_picker/global_date_picker';
diff --git a/src-docs/src/theme_k6_light.scss b/src-docs/src/theme_k6_light.scss
index 6fe9d55fe4b..544617f335d 100644
--- a/src-docs/src/theme_k6_light.scss
+++ b/src-docs/src/theme_k6_light.scss
@@ -1,4 +1,3 @@
@import '../../src/theme_k6_light';
@import './components/guide_components';
@import './views/header/global_filter_group';
-@import './views/date_picker/global_date_picker';
diff --git a/src-docs/src/theme_light.scss b/src-docs/src/theme_light.scss
index bd607c9114f..782fc422f63 100644
--- a/src-docs/src/theme_light.scss
+++ b/src-docs/src/theme_light.scss
@@ -1,4 +1,3 @@
@import '../../src/theme_light';
@import './components/guide_components';
@import './views/header/global_filter_group';
-@import './views/date_picker/global_date_picker';
diff --git a/src-docs/src/views/date_picker/date_picker_example.js b/src-docs/src/views/date_picker/date_picker_example.js
index 7843e3e30d6..b326f755eaa 100644
--- a/src-docs/src/views/date_picker/date_picker_example.js
+++ b/src-docs/src/views/date_picker/date_picker_example.js
@@ -11,7 +11,7 @@ import {
EuiLink,
EuiDatePicker,
EuiDatePickerRange,
- EuiCallOut,
+ EuiSuperDatePicker,
} from '../../../../src/components';
import DatePicker from './date_picker';
@@ -58,9 +58,9 @@ import Utc from './utc';
const utcSource = require('!!raw-loader!./utc');
const utcHtml = renderToHtml(Utc);
-import GlobalDatePicker from './global_date_picker';
-const globalDatePickerSource = require('!!raw-loader!./global_date_picker');
-const globalDatePickerHtml = renderToHtml(GlobalDatePicker);
+import SuperDatePicker from './super_date_picker';
+const superDatePickerSource = require('!!raw-loader!./super_date_picker');
+const superDatePickerHtml = renderToHtml(SuperDatePicker);
export const DatePickerExample = {
title: 'DatePicker',
@@ -266,24 +266,24 @@ export const DatePickerExample = {
),
demo: ,
}, {
- title: 'Global date picker',
+ title: 'Super date picker',
source: [{
type: GuideSectionTypes.JS,
- code: globalDatePickerSource,
+ code: superDatePickerSource,
}, {
type: GuideSectionTypes.HTML,
- code: globalDatePickerHtml,
+ code: superDatePickerHtml,
}],
text: (
-
-
- This documents a visual pattern for the eventual replacement of Kibana's
- global date/time picker. It uses all EUI components with some custom styles.
-
-
+
+ start and end date times are passed as strings
+ in either datemath format (e.g.: now, now-15m, now-15m/m)
+ or as absolute date in the format YYYY-MM-DDTHH:mm:ss.sssZ
+
),
- demo: ,
+ demo: ,
+ props: { EuiSuperDatePicker },
}],
};
diff --git a/src-docs/src/views/date_picker/global_date_picker.js b/src-docs/src/views/date_picker/global_date_picker.js
deleted file mode 100644
index 13930214175..00000000000
--- a/src-docs/src/views/date_picker/global_date_picker.js
+++ /dev/null
@@ -1,566 +0,0 @@
-
-import React, {
- Component, Fragment,
-} from 'react';
-import PropTypes from 'prop-types';
-
-import moment from 'moment';
-import classNames from 'classnames';
-
-import {
- EuiDatePicker,
- EuiDatePickerRange,
- EuiFormControlLayout,
- EuiButtonEmpty,
- EuiIcon,
- EuiLink, EuiTitle, EuiFlexGrid, EuiFlexItem,
- EuiPopover,
- EuiSpacer,
- EuiText,
- EuiHorizontalRule,
- EuiFlexGroup,
- EuiFormRow,
- EuiSelect,
- EuiFieldNumber,
- EuiButton,
- EuiTabbedContent,
- EuiForm,
- EuiSwitch,
- EuiToolTip,
- EuiFieldText,
- EuiButtonIcon,
-} from '../../../../src/components';
-
-const commonDates = [
- 'Today', 'Yesterday', 'This week', 'Week to date', 'This month', 'Month to date', 'This year', 'Year to date',
-];
-
-const relativeSelectOptions = [
- { text: 'Seconds ago', value: 'string:s' },
- { text: 'Minutes ago', value: 'string:m' },
- { text: 'Hours ago', value: 'string:h' },
- { text: 'Days ago', value: 'string:d' },
- { text: 'Weeks ago', value: 'string:w' },
- { text: 'Months ago', value: 'string:M' },
- { text: 'Years ago', value: 'string:y' },
- { text: 'Seconds from now', value: 'string:s+' },
- { text: 'Minutes from now', value: 'string:m+' },
- { text: 'Hours from now', value: 'string:h+' },
- { text: 'Days from now', value: 'string:d+' },
- { text: 'Weeks from now', value: 'string:w+' },
- { text: 'Months from now', value: 'string:M+' },
- { text: 'Years from now', value: 'string:y+' },
-];
-
-class GlobalDatePopover extends Component {
- constructor(props) {
- super(props);
-
- this.tabs = [{
- id: 'absolute',
- name: 'Absolute',
- content: (
-
-
-
-
-
-
- ),
- }, {
- id: 'relative',
- name: 'Relative',
- content: (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ),
- }, {
- id: 'now',
- name: 'Now',
- content: (
-
-
- Setting the time to "Now" means that on every refresh
- this time will be set to the time of the refresh.
-
-
- ),
- }];
-
- this.state = {
- selectedTab: this.tabs[0],
- };
- }
-
- onTabClick = (selectedTab) => {
- this.setState({ selectedTab });
- };
-
- render() {
- return (
-
- );
- }
-}
-
-// eslint-disable-next-line react/no-multi-comp
-class GlobalDateButton extends Component {
- static propTypes = {
- position: PropTypes.oneOf(['start', 'end']),
- isInvalid: PropTypes.bool,
- needsUpdating: PropTypes.bool,
- buttonOnly: PropTypes.bool,
- date: PropTypes.string,
- }
-
- constructor(props) {
- super(props);
-
- this.state = {
- isPopoverOpen: false,
- };
- }
-
- togglePopover = () => {
- this.setState({
- isPopoverOpen: !this.state.isPopoverOpen,
- });
- }
-
- closePopover = () => {
- this.setState({
- isPopoverOpen: false,
- });
- }
-
- render() {
- const {
- position,
- isInvalid,
- needsUpdating,
- date,
- buttonProps,
- buttonOnly,
- ...rest
- } = this.props;
-
- const {
- isPopoverOpen,
- } = this.state;
-
- const classes = classNames([
- 'euiGlobalDatePicker__dateButton',
- `euiGlobalDatePicker__dateButton--${position}`,
- {
- 'euiGlobalDatePicker__dateButton-isSelected': isPopoverOpen,
- 'euiGlobalDatePicker__dateButton-isInvalid': isInvalid,
- 'euiGlobalDatePicker__dateButton-needsUpdating': needsUpdating
- }
- ]);
-
- let title = date;
- if (isInvalid) {
- title = `Invalid date: ${title}`;
- } else if (needsUpdating) {
- title = `Update needed: ${title}`;
- }
-
- const button = (
-
- {date}
-
- );
-
- return buttonOnly ? button : (
-
-
-
- );
- }
-}
-
-// eslint-disable-next-line react/no-multi-comp
-export default class extends Component {
- constructor(props) {
- super(props);
-
- this.state = {
- startDate: moment().format('MMM DD YYYY h:mm:ss.SSS'),
- endDate: moment().add(11, 'd').format('MMM DD YYYY hh:mm:ss.SSS'),
- isPopoverOpen: false,
- showPrettyFormat: false,
- showNeedsUpdate: false,
- isUpdating: false,
- timerIsOn: false,
- recentlyUsed: [
- ['11/25/2017 00:00 AM', '11/25/2017 11:59 PM'],
- ['3 hours ago', '4 minutes ago'],
- 'Last 6 months',
- ['06/11/2017 06:11 AM', '06/11/2017 06:11 PM'],
- ],
- };
- }
-
- setTootipRef = node => (this.tooltip = node);
-
- showTooltip = () => this.tooltip.showToolTip();
- hideTooltip = () => this.tooltip.hideToolTip();
-
- togglePopover = (e) => {
- // HACK TODO:
- // this works because react listens to all events at the
- // document level, and you need to interact with the native
- // event's propagation to short-circuit outside click handler
- // see also: https://stackoverflow.com/a/24421834
- e.nativeEvent.stopImmediatePropagation();
-
- this.setState(prevState => ({
- isPopoverOpen: !prevState.isPopoverOpen,
- }));
- }
-
- togglePrettyFormat = () => {
- this.setState(prevState => ({
- showPrettyFormat: !prevState.showPrettyFormat,
- }));
- }
-
- toggleNeedsUpdate = () => {
- this.setState(prevState => {
-
- if (!prevState.showNeedsUpdate) {
- clearTimeout(this.tooltipTimeout);
- this.showTooltip();
- this.tooltipTimeout = setTimeout(() => {
- this.hideTooltip();
- }, 10000);
- }
-
- return ({
- showNeedsUpdate: !prevState.showNeedsUpdate,
- });
- });
- }
-
- closePopover = () => {
- this.setState({
- isPopoverOpen: false,
- });
- }
-
- toggleTimer = () => {
- this.setState(prevState => ({
- timerIsOn: !prevState.timerIsOn,
- }));
- }
-
- toggleIsUpdating = () => {
- this.setState(prevState => ({
- isUpdating: !prevState.isUpdating,
- }));
- }
-
-
- render() {
- const quickSelectButton = (
-
-
-
- );
-
- const commonlyUsed = this.renderCommonlyUsed(commonDates);
- const recentlyUsed = this.renderRecentlyUsed(this.state.recentlyUsed);
-
- const quickSelectPopover = (
-
-
- {this.renderQuickSelect()}
-
- {commonlyUsed}
-
- {recentlyUsed}
-
- {this.renderTimer()}
-
-
- );
-
- return (
-
-
-
-
-
-
-
-
-
-
- }
- endDateControl={
-
- }
- >
- {this.state.showPrettyFormat &&
-
-
- Show dates
-
- }
-
-
-
-
- {this.renderUpdateButton()}
-
-
-
- );
- }
-
- renderUpdateButton = () => {
- const color = this.state.showNeedsUpdate ? 'secondary' : 'primary';
- const icon = this.state.showNeedsUpdate ? 'kqlFunction' : 'refresh';
- let text = this.state.showNeedsUpdate ? 'Update' : 'Refresh';
-
- if (this.state.isUpdating) {
- text = 'Updating';
- }
-
- return (
-
-
- {text}
-
-
- );
- }
-
- renderQuickSelect = () => {
- const firstOptions = [
- { value: 'last', text: 'Last' },
- { value: 'previous', text: 'Previous' },
- ];
-
- const lastOptions = [
- { value: 'seconds', text: 'seconds' },
- { value: 'minutes', text: 'minutes' },
- { value: 'hours', text: 'hours' },
- { value: 'days', text: 'days' },
- { value: 'weeks', text: 'weeks' },
- { value: 'months', text: 'months' },
- { value: 'years', text: 'years' },
- ];
-
- return (
-
-
-
- Quick select
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Apply
-
-
-
-
- );
- }
-
- renderCommonlyUsed = (commonDates) => {
- const links = commonDates.map((date) => {
- return (
- {date}
- );
- });
-
- return (
-
- Commonly used
-
-
-
- {links}
-
-
-
- );
- }
-
- renderRecentlyUsed = (recentDates) => {
- const links = recentDates.map((date) => {
- let dateRange;
- if (typeof date !== 'string') {
- dateRange = `${date[0]} – ${date[1]}`;
- }
-
- return (
- {dateRange || date}
- );
- });
-
- return (
-
- Recently used date ranges
-
-
-
- {links}
-
-
-
- );
- }
-
- renderTimer = () => {
- const lastOptions = [
- { value: 'minutes', text: 'minutes' },
- { value: 'hours', text: 'hours' },
- ];
-
- return (
-
- Refresh every
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {this.state.timerIsOn ? 'Stop' : 'Start'}
-
-
-
-
-
- );
- }
-
-}
diff --git a/src-docs/src/views/date_picker/super_date_picker.js b/src-docs/src/views/date_picker/super_date_picker.js
new file mode 100644
index 00000000000..47c0347522a
--- /dev/null
+++ b/src-docs/src/views/date_picker/super_date_picker.js
@@ -0,0 +1,64 @@
+
+import React, { Component } from 'react';
+
+import {
+ EuiSuperDatePicker,
+} from '../../../../src/components';
+
+export default class extends Component {
+
+ state = {
+ recentlyUsedRanges: [],
+ isLoading: false,
+ }
+
+ onTimeChange = ({ start, end }) => {
+ this.setState((prevState) => {
+ const recentlyUsedRanges = prevState.recentlyUsedRanges.filter(recentlyUsedRange => {
+ const isDuplicate = recentlyUsedRange.start === start && recentlyUsedRange.end === end;
+ return !isDuplicate;
+ });
+ recentlyUsedRanges.unshift({ start, end });
+ return {
+ start,
+ end,
+ recentlyUsedRanges: recentlyUsedRanges.length > 10 ? recentlyUsedRanges.slice(0, 9) : recentlyUsedRanges,
+ isLoading: true,
+ };
+ }, this.startLoading);
+ }
+
+ startLoading = () => {
+ setTimeout(
+ this.stopLoading,
+ 1000);
+ }
+
+ stopLoading = () => {
+ this.setState({ isLoading: false });
+ }
+
+ onRefreshChange = ({ isPaused, refreshInterval }) => {
+ this.setState((prevState) => {
+ return {
+ isPaused: isPaused == null ? prevState.isPaused : isPaused,
+ refreshInterval: refreshInterval == null ? prevState.refreshInterval : refreshInterval,
+ };
+ });
+ }
+
+ render() {
+ return (
+
+ );
+ }
+}
diff --git a/src/components/date_picker/_index.scss b/src/components/date_picker/_index.scss
index 65219322767..6ae886348b2 100644
--- a/src/components/date_picker/_index.scss
+++ b/src/components/date_picker/_index.scss
@@ -1,3 +1,4 @@
// Uses some form mixins
@import 'date_picker';
@import 'date_picker_range';
+@import 'super_date_picker/index';
diff --git a/src/components/date_picker/index.js b/src/components/date_picker/index.js
index eb720402eec..00f6c0b638a 100644
--- a/src/components/date_picker/index.js
+++ b/src/components/date_picker/index.js
@@ -5,3 +5,7 @@ export {
export {
EuiDatePickerRange,
} from './date_picker_range';
+
+export {
+ EuiSuperDatePicker,
+} from './super_date_picker';
diff --git a/src/components/date_picker/super_date_picker/__snapshots__/super_date_picker.test.js.snap b/src/components/date_picker/super_date_picker/__snapshots__/super_date_picker.test.js.snap
new file mode 100644
index 00000000000..5147c25d067
--- /dev/null
+++ b/src/components/date_picker/super_date_picker/__snapshots__/super_date_picker.test.js.snap
@@ -0,0 +1,314 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EuiSuperDatePicker is rendered 1`] = `
+
+
+
+ }
+ >
+ }
+ iconType={false}
+ isCustom={true}
+ startDateControl={
}
+ >
+
+ Last 15 minutes
+
+
+ Show dates
+
+
+
+
+
+
+
+ Refresh
+
+
+
+
+`;
+
+exports[`EuiSuperDatePicker isLoading 1`] = `
+
+
+
+ }
+ >
+ }
+ iconType={false}
+ isCustom={true}
+ startDateControl={
}
+ >
+
+ Last 15 minutes
+
+
+ Show dates
+
+
+
+
+
+
+
+ Updating
+
+
+
+
+`;
diff --git a/src/components/date_picker/super_date_picker/_index.scss b/src/components/date_picker/super_date_picker/_index.scss
new file mode 100644
index 00000000000..6382670c4bf
--- /dev/null
+++ b/src/components/date_picker/super_date_picker/_index.scss
@@ -0,0 +1 @@
+@import 'super_date_picker';
diff --git a/src-docs/src/views/date_picker/_global_date_picker.scss b/src/components/date_picker/super_date_picker/_super_date_picker.scss
similarity index 79%
rename from src-docs/src/views/date_picker/_global_date_picker.scss
rename to src/components/date_picker/super_date_picker/_super_date_picker.scss
index d522ce46bcb..16bdde5cf33 100644
--- a/src-docs/src/views/date_picker/_global_date_picker.scss
+++ b/src/components/date_picker/super_date_picker/_super_date_picker.scss
@@ -1,17 +1,15 @@
-//// GLOBAL Date picker
-
// sass-lint:disable no-important
-.euiGlobalDatePicker__quickSelectButton {
+.euiSuperDatePicker__quickSelectButton {
// Override prepend border since button already lives inside another prepend
border-right: none !important;
- .euiGlobalDatePicker__quickSelectButtonText {
+ .euiSuperDatePicker__quickSelectButtonText {
// Override specificity from universal and sibiling selectors
margin-right: $euiSizeXS !important;
}
}
-.euiGlobalDatePicker.euiFormControlLayout {
+.euiSuperDatePicker.euiFormControlLayout {
max-width: 480px;
> .euiFormControlLayout__childrenWrapper {
@@ -30,7 +28,7 @@
}
}
-.euiGlobalDatePicker__dateButton {
+.euiSuperDatePicker__dateText {
@include euiFormControlText;
display: block;
width: 100%;
@@ -39,7 +37,9 @@
height: $euiFormControlHeight - 2px;
word-break: break-all;
transition: background $euiAnimSpeedFast ease-in;
+}
+.euiSuperDatePicker__dateButton {
$backgroundColor: tintOrShade($euiColorSuccess, 90%, 70%);
$textColor: makeHighContrastColor($euiColorSuccess, $backgroundColor);
@@ -66,20 +66,20 @@
}
}
-.euiGlobalDatePicker__dateButton--start {
+.euiSuperDatePicker__dateButton--start {
text-align: right;
}
-.euiGlobalDatePicker__dateButton--end {
+.euiSuperDatePicker__dateButton--end {
text-align: left;
}
-.euiGlobalDatePicker__updateButton {
+.euiSuperDatePicker__updateButton {
// Just wide enough for all 3 states
min-width: $euiButtonMinWidth + ($euiSizeXS * 1.5);
}
-.euiGlobalDatePicker__popoverSection {
+.euiSuperDatePicker__popoverSection {
@include euiScrollBar;
max-height: $euiSizeM * 11;
overflow: hidden;
@@ -87,10 +87,10 @@
}
@include euiBreakpoint('xs', 's') {
- .euiGlobalDatePicker__updateButton {
+ .euiSuperDatePicker__updateButton {
min-width: 0;
- .euiGlobalDatePicker__updateButtonText {
+ .euiSuperDatePicker__updateButtonText {
display: none;
}
}
diff --git a/src/components/date_picker/super_date_picker/date_modes.js b/src/components/date_picker/super_date_picker/date_modes.js
new file mode 100644
index 00000000000..5ab95e6b22a
--- /dev/null
+++ b/src/components/date_picker/super_date_picker/date_modes.js
@@ -0,0 +1,30 @@
+
+import dateMath from '@elastic/datemath';
+import { parseRelativeParts, toRelativeStringFromParts } from './relative_utils';
+
+export const DATE_MODES = {
+ ABSOLUTE: 'absolute',
+ RELATIVE: 'relative',
+ NOW: 'now',
+};
+
+export function getDateMode(value) {
+ if (value === 'now') {
+ return DATE_MODES.NOW;
+ }
+
+ if (value.includes('now')) {
+ return DATE_MODES.RELATIVE;
+ }
+
+ return DATE_MODES.absolute;
+}
+
+export function toAbsoluteString(value, roundUp) {
+ return dateMath.parse(value, { roundUp }).toISOString();
+}
+
+
+export function toRelativeString(value) {
+ return toRelativeStringFromParts(parseRelativeParts(value));
+}
diff --git a/src/components/date_picker/super_date_picker/date_popover/absolute_tab.js b/src/components/date_picker/super_date_picker/date_popover/absolute_tab.js
new file mode 100644
index 00000000000..cd3c78fe4d4
--- /dev/null
+++ b/src/components/date_picker/super_date_picker/date_popover/absolute_tab.js
@@ -0,0 +1,81 @@
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+
+import moment from 'moment';
+
+import dateMath from '@elastic/datemath';
+
+import { EuiDatePicker } from '../../date_picker';
+import { EuiFormRow, EuiFieldText } from '../../../form';
+
+const INPUT_DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss.SSS';
+
+export class EuiAbsoluteTab extends Component {
+
+ constructor(props) {
+ super(props);
+
+ const valueAsMoment = dateMath.parse(props.value, { roundUp: props.roundUp });
+ this.state = {
+ valueAsMoment,
+ textInputValue: valueAsMoment.format(INPUT_DATE_FORMAT),
+ isTextInvalid: false,
+ };
+ }
+
+ handleChange = (date) => {
+ this.props.onChange(date.toISOString());
+ this.setState({
+ valueAsMoment: date,
+ textInputValue: date.format(INPUT_DATE_FORMAT),
+ isTextInvalid: false,
+ });
+ }
+
+ handleTextChange = (evt) => {
+ const date = moment(evt.target.value, INPUT_DATE_FORMAT, true);
+ const updatedState = {
+ textInputValue: evt.target.value,
+ isTextInvalid: !date.isValid()
+ };
+ if (date.isValid()) {
+ this.props.onChange(date.toISOString());
+ updatedState.valueAsMoment = date;
+ }
+
+ this.setState(updatedState);
+ }
+
+ render() {
+ return (
+
+
+
+
+
+
+ );
+ }
+}
+
+EuiAbsoluteTab.propTypes = {
+ dateFormat: PropTypes.string.isRequired,
+ value: PropTypes.string.isRequired,
+ onChange: PropTypes.func.isRequired,
+ roundUp: PropTypes.bool.isRequired,
+};
diff --git a/src/components/date_picker/super_date_picker/date_popover/date_popover_button.js b/src/components/date_picker/super_date_picker/date_popover/date_popover_button.js
new file mode 100644
index 00000000000..6619aa81333
--- /dev/null
+++ b/src/components/date_picker/super_date_picker/date_popover/date_popover_button.js
@@ -0,0 +1,99 @@
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import classNames from 'classnames';
+
+import { EuiPopover } from '../../../popover';
+
+import { formatTimeString } from '../pretty_duration';
+import { EuiDatePopoverContent } from './date_popover_content';
+
+export class EuiDatePopoverButton extends Component {
+ static propTypes = {
+ position: PropTypes.oneOf(['start', 'end']),
+ isInvalid: PropTypes.bool,
+ needsUpdating: PropTypes.bool,
+ value: PropTypes.string.isRequired,
+ onChange: PropTypes.func.isRequired,
+ dateFormat: PropTypes.string.isRequired,
+ roundUp: PropTypes.bool,
+ }
+
+ state = {
+ isOpen: false,
+ }
+
+ togglePopover = () => {
+ this.setState((prevState) => {
+ return { isOpen: !prevState.isOpen };
+ });
+ }
+
+ closePopover = () => {
+ this.setState({
+ isOpen: false,
+ });
+ }
+
+ render() {
+ const {
+ position,
+ isInvalid,
+ needsUpdating,
+ value,
+ buttonProps,
+ roundUp,
+ onChange,
+ dateFormat,
+ ...rest
+ } = this.props;
+
+ const classes = classNames([
+ 'euiSuperDatePicker__dateText',
+ 'euiSuperDatePicker__dateButton',
+ `euiSuperDatePicker__dateButton--${position}`,
+ {
+ 'euiSuperDatePicker__dateButton-isSelected': this.state.isOpen,
+ 'euiSuperDatePicker__dateButton-isInvalid': isInvalid,
+ 'euiSuperDatePicker__dateButton-needsUpdating': needsUpdating
+ }
+ ]);
+
+ let title = value;
+ if (isInvalid) {
+ title = `Invalid date: ${title}`;
+ } else if (needsUpdating) {
+ title = `Update needed: ${title}`;
+ }
+
+ const button = (
+
+ {formatTimeString(value, dateFormat, roundUp)}
+
+ );
+
+ return (
+
+
+
+ );
+ }
+}
diff --git a/src/components/date_picker/super_date_picker/date_popover/date_popover_content.js b/src/components/date_picker/super_date_picker/date_popover/date_popover_content.js
new file mode 100644
index 00000000000..f5733e64811
--- /dev/null
+++ b/src/components/date_picker/super_date_picker/date_popover/date_popover_content.js
@@ -0,0 +1,97 @@
+
+import PropTypes from 'prop-types';
+import React from 'react';
+
+import { EuiTabbedContent } from '../../../tabs';
+import { EuiText } from '../../../text';
+
+import { EuiAbsoluteTab } from './absolute_tab';
+import { EuiRelativeTab } from './relative_tab';
+
+import {
+ getDateMode,
+ DATE_MODES,
+ toAbsoluteString,
+ toRelativeString,
+} from '../date_modes';
+
+export function EuiDatePopoverContent({ value, roundUp, onChange, dateFormat }) {
+
+ const onTabClick = (selectedTab) => {
+ switch(selectedTab.id) {
+ case DATE_MODES.ABSOLUTE:
+ onChange(toAbsoluteString(value, roundUp));
+ break;
+ case DATE_MODES.RELATIVE:
+ onChange(toRelativeString(value));
+ break;
+ case DATE_MODES.NOW:
+ onChange('now');
+ break;
+ }
+ };
+
+ const renderTabs = () => {
+ return [
+ {
+ id: DATE_MODES.ABSOLUTE,
+ name: 'Absolute',
+ content: (
+
+ ),
+ 'data-test-subj': 'superDatePickerAbsoluteTab',
+ },
+ {
+ id: DATE_MODES.RELATIVE,
+ name: 'Relative',
+ content: (
+
+ ),
+ 'data-test-subj': 'superDatePickerRelativeTab',
+ },
+ {
+ id: DATE_MODES.NOW,
+ name: 'Now',
+ content: (
+
+
+ Setting the time to "Now" means that on every refresh
+ this time will be set to the time of the refresh.
+
+
+ ),
+ 'data-test-subj': 'superDatePickerNowTab',
+ }
+ ];
+ };
+
+ return (
+
+ );
+}
+
+EuiDatePopoverContent.propTypes = {
+ value: PropTypes.string.isRequired,
+ onChange: PropTypes.func.isRequired,
+ roundUp: PropTypes.bool,
+ dateFormat: PropTypes.string.isRequired,
+};
+
+EuiDatePopoverContent.defaultProps = {
+ roundUp: false,
+};
diff --git a/src/components/date_picker/super_date_picker/date_popover/relative_tab.js b/src/components/date_picker/super_date_picker/date_popover/relative_tab.js
new file mode 100644
index 00000000000..13de817a720
--- /dev/null
+++ b/src/components/date_picker/super_date_picker/date_popover/relative_tab.js
@@ -0,0 +1,99 @@
+
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import dateMath from '@elastic/datemath';
+
+import { EuiFlexGroup, EuiFlexItem } from '../../../flex';
+import {
+ EuiForm,
+ EuiFormRow,
+ EuiSelect,
+ EuiFieldNumber,
+ EuiFieldText,
+ EuiSwitch
+} from '../../../form';
+
+import { timeUnits } from '../time_units';
+import { relativeOptions } from '../relative_options';
+import { parseRelativeParts, toRelativeStringFromParts } from '../relative_utils';
+
+export class EuiRelativeTab extends Component {
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ ...parseRelativeParts(this.props.value),
+ };
+ }
+
+ onCountChange = (evt) => {
+ const sanitizedValue = parseInt(evt.target.value, 10);
+ this.setState({
+ count: isNaN(sanitizedValue) ? '' : sanitizedValue,
+ }, this.handleChange);
+ }
+
+ onUnitChange = (evt) => {
+ this.setState({
+ unit: evt.target.value,
+ }, this.handleChange);
+ }
+
+ onRoundChange = (evt) => {
+ this.setState({
+ round: evt.target.checked,
+ }, this.handleChange);
+ };
+
+ handleChange = () => {
+ if (this.state.count === '') {
+ return;
+ }
+ this.props.onChange(toRelativeStringFromParts(this.state));
+ }
+
+ render() {
+ const formatedValue = dateMath.parse(this.props.value).format(this.props.dateFormat);
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+EuiRelativeTab.propTypes = {
+ dateFormat: PropTypes.string.isRequired,
+ value: PropTypes.string.isRequired,
+ onChange: PropTypes.func.isRequired,
+};
diff --git a/src/components/date_picker/super_date_picker/index.js b/src/components/date_picker/super_date_picker/index.js
new file mode 100644
index 00000000000..183a5d85800
--- /dev/null
+++ b/src/components/date_picker/super_date_picker/index.js
@@ -0,0 +1,5 @@
+import {
+ WrappedEuiSuperDatePicker,
+} from './super_date_picker';
+
+export { WrappedEuiSuperDatePicker as EuiSuperDatePicker };
diff --git a/src/components/date_picker/super_date_picker/pretty_duration.js b/src/components/date_picker/super_date_picker/pretty_duration.js
new file mode 100644
index 00000000000..feb9fe2ff58
--- /dev/null
+++ b/src/components/date_picker/super_date_picker/pretty_duration.js
@@ -0,0 +1,81 @@
+
+import dateMath from '@elastic/datemath';
+import moment from 'moment';
+import { timeUnits, timeUnitsPlural } from './time_units';
+import { getDateMode, DATE_MODES } from './date_modes';
+import { parseRelativeParts } from './relative_utils';
+
+const ISO_FORMAT = 'YYYY-MM-DDTHH:mm:ss.SSSZ';
+
+function cantLookup(timeFrom, timeTo, dateFormat) {
+ const displayFrom = formatTimeString(timeFrom, dateFormat);
+ const displayTo = formatTimeString(timeTo, dateFormat, true);
+ return `${displayFrom} to ${displayTo}`;
+}
+
+function isRelativeToNow(timeFrom, timeTo) {
+ const fromDateMode = getDateMode(timeFrom);
+ const toDateMode = getDateMode(timeTo);
+ const isLast = fromDateMode === DATE_MODES.RELATIVE && toDateMode === DATE_MODES.NOW;
+ const isNext = fromDateMode === DATE_MODES.NOW && toDateMode === DATE_MODES.RELATIVE;
+ return isLast || isNext;
+}
+
+export function formatTimeString(timeString, dateFormat, roundUp = false) {
+ const timeAsMoment = moment(timeString, ISO_FORMAT, true);
+ if (timeAsMoment.isValid()) {
+ return timeAsMoment.format(dateFormat);
+ }
+
+ if (timeString === 'now') {
+ return 'now';
+ }
+
+ const tryParse = dateMath.parse(timeString, { roundUp: roundUp });
+ if (moment.isMoment(tryParse)) {
+ return `~ ${tryParse.fromNow()}`;
+ }
+
+ return timeString;
+}
+
+export function prettyDuration(timeFrom, timeTo, quickRanges = [], dateFormat) {
+ const matchingQuickRange = quickRanges.find(({ start: quickFrom, end: quickTo }) => {
+ return timeFrom === quickFrom && timeTo === quickTo;
+ });
+ if (matchingQuickRange) {
+ return matchingQuickRange.label;
+ }
+
+ if (isRelativeToNow(timeFrom, timeTo)) {
+ let timeTense;
+ let relativeParts;
+ if (getDateMode(timeTo) === DATE_MODES.NOW) {
+ timeTense = 'Last';
+ relativeParts = parseRelativeParts(timeFrom);
+ } else {
+ timeTense = 'Next';
+ relativeParts = parseRelativeParts(timeTo);
+ }
+ const countTimeUnit = relativeParts.unit.substring(0, 1);
+ const countTimeUnitFullName = relativeParts.count > 1 ? timeUnitsPlural[countTimeUnit] : timeUnits[countTimeUnit];
+ let text = `${timeTense} ${relativeParts.count} ${countTimeUnitFullName}`;
+ if (relativeParts.round) {
+ text += ` rounded to the ${timeUnits[relativeParts.roundUnit]}`;
+ }
+ return text;
+ }
+
+ return cantLookup(timeFrom, timeTo, dateFormat);
+}
+
+export function showPrettyDuration(timeFrom, timeTo, quickRanges = []) {
+ const matchingQuickRange = quickRanges.find(({ start: quickFrom, end: quickTo }) => {
+ return timeFrom === quickFrom && timeTo === quickTo;
+ });
+ if (matchingQuickRange) {
+ return true;
+ }
+
+ return isRelativeToNow(timeFrom, timeTo);
+}
diff --git a/src/components/date_picker/super_date_picker/pretty_duration.test.js b/src/components/date_picker/super_date_picker/pretty_duration.test.js
new file mode 100644
index 00000000000..3f0124eea17
--- /dev/null
+++ b/src/components/date_picker/super_date_picker/pretty_duration.test.js
@@ -0,0 +1,69 @@
+
+import { prettyDuration, showPrettyDuration } from './pretty_duration';
+
+const dateFormat = 'MMMM Do YYYY, HH:mm:ss.SSS';
+const quickRanges = [
+ {
+ start: 'now-15m',
+ end: 'now',
+ label: 'quick range 15 minutes custom display',
+ }
+];
+
+describe('prettyDuration', () => {
+ test('quick range', () => {
+ const timeFrom = 'now-15m';
+ const timeTo = 'now';
+ expect(prettyDuration(timeFrom, timeTo, quickRanges, dateFormat)).toBe('quick range 15 minutes custom display');
+ });
+
+ test('last', () => {
+ const timeFrom = 'now-16m';
+ const timeTo = 'now';
+ expect(prettyDuration(timeFrom, timeTo, quickRanges, dateFormat)).toBe('Last 16 minutes');
+ });
+
+ test('last that is rounded', () => {
+ const timeFrom = 'now-1M/w';
+ const timeTo = 'now';
+ expect(prettyDuration(timeFrom, timeTo, quickRanges, dateFormat)).toBe('Last 1 month rounded to the week');
+ });
+
+ test('next', () => {
+ const timeFrom = 'now';
+ const timeTo = 'now+16m';
+ expect(prettyDuration(timeFrom, timeTo, quickRanges, dateFormat)).toBe('Next 16 minutes');
+ });
+
+ test('from is in past', () => {
+ const timeFrom = 'now-17m';
+ const timeTo = 'now-15m';
+ expect(prettyDuration(timeFrom, timeTo, quickRanges, dateFormat)).toBe('~ 17 minutes ago to ~ 15 minutes ago');
+ });
+});
+
+describe('showPrettyDuration', () => {
+ test('should show pretty duration for quick range', () => {
+ expect(showPrettyDuration('now-15m', 'now', quickRanges)).toBe(true);
+ });
+
+ test('should show pretty duration for last', () => {
+ expect(showPrettyDuration('now-17m', 'now', quickRanges)).toBe(true);
+ });
+
+ test('should show pretty duration for next', () => {
+ expect(showPrettyDuration('now', 'now+17m', quickRanges)).toBe(true);
+ });
+
+ test('should not show pretty duration for relative to relative', () => {
+ expect(showPrettyDuration('now-17m', 'now-3m', quickRanges)).toBe(false);
+ });
+
+ test('should not show pretty duration for absolute to absolute', () => {
+ expect(showPrettyDuration('2018-01-17T18:57:57.149Z', '2018-01-17T20:00:00.000Z', quickRanges)).toBe(false);
+ });
+
+ test('should not show pretty duration for absolute to now', () => {
+ expect(showPrettyDuration('2018-01-17T18:57:57.149Z', 'now', quickRanges)).toBe(false);
+ });
+});
diff --git a/src/components/date_picker/super_date_picker/quick_select_popover/__snapshots__/quick_select.test.js.snap b/src/components/date_picker/super_date_picker/quick_select_popover/__snapshots__/quick_select.test.js.snap
new file mode 100644
index 00000000000..95e30574a8e
--- /dev/null
+++ b/src/components/date_picker/super_date_picker/quick_select_popover/__snapshots__/quick_select.test.js.snap
@@ -0,0 +1,433 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EuiSuperDatePicker is rendered 1`] = `
+
+
+
+
+
+ Quick select
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Apply
+
+
+
+
+
+
+`;
+
+exports[`EuiSuperDatePicker prevQuickSelect 1`] = `
+
+
+
+
+
+ Quick select
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Apply
+
+
+
+
+
+
+`;
diff --git a/src/components/date_picker/super_date_picker/quick_select_popover/commonly_used_time_ranges.js b/src/components/date_picker/super_date_picker/quick_select_popover/commonly_used_time_ranges.js
new file mode 100644
index 00000000000..a80774719e4
--- /dev/null
+++ b/src/components/date_picker/super_date_picker/quick_select_popover/commonly_used_time_ranges.js
@@ -0,0 +1,47 @@
+
+import PropTypes from 'prop-types';
+import React, { Fragment } from 'react';
+import { commonlyUsedRangeShape } from '../types';
+
+import { EuiFlexGrid, EuiFlexItem } from '../../../flex';
+import { EuiTitle } from '../../../title';
+import { EuiSpacer } from '../../../spacer';
+import { EuiLink } from '../../../link';
+import { EuiText } from '../../../text';
+import { EuiHorizontalRule } from '../../../horizontal_rule';
+
+export function EuiCommonlyUsedTimeRanges({ applyTime, commonlyUsedRanges }) {
+ const links = commonlyUsedRanges.map(({ start, end, label }) => {
+ const applyCommonlyUsed = () => {
+ applyTime({ start, end });
+ };
+ return (
+
+
+ {label}
+
+
+ );
+ });
+
+ return (
+
+ Commonly used
+
+
+
+ {links}
+
+
+
+
+ );
+}
+
+EuiCommonlyUsedTimeRanges.propTypes = {
+ applyTime: PropTypes.func.isRequired,
+ commonlyUsedRanges: PropTypes.arrayOf(commonlyUsedRangeShape).isRequired,
+};
diff --git a/src/components/date_picker/super_date_picker/quick_select_popover/quick_select.js b/src/components/date_picker/super_date_picker/quick_select_popover/quick_select.js
new file mode 100644
index 00000000000..3d06c1dfa9b
--- /dev/null
+++ b/src/components/date_picker/super_date_picker/quick_select_popover/quick_select.js
@@ -0,0 +1,194 @@
+
+import PropTypes from 'prop-types';
+import React, { Component, Fragment } from 'react';
+import moment from 'moment';
+import dateMath from '@elastic/datemath';
+
+import { EuiButton, EuiButtonIcon } from '../../../button';
+import { EuiFlexGroup, EuiFlexItem } from '../../../flex';
+import { EuiTitle } from '../../../title';
+import { EuiSpacer } from '../../../spacer';
+import { EuiFormRow, EuiSelect, EuiFieldNumber } from '../../../form';
+import { EuiToolTip } from '../../../tool_tip';
+import { EuiHorizontalRule } from '../../../horizontal_rule';
+
+import { timeUnits } from '../time_units';
+
+const LAST = 'last';
+const NEXT = 'next';
+
+const timeTenseOptions = [
+ { value: LAST, text: 'Last' },
+ { value: NEXT, text: 'Next' },
+];
+const timeUnitsOptions = Object.keys(timeUnits).map(key => {
+ return { value: key, text: `${timeUnits[key]}s` };
+});
+
+export class EuiQuickSelect extends Component {
+ constructor(props) {
+ super(props);
+
+ const { timeTense, timeValue, timeUnits } = this.props.prevQuickSelect;
+ this.state = {
+ timeTense: timeTense ? timeTense : LAST,
+ timeValue: timeValue ? timeValue : 15,
+ timeUnits: timeUnits ? timeUnits : 'm',
+ };
+ }
+
+ onTimeTenseChange = (evt) => {
+ this.setState({
+ timeTense: evt.target.value,
+ });
+ }
+
+ onTimeValueChange = (evt) => {
+ const sanitizedValue = parseInt(evt.target.value, 10);
+ this.setState({
+ timeValue: isNaN(sanitizedValue) ? '' : sanitizedValue,
+ });
+ }
+
+ onTimeUnitsChange = (evt) => {
+ this.setState({
+ timeUnits: evt.target.value,
+ });
+ }
+
+ applyQuickSelect = () => {
+ const {
+ timeTense,
+ timeValue,
+ timeUnits,
+ } = this.state;
+
+ if (timeTense === NEXT) {
+ this.props.applyTime({
+ start: 'now',
+ end: `now+${timeValue}${timeUnits}`,
+ quickSelect: { ...this.state },
+ });
+ return;
+ }
+
+ this.props.applyTime({
+ start: `now-${timeValue}${timeUnits}`,
+ end: 'now',
+ quickSelect: { ...this.state },
+ });
+ }
+
+ getBounds = () => {
+ return {
+ min: dateMath.parse(this.props.start),
+ max: dateMath.parse(this.props.end, { roundUp: true }),
+ };
+ }
+
+ stepForward = () => {
+ const { min, max } = this.getBounds();
+ const diff = max.diff(min);
+ this.props.applyTime({
+ start: moment(max).add(1, 'ms').toISOString(),
+ end: moment(max).add(diff + 1, 'ms').toISOString(),
+ keepPopoverOpen: true,
+ });
+ }
+
+ stepBackward = () => {
+ const { min, max } = this.getBounds();
+ const diff = max.diff(min);
+ this.props.applyTime({
+ start: moment(min).subtract(diff + 1, 'ms').toISOString(),
+ end: moment(min).subtract(1, 'ms').toISOString(),
+ keepPopoverOpen: true,
+ });
+ }
+
+ render() {
+ return (
+
+
+
+ Quick select
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Apply
+
+
+
+
+
+
+ );
+ }
+}
+
+EuiQuickSelect.propTypes = {
+ applyTime: PropTypes.func.isRequired,
+ start: PropTypes.string.isRequired,
+ end: PropTypes.string.isRequired,
+ prevQuickSelect: PropTypes.object,
+};
+
+EuiQuickSelect.defaultProps = {
+ prevQuickSelect: {},
+};
diff --git a/src/components/date_picker/super_date_picker/quick_select_popover/quick_select.test.js b/src/components/date_picker/super_date_picker/quick_select_popover/quick_select.test.js
new file mode 100644
index 00000000000..49733a1b676
--- /dev/null
+++ b/src/components/date_picker/super_date_picker/quick_select_popover/quick_select.test.js
@@ -0,0 +1,41 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import {
+ EuiQuickSelect,
+} from './quick_select';
+
+const noop = () => {};
+const defaultProps = {
+ applyTime: noop,
+ start: 'now-15m',
+ end: 'now',
+};
+describe('EuiSuperDatePicker', () => {
+ test('is rendered', () => {
+ const component = shallow(
+
+ );
+
+ expect(component)
+ .toMatchSnapshot();
+ });
+
+ test('prevQuickSelect', () => {
+ const component = shallow(
+
+ );
+
+ expect(component)
+ .toMatchSnapshot();
+ });
+});
diff --git a/src/components/date_picker/super_date_picker/quick_select_popover/quick_select_popover.js b/src/components/date_picker/super_date_picker/quick_select_popover/quick_select_popover.js
new file mode 100644
index 00000000000..889f5ba3f34
--- /dev/null
+++ b/src/components/date_picker/super_date_picker/quick_select_popover/quick_select_popover.js
@@ -0,0 +1,118 @@
+
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import { commonlyUsedRangeShape, recentlyUsedRangeShape } from '../types';
+
+import {
+ EuiButtonEmpty,
+} from '../../../button';
+
+import {
+ EuiIcon,
+} from '../../../icon';
+
+import {
+ EuiPopover,
+} from '../../../popover';
+
+import { EuiQuickSelect } from './quick_select';
+import { EuiCommonlyUsedTimeRanges } from './commonly_used_time_ranges';
+import { EuiRecentlyUsed } from './recently_used';
+import { EuiRefreshInterval } from './refresh_interval';
+
+export class EuiQuickSelectPopover extends Component {
+
+ state = {
+ isOpen: false,
+ }
+
+ closePopover = () => {
+ this.setState({ isOpen: false });
+ }
+
+ togglePopover = () => {
+ this.setState((prevState) => ({
+ isOpen: !prevState.isOpen
+ }));
+ }
+
+ applyTime = ({ start, end, quickSelect, keepPopoverOpen = false }) => {
+ this.props.applyTime({
+ start,
+ end,
+ });
+ if (quickSelect) {
+ this.setState({ prevQuickSelect: quickSelect });
+ }
+ if (!keepPopoverOpen) {
+ this.closePopover();
+ }
+ }
+
+ render() {
+ const quickSelectButton = (
+
+
+
+ );
+
+ return (
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+EuiQuickSelectPopover.propTypes = {
+ applyTime: PropTypes.func.isRequired,
+ start: PropTypes.string.isRequired,
+ end: PropTypes.string.isRequired,
+ applyRefreshInterval: PropTypes.func,
+ isPaused: PropTypes.bool.isRequired,
+ refreshInterval: PropTypes.number.isRequired,
+ commonlyUsedRanges: PropTypes.arrayOf(commonlyUsedRangeShape).isRequired,
+ dateFormat: PropTypes.string.isRequired,
+ recentlyUsedRanges: PropTypes.arrayOf(recentlyUsedRangeShape).isRequired,
+};
diff --git a/src/components/date_picker/super_date_picker/quick_select_popover/recently_used.js b/src/components/date_picker/super_date_picker/quick_select_popover/recently_used.js
new file mode 100644
index 00000000000..ab3afc75de5
--- /dev/null
+++ b/src/components/date_picker/super_date_picker/quick_select_popover/recently_used.js
@@ -0,0 +1,55 @@
+
+import PropTypes from 'prop-types';
+import React, { Fragment } from 'react';
+import { commonlyUsedRangeShape, recentlyUsedRangeShape } from '../types';
+import { prettyDuration } from '../pretty_duration';
+
+import { EuiFlexGroup, EuiFlexItem } from '../../../flex';
+import { EuiTitle } from '../../../title';
+import { EuiSpacer } from '../../../spacer';
+import { EuiLink } from '../../../link';
+import { EuiText } from '../../../text';
+import { EuiHorizontalRule } from '../../../horizontal_rule';
+
+export function EuiRecentlyUsed({ applyTime, commonlyUsedRanges, dateFormat, recentlyUsedRanges }) {
+ if (recentlyUsedRanges.length === 0) {
+ return null;
+ }
+
+ const links = recentlyUsedRanges.map(({ start, end }) => {
+ const applyRecentlyUsed = () => {
+ applyTime({ start, end });
+ };
+ return (
+
+
+ {prettyDuration(start, end, commonlyUsedRanges, dateFormat)}
+
+
+ );
+ });
+
+ return (
+
+ Recently used date ranges
+
+
+
+ {links}
+
+
+
+
+ );
+}
+
+EuiRecentlyUsed.propTypes = {
+ applyTime: PropTypes.func.isRequired,
+ commonlyUsedRanges: PropTypes.arrayOf(commonlyUsedRangeShape).isRequired,
+ dateFormat: PropTypes.string.isRequired,
+ recentlyUsedRanges: PropTypes.arrayOf(recentlyUsedRangeShape),
+};
+
+EuiRecentlyUsed.defaultProps = {
+ recentlyUsedRanges: []
+};
diff --git a/src/components/date_picker/super_date_picker/quick_select_popover/refresh_interval.js b/src/components/date_picker/super_date_picker/quick_select_popover/refresh_interval.js
new file mode 100644
index 00000000000..59f741b060c
--- /dev/null
+++ b/src/components/date_picker/super_date_picker/quick_select_popover/refresh_interval.js
@@ -0,0 +1,137 @@
+import PropTypes from 'prop-types';
+import React, { Component, Fragment } from 'react';
+import { timeUnits } from '../time_units';
+
+import { EuiFlexGroup, EuiFlexItem } from '../../../flex';
+import { EuiTitle } from '../../../title';
+import { EuiSpacer } from '../../../spacer';
+import { EuiFormRow, EuiSelect, EuiFieldNumber } from '../../../form';
+import { EuiButton } from '../../../button';
+
+const refreshUnitsOptions = Object.keys(timeUnits)
+ .filter(timeUnit => {
+ return timeUnit === 'h' || timeUnit === 'm';
+ })
+ .map(timeUnit => {
+ return { value: timeUnit, text: `${timeUnits[timeUnit]}s` };
+ });
+
+const MILLISECONDS_IN_MINUTE = 1000 * 60;
+const MILLISECONDS_IN_HOUR = MILLISECONDS_IN_MINUTE * 60;
+
+function convertMilliseconds(milliseconds) {
+ if (milliseconds > MILLISECONDS_IN_HOUR) {
+ return {
+ units: 'h',
+ value: milliseconds / MILLISECONDS_IN_HOUR
+ };
+ }
+
+ return {
+ units: 'm',
+ value: milliseconds / MILLISECONDS_IN_MINUTE
+ };
+}
+
+export class EuiRefreshInterval extends Component {
+
+ constructor(props) {
+ super(props);
+
+ const { value, units } = convertMilliseconds(props.refreshInterval);
+ this.state = {
+ value,
+ units,
+ };
+ }
+
+ onValueChange = (evt) => {
+ const sanitizedValue = parseInt(evt.target.value, 10);
+ this.setState({
+ value: isNaN(sanitizedValue) ? '' : sanitizedValue,
+ }, this.applyRefreshInterval);
+ };
+
+ onUnitsChange = (evt) => {
+ this.setState({
+ units: evt.target.value,
+ }, this.applyRefreshInterval);
+ }
+
+ applyRefreshInterval = () => {
+ if (this.state.value === '') {
+ return;
+ }
+
+ const valueInMilliSeconds = this.state.units === 'h'
+ ? this.state.value * MILLISECONDS_IN_HOUR
+ : this.state.value * MILLISECONDS_IN_MINUTE;
+
+ this.props.applyRefreshInterval({
+ refreshInterval: valueInMilliSeconds,
+ isPaused: valueInMilliSeconds <= 0 ? true : this.props.isPaused,
+ });
+ }
+
+ toogleRefresh = () => {
+ this.props.applyRefreshInterval({
+ isPaused: !this.props.isPaused
+ });
+ }
+
+ render() {
+ if (!this.props.applyRefreshInterval) {
+ return null;
+ }
+
+ return (
+
+ Refresh every
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {this.props.isPaused ? 'Start' : 'Stop'}
+
+
+
+
+
+ );
+ }
+}
+
+EuiRefreshInterval.propTypes = {
+ applyRefreshInterval: PropTypes.func,
+ isPaused: PropTypes.bool.isRequired,
+ refreshInterval: PropTypes.number.isRequired,
+};
diff --git a/src/components/date_picker/super_date_picker/relative_options.js b/src/components/date_picker/super_date_picker/relative_options.js
new file mode 100644
index 00000000000..677191356cf
--- /dev/null
+++ b/src/components/date_picker/super_date_picker/relative_options.js
@@ -0,0 +1,27 @@
+
+export const relativeOptions = [
+ { text: 'Seconds ago', value: 's' },
+ { text: 'Minutes ago', value: 'm' },
+ { text: 'Hours ago', value: 'h' },
+ { text: 'Days ago', value: 'd' },
+ { text: 'Weeks ago', value: 'w' },
+ { text: 'Months ago', value: 'M' },
+ { text: 'Years ago', value: 'y' },
+
+ { text: 'Seconds from now', value: 's+' },
+ { text: 'Minutes from now', value: 'm+' },
+ { text: 'Hours from now', value: 'h+' },
+ { text: 'Days from now', value: 'd+' },
+ { text: 'Weeks from now', value: 'w+' },
+ { text: 'Months from now', value: 'M+' },
+ { text: 'Years from now', value: 'y+' },
+];
+
+export const relativeUnitsFromLargestToSmallest = relativeOptions
+ .filter(({ value }) => {
+ return !value.includes('+');
+ })
+ .map(({ value }) => {
+ return value;
+ })
+ .reverse();
diff --git a/src/components/date_picker/super_date_picker/relative_utils.js b/src/components/date_picker/super_date_picker/relative_utils.js
new file mode 100644
index 00000000000..4732080fe1b
--- /dev/null
+++ b/src/components/date_picker/super_date_picker/relative_utils.js
@@ -0,0 +1,62 @@
+
+import dateMath from '@elastic/datemath';
+import moment from 'moment';
+import _ from 'lodash';
+import { relativeUnitsFromLargestToSmallest } from './relative_options';
+
+const ROUND_DELIMETER = '/';
+
+export function parseRelativeParts(value) {
+ const matches = _.isString(value) && value.match(/now(([\-\+])([0-9]+)([smhdwMy])(\/[smhdwMy])?)?/);
+
+ const isNow = matches && !matches[1];
+ const operator = matches && matches[2];
+ const count = matches && matches[3];
+ const unit = matches && matches[4];
+ const roundBy = matches && matches[5];
+
+ if (isNow) {
+ return { count: 0, unit: 's', round: false };
+ }
+
+ if (count && unit) {
+ const isRounded = roundBy ? true : false;
+ return {
+ count: parseInt(count, 10),
+ unit: operator === '+' ? `${unit}+` : unit,
+ round: isRounded,
+ roundUnit: isRounded ? roundBy.replace(ROUND_DELIMETER, '') : undefined,
+ };
+ }
+
+ const results = { count: 0, unit: 's', round: false };
+ const duration = moment.duration(moment().diff(dateMath.parse(value)));
+ let unitOp = '';
+ for (let i = 0; i < relativeUnitsFromLargestToSmallest.length; i++) {
+ const as = duration.as(relativeUnitsFromLargestToSmallest[i]);
+ if (as < 0) unitOp = '+';
+ if (Math.abs(as) > 1) {
+ results.count = Math.round(Math.abs(as));
+ results.unit = relativeUnitsFromLargestToSmallest[i] + unitOp;
+ results.round = false;
+ break;
+ }
+ }
+ return results;
+}
+
+export function toRelativeStringFromParts(relativeParts) {
+ const count = _.get(relativeParts, `count`, 0);
+ const isRounded = _.get(relativeParts, `round`, false);
+
+ if (count === 0 && !isRounded) {
+ return 'now';
+ }
+
+ const matches = _.get(relativeParts, `unit`, 's').match(/([smhdwMy])(\+)?/);
+ const unit = matches[1];
+ const operator = matches && matches[2] ? matches[2] : '-';
+ const round = isRounded ? `${ROUND_DELIMETER}${unit}` : '';
+
+ return `now${operator}${count}${unit}${round}`;
+}
diff --git a/src/components/date_picker/super_date_picker/relative_utils.test.js b/src/components/date_picker/super_date_picker/relative_utils.test.js
new file mode 100644
index 00000000000..43fe7b048c9
--- /dev/null
+++ b/src/components/date_picker/super_date_picker/relative_utils.test.js
@@ -0,0 +1,121 @@
+
+import { parseRelativeParts, toRelativeStringFromParts } from './relative_utils';
+import moment from 'moment';
+
+describe('parseRelativeParts', () => {
+ describe('relative', () => {
+ it('should parse now', () => {
+ const out = parseRelativeParts('now');
+ expect(out).toEqual({
+ count: 0,
+ unit: 's',
+ round: false
+ });
+ });
+
+ it('should parse now-2h', () => {
+ const out = parseRelativeParts('now-2h');
+ expect(out).toEqual({
+ count: 2,
+ unit: 'h',
+ round: false
+ });
+ });
+
+ it('should parse now-2h/h', () => {
+ const out = parseRelativeParts('now-2h/h');
+ expect(out).toEqual({
+ count: 2,
+ unit: 'h',
+ round: true,
+ roundUnit: 'h',
+ });
+ });
+
+ it('should parse now+10m/m', () => {
+ const out = parseRelativeParts('now+10m/m');
+ expect(out).toEqual({
+ count: 10,
+ unit: 'm+',
+ round: true,
+ roundUnit: 'm',
+ });
+ });
+ });
+
+ describe('absolute', () => {
+ it('should parse now', () => {
+ const out = parseRelativeParts(moment().toDate().toISOString());
+ expect(out).toEqual({
+ count: 0,
+ unit: 's',
+ round: false
+ });
+ });
+
+ it('should parse 3 months ago', () => {
+ const out = parseRelativeParts(moment().subtract(3, 'M').toDate().toISOString());
+ expect(out).toEqual({
+ count: 3,
+ unit: 'M',
+ round: false
+ });
+ });
+
+ it('should parse 15 minutes ago', () => {
+ const out = parseRelativeParts(moment().subtract(15, 'm').toDate().toISOString());
+ expect(out).toEqual({
+ count: 15,
+ unit: 'm',
+ round: false
+ });
+ });
+
+ it('should parse 2 hours from now', () => {
+ const out = parseRelativeParts(moment().add(2, 'h').toDate().toISOString());
+ expect(out).toEqual({
+ count: 2,
+ unit: 'h+',
+ round: false
+ });
+ });
+ });
+});
+
+describe('toRelativeStringFromParts', () => {
+ it('should convert parts to now', () => {
+ const out = toRelativeStringFromParts({
+ count: 0,
+ unit: 's',
+ round: false
+ });
+ expect(out).toEqual('now');
+ });
+
+ it('should convert parts to now-2h', () => {
+ const out = toRelativeStringFromParts({
+ count: 2,
+ unit: 'h',
+ round: false
+ });
+ expect(out).toEqual('now-2h');
+ });
+
+ it('should convert parts to now-2h/h', () => {
+ const out = toRelativeStringFromParts({
+ count: 2,
+ unit: 'h',
+ round: true
+ });
+ expect(out).toEqual('now-2h/h');
+ });
+
+ it('should convert parts to now+10m/m', () => {
+ const out = toRelativeStringFromParts({
+ count: 10,
+ unit: 'm+',
+ round: true
+ });
+ expect(out).toEqual('now+10m/m');
+ });
+});
diff --git a/src/components/date_picker/super_date_picker/super_date_picker.js b/src/components/date_picker/super_date_picker/super_date_picker.js
new file mode 100644
index 00000000000..97756ccd20b
--- /dev/null
+++ b/src/components/date_picker/super_date_picker/super_date_picker.js
@@ -0,0 +1,311 @@
+
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import { commonlyUsedRangeShape, recentlyUsedRangeShape } from './types';
+import { prettyDuration, showPrettyDuration } from './pretty_duration';
+
+import dateMath from '@elastic/datemath';
+
+import { EuiQuickSelectPopover } from './quick_select_popover/quick_select_popover';
+import { EuiDatePopoverButton } from './date_popover/date_popover_button';
+
+import { EuiDatePickerRange } from '../date_picker_range';
+import { EuiFormControlLayout } from '../../form';
+import { EuiButton, EuiButtonEmpty } from '../../button';
+import { EuiFlexGroup, EuiFlexItem } from '../../flex';
+import { EuiToolTip } from '../../tool_tip';
+
+// EuiSuperDatePicker has state that needs to be reset when start or end change.
+// Instead of using getDerivedStateFromProps, this wrapper adds a key to the component.
+// When a key changes, React will create a new component instance rather than update the current one
+// https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html#recommendation-fully-uncontrolled-component-with-a-key
+export function WrappedEuiSuperDatePicker(props) {
+ return (
+
+ );
+}
+
+WrappedEuiSuperDatePicker.propTypes = {
+ isLoading: PropTypes.bool,
+ /**
+ * String as either datemath (e.g.: now, now-15m, now-15m/m) or
+ * absolute date in the format 'YYYY-MM-DDTHH:mm:ss.sssZ'
+ */
+ start: PropTypes.string,
+ /**
+ * String as either datemath (e.g.: now, now-15m, now-15m/m) or
+ * absolute date in the format 'YYYY-MM-DDTHH:mm:ss.sssZ'
+ */
+ end: PropTypes.string,
+ /**
+ * Callback for when the time changes. Called with { start, end }
+ */
+ onTimeChange: PropTypes.func.isRequired,
+ isPaused: PropTypes.bool,
+ /**
+ * Refresh interval in milliseconds
+ */
+ refreshInterval: PropTypes.number,
+ /**
+ * Callback for when the refresh interval changes. Called with { isPaused, refreshInterval }
+ * Supply onRefreshChange to show refresh interval inputs in quick select popover
+ */
+ onRefreshChange: PropTypes.func,
+
+ /**
+ * 'start' and 'end' must be string as either datemath (e.g.: now, now-15m, now-15m/m) or
+ * absolute date in the format 'YYYY-MM-DDTHH:mm:ss.sssZ'
+ */
+ commonlyUsedRanges: PropTypes.arrayOf(commonlyUsedRangeShape),
+ dateFormat: PropTypes.string,
+ /**
+ * 'start' and 'end' must be string as either datemath (e.g.: now, now-15m, now-15m/m) or
+ * absolute date in the format 'YYYY-MM-DDTHH:mm:ss.sssZ'
+ */
+ recentlyUsedRanges: PropTypes.arrayOf(recentlyUsedRangeShape),
+};
+
+WrappedEuiSuperDatePicker.defaultProps = {
+ start: 'now-15m',
+ end: 'now',
+ isPaused: true,
+ refreshInterval: 0,
+ commonlyUsedRanges: [
+ { start: 'now/d', end: 'now/d', label: 'Today' },
+ { start: 'now-1d/d', end: 'now-1d/d', label: 'Yesterday' },
+ { start: 'now/w', end: 'now/w', label: 'This week' },
+ { start: 'now/w', end: 'now', label: 'Week to date' },
+ { start: 'now/M', end: 'now/M', label: 'This month' },
+ { start: 'now/M', end: 'now', label: 'Month to date' },
+ { start: 'now/y', end: 'now/y', label: 'This year' },
+ { start: 'now/y', end: 'now', label: 'Year to date' },
+ ],
+ dateFormat: 'MMM D, YYYY @ HH:mm:ss.SSS',
+ recentlyUsedRanges: [],
+};
+
+export class EuiSuperDatePicker extends Component {
+
+ constructor(props) {
+ super(props);
+
+ const {
+ start,
+ end,
+ commonlyUsedRanges
+ } = this.props;
+
+ this.state = {
+ start,
+ end,
+ isInvalid: false,
+ hasChanged: false,
+ showPrettyDuration: showPrettyDuration(start, end, commonlyUsedRanges),
+ };
+ }
+
+ componentWillUnmount() {
+ this._isMounted = false;
+ }
+
+ componentDidMount() {
+ this._isMounted = true;
+ }
+
+ setTootipRef = node => (this.tooltip = node);
+
+ showTooltip = () => {
+ if (!this._isMounted) {
+ return;
+ }
+ this.tooltip.showToolTip();
+ }
+ hideTooltip = () => {
+ if (!this._isMounted) {
+ return;
+ }
+ this.tooltip.hideToolTip();
+ }
+
+ setTime = ({ start, end }) => {
+ const startMoment = dateMath.parse(start);
+ const endMoment = dateMath.parse(end, { roundUp: true });
+ const isInvalid = (start === 'now' && end === 'now') || startMoment.isAfter(endMoment);
+
+ if (this.tooltipTimeout) {
+ clearTimeout(this.tooltipTimeout);
+ this.hideTooltip();
+ this.tooltipTimeout = null;
+ }
+
+ this.setState({
+ start,
+ end,
+ isInvalid,
+ hasChanged: true,
+ });
+
+ if (!isInvalid) {
+ this.showTooltip();
+ this.tooltipTimeout = setTimeout(() => {
+ this.hideTooltip();
+ }, 2000);
+ }
+ }
+
+ setStart = (start) => {
+ this.setTime({ start, end: this.state.end });
+ }
+
+ setEnd = (end) => {
+ this.setTime({ start: this.state.start, end });
+ }
+
+ applyTime = () => {
+ this.props.onTimeChange({ start: this.state.start, end: this.state.end });
+ }
+
+ applyQuickTime = ({ start, end }) => {
+ this.props.onTimeChange({ start, end });
+ }
+
+ hidePrettyDuration = () => {
+ this.setState({ showPrettyDuration: false });
+ }
+
+ renderDatePickerRange = () => {
+ const {
+ start,
+ end,
+ hasChanged,
+ isInvalid,
+ } = this.state;
+
+ if (this.state.showPrettyDuration) {
+ return (
+ }
+ endDateControl={
}
+ >
+
+ {prettyDuration(start, end, this.props.commonlyUsedRanges, this.props.dateFormat)}
+
+
+ Show dates
+
+
+ );
+ }
+
+ return (
+
+ }
+ endDateControl={
+
+ }
+ />
+ );
+ }
+
+ renderUpdateButton = () => {
+ let buttonText = 'Refresh';
+ if (this.state.hasChanged || this.props.isLoading) {
+ buttonText = this.props.isLoading ? 'Updating' : 'Update';
+ }
+ return (
+
+
+ {buttonText}
+
+
+ );
+ }
+
+ render() {
+ const quickSelect = (
+
+ );
+ return (
+
+
+
+
+ {this.renderDatePickerRange()}
+
+
+
+
+ {this.renderUpdateButton()}
+
+
+
+ );
+ }
+}
+
+EuiSuperDatePicker.propTypes = WrappedEuiSuperDatePicker.propTypes;
+EuiSuperDatePicker.defaultProps = WrappedEuiSuperDatePicker.defaultProps;
+
diff --git a/src/components/date_picker/super_date_picker/super_date_picker.test.js b/src/components/date_picker/super_date_picker/super_date_picker.test.js
new file mode 100644
index 00000000000..5fa4f20e4ed
--- /dev/null
+++ b/src/components/date_picker/super_date_picker/super_date_picker.test.js
@@ -0,0 +1,33 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import {
+ EuiSuperDatePicker,
+} from './super_date_picker';
+
+const noop = () => {};
+
+describe('EuiSuperDatePicker', () => {
+ test('is rendered', () => {
+ const component = shallow(
+
+ );
+
+ expect(component)
+ .toMatchSnapshot();
+ });
+
+ test('isLoading', () => {
+ const component = shallow(
+
+ );
+
+ expect(component)
+ .toMatchSnapshot();
+ });
+});
diff --git a/src/components/date_picker/super_date_picker/time_units.js b/src/components/date_picker/super_date_picker/time_units.js
new file mode 100644
index 00000000000..73766803588
--- /dev/null
+++ b/src/components/date_picker/super_date_picker/time_units.js
@@ -0,0 +1,20 @@
+
+export const timeUnits = {
+ s: 'second',
+ m: 'minute',
+ h: 'hour',
+ d: 'day',
+ w: 'week',
+ M: 'month',
+ y: 'year'
+};
+
+export const timeUnitsPlural = {
+ s: 'seconds',
+ m: 'minutes',
+ h: 'hours',
+ d: 'days',
+ w: 'weeks',
+ M: 'months',
+ y: 'years'
+};
diff --git a/src/components/date_picker/super_date_picker/types.js b/src/components/date_picker/super_date_picker/types.js
new file mode 100644
index 00000000000..7e58ad0acfa
--- /dev/null
+++ b/src/components/date_picker/super_date_picker/types.js
@@ -0,0 +1,12 @@
+import PropTypes from 'prop-types';
+
+export const commonlyUsedRangeShape = PropTypes.shape({
+ start: PropTypes.string.isRequired,
+ end: PropTypes.string.isRequired,
+ label: PropTypes.string.isRequired,
+});
+
+export const recentlyUsedRangeShape = PropTypes.shape({
+ start: PropTypes.string.isRequired,
+ end: PropTypes.string.isRequired,
+});
diff --git a/src/components/index.js b/src/components/index.js
index abcebea19a6..a6d565280f5 100644
--- a/src/components/index.js
+++ b/src/components/index.js
@@ -73,6 +73,7 @@ export {
export {
EuiDatePicker,
EuiDatePickerRange,
+ EuiSuperDatePicker,
} from './date_picker';
export {
diff --git a/yarn.lock b/yarn.lock
index dc32a39eb95..28e52972457 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -780,6 +780,13 @@
lodash "^4.17.10"
to-fast-properties "^2.0.0"
+"@elastic/datemath@^5.0.2":
+ version "5.0.2"
+ resolved "https://registry.yarnpkg.com/@elastic/datemath/-/datemath-5.0.2.tgz#1e62fe7137acd6ebcde9a794ef22b91820c9e6cf"
+ integrity sha512-MYU7KedGPMYu3ljgrO3tY8I8rD73lvBCltd78k5avDIv/6gMbuhKXsMhkEPbb9angs9hR/2ADk0QcGbVxUBXUw==
+ dependencies:
+ tslib "^1.9.3"
+
"@elastic/eslint-config-kibana@^0.15.0":
version "0.15.0"
resolved "https://registry.yarnpkg.com/@elastic/eslint-config-kibana/-/eslint-config-kibana-0.15.0.tgz#a552793497cdfc1829c2f9b7cd7018eb008f1606"
@@ -13508,7 +13515,7 @@ trim@0.0.1:
dependencies:
glob "^6.0.4"
-tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0:
+tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3:
version "1.9.3"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286"
integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==