From cddaf2892804566492eb55ca1f1bc743da5c9870 Mon Sep 17 00:00:00 2001 From: Dimitri Mitropoulos Date: Mon, 13 Apr 2020 18:27:15 -0400 Subject: [PATCH] converts all DatePicker family of components to TypeScript (#2891) * converts all DatePicker family of components to TypeScript * updates ScreenReaderOnly text per review feedback * widens RefCallback usage to just Ref per review feedback * export ReactDatePicker and ReactDatePickerProps from @elastic/eui * Move date picker onChange string argument definition to super date picker * Semi-revert EuiAbsoluteTab's onChange call * Cleaned up aria-describedby in EuiRefreshInteral * makes valueAsMoment usage more consistent/idiomatic * makes optional event arguments consistent * Clean up regressions found in Kibana testing Co-authored-by: Chandler Prall --- CHANGELOG.md | 1 + src/components/common.ts | 20 + ...test.js.snap => date_picker.test.tsx.snap} | 1 - ...s.snap => date_picker_range.test.tsx.snap} | 0 ...te_picker.test.js => date_picker.test.tsx} | 8 +- .../{date_picker.js => date_picker.tsx} | 236 +++++------- ...nge.test.js => date_picker_range.test.tsx} | 0 ..._picker_range.js => date_picker_range.tsx} | 103 ++--- src/components/date_picker/index.d.ts | 104 ----- src/components/date_picker/index.js | 10 - src/components/date_picker/index.ts | 28 ++ .../date_picker/react-datepicker.d.ts | 86 ++++- ...s.snap => super_date_picker.test.tsx.snap} | 1 - ...snap => super_update_button.test.tsx.snap} | 0 .../super_date_picker/async_interval.js | 22 -- ...nterval.test.js => async_interval.test.ts} | 21 +- .../super_date_picker/async_interval.ts | 25 ++ ...{date_modes.test.js => date_modes.test.ts} | 2 +- .../{date_modes.js => date_modes.ts} | 18 +- .../{absolute_tab.js => absolute_tab.tsx} | 95 +++-- ...over_button.js => date_popover_button.tsx} | 54 ++- ...er_content.js => date_popover_content.tsx} | 54 +-- .../super_date_picker/date_popover/index.ts | 10 + .../{relative_tab.js => relative_tab.tsx} | 94 +++-- .../date_picker/super_date_picker/index.js | 5 - .../date_picker/super_date_picker/index.ts | 17 + ...ration.test.js => pretty_duration.test.ts} | 0 ...{pretty_duration.js => pretty_duration.ts} | 32 +- ...est.js.snap => quick_select.test.tsx.snap} | 16 +- ...nap => quick_select_popover.test.tsx.snap} | 8 +- ...anges.js => commonly_used_time_ranges.tsx} | 31 +- .../quick_select_popover/index.ts | 14 + ...k_select.test.js => quick_select.test.tsx} | 4 +- .../{quick_select.js => quick_select.tsx} | 122 +++--- ....test.js => quick_select_popover.test.tsx} | 7 +- ...ct_popover.js => quick_select_popover.tsx} | 120 +++--- ...ils.test.js => quick_select_utils.test.ts} | 24 +- ..._select_utils.js => quick_select_utils.ts} | 68 ++-- .../{recently_used.js => recently_used.tsx} | 29 +- ...fresh_interval.js => refresh_interval.tsx} | 135 ++++--- .../super_date_picker/relative_options.ts | 17 +- ...e_utils.test.js => relative_utils.test.ts} | 0 .../{relative_utils.js => relative_utils.ts} | 21 +- ...ker.test.js => super_date_picker.test.tsx} | 26 +- ...r_date_picker.js => super_date_picker.tsx} | 356 ++++++++++-------- ...n.test.js => super_update_button.test.tsx} | 0 ...date_button.js => super_update_button.tsx} | 55 +-- .../super_date_picker/time_units.js | 19 - .../super_date_picker/time_units.ts | 21 ++ .../date_picker/super_date_picker/types.js | 17 - src/components/date_picker/types.ts | 74 ++++ src/components/index.d.ts | 5 +- 52 files changed, 1252 insertions(+), 984 deletions(-) rename src/components/date_picker/__snapshots__/{date_picker.test.js.snap => date_picker.test.tsx.snap} (99%) rename src/components/date_picker/__snapshots__/{date_picker_range.test.js.snap => date_picker_range.test.tsx.snap} (100%) rename src/components/date_picker/{date_picker.test.js => date_picker.test.tsx} (84%) rename src/components/date_picker/{date_picker.js => date_picker.tsx} (65%) rename src/components/date_picker/{date_picker_range.test.js => date_picker_range.test.tsx} (100%) rename src/components/date_picker/{date_picker_range.js => date_picker_range.tsx} (61%) delete mode 100644 src/components/date_picker/index.d.ts delete mode 100644 src/components/date_picker/index.js create mode 100644 src/components/date_picker/index.ts rename src/components/date_picker/super_date_picker/__snapshots__/{super_date_picker.test.js.snap => super_date_picker.test.tsx.snap} (98%) rename src/components/date_picker/super_date_picker/__snapshots__/{super_update_button.test.js.snap => super_update_button.test.tsx.snap} (100%) delete mode 100644 src/components/date_picker/super_date_picker/async_interval.js rename src/components/date_picker/super_date_picker/{async_interval.test.js => async_interval.test.ts} (85%) create mode 100644 src/components/date_picker/super_date_picker/async_interval.ts rename src/components/date_picker/super_date_picker/{date_modes.test.js => date_modes.test.ts} (97%) rename src/components/date_picker/super_date_picker/{date_modes.js => date_modes.ts} (59%) rename src/components/date_picker/super_date_picker/date_popover/{absolute_tab.js => absolute_tab.tsx} (50%) rename src/components/date_picker/super_date_picker/date_popover/{date_popover_button.js => date_popover_button.tsx} (61%) rename src/components/date_picker/super_date_picker/date_popover/{date_popover_content.js => date_popover_content.tsx} (71%) create mode 100644 src/components/date_picker/super_date_picker/date_popover/index.ts rename src/components/date_picker/super_date_picker/date_popover/{relative_tab.js => relative_tab.tsx} (66%) delete mode 100644 src/components/date_picker/super_date_picker/index.js create mode 100644 src/components/date_picker/super_date_picker/index.ts rename src/components/date_picker/super_date_picker/{pretty_duration.test.js => pretty_duration.test.ts} (100%) rename src/components/date_picker/super_date_picker/{pretty_duration.js => pretty_duration.ts} (77%) rename src/components/date_picker/super_date_picker/quick_select_popover/__snapshots__/{quick_select.test.js.snap => quick_select.test.tsx.snap} (97%) rename src/components/date_picker/super_date_picker/quick_select_popover/__snapshots__/{quick_select_popover.test.js.snap => quick_select_popover.test.tsx.snap} (95%) rename src/components/date_picker/super_date_picker/quick_select_popover/{commonly_used_time_ranges.js => commonly_used_time_ranges.tsx} (68%) create mode 100644 src/components/date_picker/super_date_picker/quick_select_popover/index.ts rename src/components/date_picker/super_date_picker/quick_select_popover/{quick_select.test.js => quick_select.test.tsx} (100%) rename src/components/date_picker/super_date_picker/quick_select_popover/{quick_select.js => quick_select.tsx} (75%) rename src/components/date_picker/super_date_picker/quick_select_popover/{quick_select_popover.test.js => quick_select_popover.test.tsx} (85%) rename src/components/date_picker/super_date_picker/quick_select_popover/{quick_select_popover.js => quick_select_popover.tsx} (59%) rename src/components/date_picker/super_date_picker/quick_select_popover/{quick_select_utils.test.js => quick_select_utils.test.ts} (71%) rename src/components/date_picker/super_date_picker/quick_select_popover/{quick_select_utils.js => quick_select_utils.ts} (52%) rename src/components/date_picker/super_date_picker/quick_select_popover/{recently_used.js => recently_used.tsx} (70%) rename src/components/date_picker/super_date_picker/quick_select_popover/{refresh_interval.js => refresh_interval.tsx} (60%) rename src/components/date_picker/super_date_picker/{relative_utils.test.js => relative_utils.test.ts} (100%) rename src/components/date_picker/super_date_picker/{relative_utils.js => relative_utils.ts} (72%) rename src/components/date_picker/super_date_picker/{super_date_picker.test.js => super_date_picker.test.tsx} (77%) rename src/components/date_picker/super_date_picker/{super_date_picker.js => super_date_picker.tsx} (61%) rename src/components/date_picker/super_date_picker/{super_update_button.test.js => super_update_button.test.tsx} (100%) rename src/components/date_picker/super_date_picker/{super_update_button.js => super_update_button.tsx} (75%) delete mode 100644 src/components/date_picker/super_date_picker/time_units.js create mode 100644 src/components/date_picker/super_date_picker/time_units.ts delete mode 100644 src/components/date_picker/super_date_picker/types.js create mode 100644 src/components/date_picker/types.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index c81ae86fe2c..002c871251a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## [`master`](https://github.com/elastic/eui/tree/master) - Converted `NavDrawer`, `NavDrawerGroup`, and `NavDrawerFlyout` to TypeScript ([#3268](https://github.com/elastic/eui/pull/3268)) +- Converted `EuiDatePicker`, `EuiDatePickerRange`, `EuiSuperDatePicker`, and `EuiSuperUpdateButton` to TypeScript ([#2891](https://github.com/elastic/eui/pull/2891)) ## [`22.5.0`](https://github.com/elastic/eui/tree/v22.5.0) diff --git a/src/components/common.ts b/src/components/common.ts index 6e61a578fa7..a60a433a356 100644 --- a/src/components/common.ts +++ b/src/components/common.ts @@ -32,6 +32,26 @@ export type PropsOf = C extends SFC ? ComponentProps : never; +// Utility methods for ApplyClassComponentDefaults +type ExtractDefaultProps = T extends { defaultProps: infer D } ? D : never; +type ExtractProps< + C extends new (...args: any) => any, + IT = InstanceType +> = IT extends Component ? P : never; + +/** + * Because of how TypeScript's LibraryManagedAttributes is designed to handle defaultProps (https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-0.html#support-for-defaultprops-in-jsx) + * we can't directly export the props definition as the defaulted values are not made optional, + * because it isn't processed by LibraryManagedAttributes. To get around this, we: + * - remove the props which have default values applied + * - export (Props - Defaults) & Partial + */ +export type ApplyClassComponentDefaults< + C extends new (...args: any) => any, + D = ExtractDefaultProps, + P = ExtractProps +> = Omit & Partial; + /* https://github.com/Microsoft/TypeScript/issues/28339 Problem: Pick and Omit do not distribute over union types, which manifests when diff --git a/src/components/date_picker/__snapshots__/date_picker.test.js.snap b/src/components/date_picker/__snapshots__/date_picker.test.tsx.snap similarity index 99% rename from src/components/date_picker/__snapshots__/date_picker.test.js.snap rename to src/components/date_picker/__snapshots__/date_picker.test.tsx.snap index 0d50659bbed..473823bf096 100644 --- a/src/components/date_picker/__snapshots__/date_picker.test.js.snap +++ b/src/components/date_picker/__snapshots__/date_picker.test.tsx.snap @@ -6,7 +6,6 @@ exports[`EuiDatePicker is rendered 1`] = ` className="euiDatePicker euiDatePicker--shadow" > { test('is rendered', () => { - const component = shallow(); + const component = shallow( + + ); expect(component).toMatchSnapshot(); // snapshot of wrapping dom expect(component.find('ContextConsumer').shallow()).toMatchSnapshot(); // snapshot of DatePicker usage }); describe('localization', () => { - const selectedDate = new Moment('2019-07-01T00:00:00-0700').locale('fr'); + const selectedDate = moment('2019-07-01T00:00:00-0700').locale('fr'); test('accepts the locale prop', () => { const component = mount( diff --git a/src/components/date_picker/date_picker.js b/src/components/date_picker/date_picker.tsx similarity index 65% rename from src/components/date_picker/date_picker.js rename to src/components/date_picker/date_picker.tsx index 6d97c77acf5..bc0c1cba780 100644 --- a/src/components/date_picker/date_picker.js +++ b/src/components/date_picker/date_picker.tsx @@ -1,22 +1,97 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; +import React, { Component, MouseEventHandler, Ref } from 'react'; import classNames from 'classnames'; -import moment from 'moment'; -import { ReactDatePicker as DatePicker } from '../../../packages'; +import { Moment } from 'moment'; // eslint-disable-line import/named -import { EuiFormControlLayout } from '../form/form_control_layout'; - -import { EuiValidatableControl } from '../form/validatable_control'; +import { EuiFormControlLayout, EuiValidatableControl } from '../form'; +import { EuiFormControlLayoutIconsProps } from '../form/form_control_layout/form_control_layout_icons'; import { EuiErrorBoundary } from '../error_boundary'; import { EuiI18nConsumer } from '../context'; +import { ApplyClassComponentDefaults, CommonProps } from '../common'; + +// @ts-ignore the type is provided by react-datepicker.d.ts +import { ReactDatePicker as _ReactDatePicker } from '../../../packages'; +import ReactDatePicker, { ReactDatePickerProps } from './react-datepicker'; // eslint-disable-line import/no-unresolved export const euiDatePickerDefaultDateFormat = 'MM/DD/YYYY'; export const euiDatePickerDefaultTimeFormat = 'hh:mm A'; -export class EuiDatePicker extends Component { +const DatePicker = _ReactDatePicker as typeof ReactDatePicker; + +interface EuiExtendedDatePickerProps extends ReactDatePickerProps { + /** + * Applies classes to the numbered days provided. Check docs for example. + */ + dayClassName?: (date: Moment) => string | null; + + /** + * Makes the input full width + */ + fullWidth?: boolean; + + /** + * ref for the ReactDatePicker instance + */ + inputRef: Ref>; + + /** + * Provides styling to the input when invalid + */ + isInvalid?: boolean; + + /** + * Provides styling to the input when loading + */ + isLoading?: boolean; + + /** + * What to do when the input is cleared by the x icon + */ + onClear?: MouseEventHandler; + + /** + * Opens to this date (in moment format) on first press, regardless of selection + */ + openToDate?: Moment; + + /** + * Shows only when no date is selected + */ + placeholder?: string; + + /** + * Can turn the shadow off if using the inline prop + */ + shadow?: boolean; + + /** + * Show the icon in input + */ + showIcon?: boolean; +} + +type _EuiDatePickerProps = CommonProps & EuiExtendedDatePickerProps; + +export type EuiDatePickerProps = ApplyClassComponentDefaults< + typeof EuiDatePicker +>; + +export class EuiDatePicker extends Component<_EuiDatePickerProps> { + static defaultProps = { + adjustDateOnChange: true, + dateFormat: euiDatePickerDefaultDateFormat, + fullWidth: false, + inputRef: () => {}, + isLoading: false, + shadow: true, + shouldCloseOnSelect: true, + showIcon: true, + showTimeSelect: false, + timeFormat: euiDatePickerDefaultTimeFormat, + }; + render() { const { adjustDateOnChange, @@ -27,7 +102,7 @@ export class EuiDatePicker extends Component { dayClassName, disabled, excludeDates, - filterDates, + filterDate, fullWidth, injectTimes, inline, @@ -72,9 +147,9 @@ export class EuiDatePicker extends Component { className ); - let optionalIcon; + let optionalIcon: EuiFormControlLayoutIconsProps['icon']; if (inline || customInput || !showIcon) { - optionalIcon = null; + optionalIcon = undefined; } else if (showTimeSelectOnly) { optionalIcon = 'clock'; } else { @@ -130,7 +205,7 @@ export class EuiDatePicker extends Component { @@ -145,7 +220,7 @@ export class EuiDatePicker extends Component { dayClassName={dayClassName} disabled={disabled} excludeDates={excludeDates} - filterDates={filterDates} + filterDate={filterDate} injectTimes={injectTimes} inline={inline} locale={locale || contextLocale} @@ -167,7 +242,7 @@ export class EuiDatePicker extends Component { timeFormat={timeFormat} utcOffset={utcOffset} yearDropdownItemNumber={7} - accessibleMode={true} + accessibleMode {...rest} /> ); @@ -180,136 +255,3 @@ export class EuiDatePicker extends Component { ); } } - -EuiDatePicker.propTypes = { - /** - * Whether changes to Year and Month (via dropdowns) should trigger `onChange` - */ - adjustDateOnChange: PropTypes.bool, - /** - * Optional class added to the calendar portion of datepicker - */ - calendarClassName: PropTypes.string, - - /** - * Added to the actual input of the calendar - */ - className: PropTypes.string, - /** - * Replaces the input with any node, like a button - */ - customInput: PropTypes.node, - /** - * Accepts any moment format string - */ - dateFormat: PropTypes.string, - /** - * Applies classes to the numbered days provided. Check docs for example. - */ - dayClassName: PropTypes.func, - - /** - * Array of dates allowed. Check docs for example. - */ - filterDates: PropTypes.array, - /** - * Makes the input full width - */ - fullWidth: PropTypes.bool, - /** - * Adds additional times to the time selector other then :30 increments - */ - injectTimes: PropTypes.array, - /** - * Applies ref to the input - */ - inputRef: PropTypes.func, - /** - * Provides styling to the input when invalid - */ - isInvalid: PropTypes.bool, - /** - * Provides styling to the input when loading - */ - isLoading: PropTypes.bool, - /** - * Switches the locale / display. "en-us", "zn-ch"...etc - */ - locale: PropTypes.string, - /** - * The max date accepted (in moment format) as a selection - */ - maxDate: PropTypes.instanceOf(moment), - /** - * The max time accepted (in moment format) as a selection - */ - maxTime: PropTypes.instanceOf(moment), - /** - * The min date accepted (in moment format) as a selection - */ - minDate: PropTypes.instanceOf(moment), - /** - * The min time accepted (in moment format) as a selection - */ - minTime: PropTypes.instanceOf(moment), - /** - * What to do when the input changes - */ - onChange: PropTypes.func, - /** - * What to do when the input is cleared by the x icon - */ - onClear: PropTypes.func, - /** - * Opens to this date (in moment format) on first press, regardless of selection - */ - openToDate: PropTypes.instanceOf(moment), - /** - * Shows only when no date is selected - */ - placeholder: PropTypes.string, - /** - * Class applied to the popup, when inline is false - */ - popperClassName: PropTypes.string, - /** - * The selected datetime (in moment format) - */ - selected: PropTypes.instanceOf(moment), - /** - * Can turn the shadow off if using the inline prop - */ - shadow: PropTypes.bool, - /** - * Will close the popup on selection - */ - shouldCloseOnSelect: PropTypes.bool, - /** - * Show the icon in input - */ - showIcon: PropTypes.bool, - /** - * Show the time selection alongside the calendar - */ - showTimeSelect: PropTypes.bool, - /** - * Only show the time selector, not the calendar - */ - showTimeSelectOnly: PropTypes.bool, - /** - * The format of the time within the selector, in moment notation - */ - timeFormat: PropTypes.string, -}; - -EuiDatePicker.defaultProps = { - adjustDateOnChange: true, - dateFormat: euiDatePickerDefaultDateFormat, - fullWidth: false, - isLoading: false, - shadow: true, - shouldCloseOnSelect: true, - showIcon: true, - showTimeSelect: false, - timeFormat: euiDatePickerDefaultTimeFormat, -}; diff --git a/src/components/date_picker/date_picker_range.test.js b/src/components/date_picker/date_picker_range.test.tsx similarity index 100% rename from src/components/date_picker/date_picker_range.test.js rename to src/components/date_picker/date_picker_range.test.tsx diff --git a/src/components/date_picker/date_picker_range.js b/src/components/date_picker/date_picker_range.tsx similarity index 61% rename from src/components/date_picker/date_picker_range.js rename to src/components/date_picker/date_picker_range.tsx index ac8997fb722..f48e2337ebc 100644 --- a/src/components/date_picker/date_picker_range.js +++ b/src/components/date_picker/date_picker_range.tsx @@ -1,16 +1,52 @@ -import React, { cloneElement, Fragment } from 'react'; -import PropTypes from 'prop-types'; +import React, { + Fragment, + FunctionComponent, + ReactNode, + cloneElement, + ReactElement, +} from 'react'; import classNames from 'classnames'; import { EuiText } from '../text'; -import { EuiIcon } from '../icon'; +import { EuiIcon, IconType } from '../icon'; +import { CommonProps } from '../common'; +import { EuiDatePickerProps } from './date_picker'; -export const EuiDatePickerRange = ({ +export type EuiDatePickerRangeProps = CommonProps & { + /** + * Including any children will replace all innerds with the provided children + */ + children?: ReactNode; + + /** + * The end date `EuiDatePicker` element + */ + endDateControl: ReactNode; + fullWidth?: boolean; + + /** + * Pass either an icon type or set to `false` to remove icon entirely + */ + iconType?: boolean | IconType; + + /** + * Won't apply any additional props to start and end date components + */ + isCustom?: boolean; + readOnly?: boolean; + + /** + * The start date `EuiDatePicker` element + */ + startDateControl: ReactNode; +}; + +export const EuiDatePickerRange: FunctionComponent = ({ children, className, startDateControl, endDateControl, - iconType, + iconType = true, fullWidth, isCustom, readOnly, @@ -42,17 +78,23 @@ export const EuiDatePickerRange = ({ let endControl = endDateControl; if (!isCustom) { - startControl = cloneElement(startDateControl, { - showIcon: false, - fullWidth: fullWidth, - readOnly: readOnly, - }); + startControl = cloneElement( + startDateControl as ReactElement, + { + showIcon: false, + fullWidth: fullWidth, + readOnly: readOnly, + } + ); - endControl = cloneElement(endDateControl, { - showIcon: false, - fullWidth: fullWidth, - readOnly: readOnly, - }); + endControl = cloneElement( + endDateControl as ReactElement, + { + showIcon: false, + fullWidth: fullWidth, + readOnly: readOnly, + } + ); } return ( @@ -75,34 +117,3 @@ export const EuiDatePickerRange = ({ ); }; - -EuiDatePickerRange.propTypes = { - /** - * The start date `EuiDatePicker` element - */ - startDateControl: PropTypes.node.isRequired, - /** - * The end date `EuiDatePicker` element - */ - endDateControl: PropTypes.node.isRequired, - /** - * Pass either an icon type or set to `false` to remove icon entirely - */ - iconType: PropTypes.oneOfType([ - PropTypes.bool, - PropTypes.oneOfType([PropTypes.string, PropTypes.node]), - ]), - fullWidth: PropTypes.bool, - /** - * Won't apply any additional props to start and end date components - */ - isCustom: PropTypes.bool, - /** - * Including any children will replace all innerds with the provided children - */ - children: PropTypes.node, -}; - -EuiDatePickerRange.defaultProps = { - iconType: true, -}; diff --git a/src/components/date_picker/index.d.ts b/src/components/date_picker/index.d.ts deleted file mode 100644 index ff810cec0be..00000000000 --- a/src/components/date_picker/index.d.ts +++ /dev/null @@ -1,104 +0,0 @@ -import React from 'react'; -import { CommonProps } from '../common'; -import { IconType } from '../icon'; -import _ReactDatePicker, { - ReactDatePickerProps as _ReactDatePickerProps, -} from './react-datepicker'; // eslint-disable-line import/no-unresolved -import { Moment } from 'moment'; // eslint-disable-line import/named - -declare module '@elastic/eui' { - interface OnTimeChangeProps { - start: string; - end: string; - isInvalid: boolean; - isQuickSelection: boolean; - } - - interface OnRefreshProps { - start: string; - end: string; - refreshInterval: number; - } - - interface OnRefreshChangeProps { - isPaused: boolean; - refreshInterval: number; - } - - interface EuiExtendedDatePickerProps extends _ReactDatePickerProps { - fullWidth?: boolean; - isInvalid?: boolean; - isLoading?: boolean; - injectTimes?: Moment[]; // added here because the type is missing in @types/react-datepicker@1.8.0 - inputRef?: React.Ref; - placeholder?: string; - shadow?: boolean; - showIcon?: boolean; - } - - export type EuiDatePickerProps = CommonProps & EuiExtendedDatePickerProps; - export const EuiDatePicker: React.SFC; - - export type EuiDatePickerRangeProps = CommonProps & { - startDateControl: React.ReactElement; - endDateControl: React.ReactElement; - iconType?: boolean | IconType; - fullWidth?: boolean; - isCustom?: boolean; - }; - - export const EuiDatePickerRange: React.SFC; - - export interface EuiSuperDatePickerCommonRange { - start: string; - end: string; - label: string; - } - - export interface EuiSuperDatePickerRecentRange { - start: string; - end: string; - } - - export interface EuiSuperDatePickerQuickSelectPanel { - title: string; - content: React.ReactNode; - } - - export type EuiSuperDatePickerProps = CommonProps & { - isLoading?: boolean; - start?: string; - end?: string; - isPaused?: boolean; - refreshInterval?: number; - onTimeChange: (props: OnTimeChangeProps) => void; - onRefresh?: (props: OnRefreshProps) => void; - onRefreshChange?: (props: OnRefreshChangeProps) => void; - commonlyUsedRanges?: EuiSuperDatePickerCommonRange[]; - dateFormat?: string; - recentlyUsedRanges?: EuiSuperDatePickerRecentRange[]; - showUpdateButton?: boolean; - isAutoRefreshOnly?: boolean; - customQuickSelectPanels?: EuiSuperDatePickerQuickSelectPanel[]; - }; - - export const EuiSuperDatePicker: React.SFC; - - export const ReactDatePicker: typeof _ReactDatePicker; - export const ReactDatePickerProps: _ReactDatePickerProps; - - interface DurationRange { - start: string; - end: string; - label: string; - } - - export const commonDurationRanges: DurationRange[]; - - export function prettyDuration( - timeFrom: string, - timeTo: string, - quickRanges: DurationRange[], - dateFormat: string - ): string; -} diff --git a/src/components/date_picker/index.js b/src/components/date_picker/index.js deleted file mode 100644 index 69a0ec433e7..00000000000 --- a/src/components/date_picker/index.js +++ /dev/null @@ -1,10 +0,0 @@ -export { EuiDatePicker } from './date_picker'; - -export { EuiDatePickerRange } from './date_picker_range'; - -export { - EuiSuperDatePicker, - EuiSuperUpdateButton, - prettyDuration, - commonDurationRanges, -} from './super_date_picker'; diff --git a/src/components/date_picker/index.ts b/src/components/date_picker/index.ts new file mode 100644 index 00000000000..b9421dfe6b7 --- /dev/null +++ b/src/components/date_picker/index.ts @@ -0,0 +1,28 @@ +export * from './super_date_picker'; + +export { EuiDatePicker, EuiDatePickerProps } from './date_picker'; + +export { + EuiDatePickerRange, + EuiDatePickerRangeProps, +} from './date_picker_range'; + +export { + DurationRange as EuiSuperDatePickerCommonRange, + DurationRange as EuiSuperDatePickerDurationRange, + DurationRange as EuiSuperDatePickerRecentRange, + TimeUnitId, + TimeUnitFromNowId, + TimeUnitLabel, + TimeUnitLabelPlural, + AbsoluteDateMode, + RelativeDateMode, + NowDateMode, + DateMode, + OnRefreshChangeProps, + ShortDate, + RelativeParts, + RelativeOption, + QuickSelect, + QuickSelectPanel as EuiSuperDatePickerQuickSelectPanel, +} from './types'; diff --git a/src/components/date_picker/react-datepicker.d.ts b/src/components/date_picker/react-datepicker.d.ts index 81f19ce3cb1..09b7b406055 100644 --- a/src/components/date_picker/react-datepicker.d.ts +++ b/src/components/date_picker/react-datepicker.d.ts @@ -15,15 +15,35 @@ import * as React from 'react'; import * as moment from 'moment'; export interface ReactDatePickerProps { + /** + * Whether changes to Year and Month (via dropdowns) should trigger `onChange` + */ adjustDateOnChange?: boolean; + accessibleMode?: boolean; allowSameDay?: boolean; autoComplete?: string; autoFocus?: boolean; + + /** + * Optional class added to the calendar portion of datepicker + */ calendarClassName?: string; children?: React.ReactNode; + + /** + * Added to the actual input of the calendar + */ className?: string; + + /** + * Replaces the input with any node, like a button + */ customInput?: React.ReactNode; customInputRef?: string; + + /** + * Accepts any moment format string + */ dateFormat?: string | string[]; dateFormatCalendar?: string; dayClassName?(date: moment.Moment): string | null; @@ -42,37 +62,67 @@ export interface ReactDatePickerProps { includeDates?: moment.Moment[]; includeTimes?: moment.Moment[]; inline?: boolean; + + /** + * Adds additional times to the time selector other then :30 increments + */ + injectTimes?: moment.Moment[]; isClearable?: boolean; - locale?: string; + + /** + * Switches the locale / display. "en-us", "zn-ch"...etc + */ + locale?: moment.LocaleSpecifier; + + /** + * The max date accepted (in moment format) as a selection + */ maxDate?: moment.Moment; + + /** + * The max time accepted (in moment format) as a selection + */ maxTime?: moment.Moment; + + /** + * The min date accepted (in moment format) as a selection + */ minDate?: moment.Moment; + + /** + * The min time accepted (in moment format) as a selection + */ minTime?: moment.Moment; monthsShown?: number; name?: string; onBlur?(event: React.FocusEvent): void; - onChange( + + /** + * What to do when the input changes + */ + onChange?( date: moment.Moment | null, - event: React.SyntheticEvent | undefined + event?: React.SyntheticEvent ): void; onChangeRaw?(event: React.FocusEvent): void; onClickOutside?(event: React.MouseEvent): void; onFocus?(event: React.FocusEvent): void; onKeyDown?(event: React.KeyboardEvent): void; onMonthChange?(date: moment.Moment): void; - onSelect?( - date: moment.Moment, - event: React.SyntheticEvent | undefined - ): void; + onSelect?(date: moment.Moment, event?: React.SyntheticEvent): void; onWeekSelect?( firstDayOfWeek: moment.Moment, weekNumber: string | number, - event: React.SyntheticEvent | undefined + event?: React.SyntheticEvent ): void; onYearChange?(date: moment.Moment): void; openToDate?: moment.Moment; peekNextMonth?: boolean; placeholderText?: string; + + /** + * Class applied to the popup, when inline is false + */ popperClassName?: string; popperContainer?(props: { children: React.ReactNode[] }): React.ReactNode; popperPlacement?: string; @@ -81,14 +131,30 @@ export interface ReactDatePickerProps { required?: boolean; scrollableMonthYearDropdown?: boolean; scrollableYearDropdown?: boolean; + + /** + * The selected datetime (in moment format) + */ selected?: moment.Moment | null; selectsEnd?: boolean; selectsStart?: boolean; + + /** + * Will close the popup on selection + */ shouldCloseOnSelect?: boolean; showDisabledMonthNavigation?: boolean; showMonthDropdown?: boolean; showMonthYearDropdown?: boolean; + + /** + * Show the time selection alongside the calendar + */ showTimeSelect?: boolean; + + /** + * Only show the time selector, not the calendar + */ showTimeSelectOnly?: boolean; showWeekNumbers?: boolean; showYearDropdown?: boolean; @@ -96,6 +162,10 @@ export interface ReactDatePickerProps { startOpen?: boolean; tabIndex?: number; timeCaption?: string; + + /** + * The format of the time within the selector, in moment notation + */ timeFormat?: string; timeIntervals?: number; title?: string; 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.tsx.snap similarity index 98% rename from src/components/date_picker/super_date_picker/__snapshots__/super_date_picker.test.js.snap rename to src/components/date_picker/super_date_picker/__snapshots__/super_date_picker.test.tsx.snap index 802d4d7dc3b..629084f893c 100644 --- 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.tsx.snap @@ -12,7 +12,6 @@ exports[`EuiSuperDatePicker is rendered 1`] = ` isDisabled={false} prepend={ { - if (!this.isStopped) { - this.timeoutId = window.setTimeout(async () => { - this.__pendingFn = await fn(); - this.setAsyncInterval(fn, ms); - }, ms); - } - }; - - stop = () => { - this.isStopped = true; - window.clearTimeout(this.timeoutId); - }; -} diff --git a/src/components/date_picker/super_date_picker/async_interval.test.js b/src/components/date_picker/super_date_picker/async_interval.test.ts similarity index 85% rename from src/components/date_picker/super_date_picker/async_interval.test.js rename to src/components/date_picker/super_date_picker/async_interval.test.ts index 7f660070b85..db2bb0af173 100644 --- a/src/components/date_picker/super_date_picker/async_interval.test.js +++ b/src/components/date_picker/super_date_picker/async_interval.test.ts @@ -13,11 +13,16 @@ describe('AsyncInterval', () => { // Advances time and awaits any pending promises after every 100ms // This helper makes it easier to advance time without worrying // whether tasks are still lingering on the event loop - async function andvanceTimerAndAwaitFn(instance, ms) { - const iterations = [...Array(Math.floor(ms / 100)).keys()]; - const remainder = ms % 100; - // eslint-disable-next-line @typescript-eslint/no-unused-vars + async function andvanceTimerAndAwaitFn( + instance: AsyncInterval, + milliseconds: number + ) { + const iterations = [...Array(Math.floor(milliseconds / 100)).keys()]; + const remainder = milliseconds % 100; + /* eslint-disable @typescript-eslint/no-unused-vars */ + // @ts-ignore for (const item of iterations) { + /* eslint-enable @typescript-eslint/no-unused-vars */ await instance.__pendingFn; jest.advanceTimersByTime(100); await instance.__pendingFn; @@ -27,8 +32,8 @@ describe('AsyncInterval', () => { } describe('when creating a 1000ms interval', async () => { - let instance; - let spy; + let instance: AsyncInterval; + let spy: jest.Mock; beforeEach(() => { spy = jest.fn(); instance = new AsyncInterval(spy, 1000); @@ -64,8 +69,8 @@ describe('AsyncInterval', () => { }); describe('when creating a 1000ms interval that calls a fn that takes 2000ms to complete', async () => { - let instance; - let spy; + let instance: AsyncInterval; + let spy: jest.Mock; beforeEach(() => { spy = jest.fn(async () => await sleep(2000)); instance = new AsyncInterval(spy, 1000); diff --git a/src/components/date_picker/super_date_picker/async_interval.ts b/src/components/date_picker/super_date_picker/async_interval.ts new file mode 100644 index 00000000000..00a3f78d1d7 --- /dev/null +++ b/src/components/date_picker/super_date_picker/async_interval.ts @@ -0,0 +1,25 @@ +export class AsyncInterval { + timeoutId: number | null = null; + isStopped = false; + __pendingFn: Function = () => {}; + + constructor(fn: Function, refreshInterval: number) { + this.setAsyncInterval(fn, refreshInterval); + } + + setAsyncInterval = (fn: Function, milliseconds: number) => { + if (!this.isStopped) { + this.timeoutId = window.setTimeout(async () => { + this.__pendingFn = await fn(); + this.setAsyncInterval(fn, milliseconds); + }, milliseconds); + } + }; + + stop = () => { + this.isStopped = true; + if (this.timeoutId !== null) { + window.clearTimeout(this.timeoutId); + } + }; +} diff --git a/src/components/date_picker/super_date_picker/date_modes.test.js b/src/components/date_picker/super_date_picker/date_modes.test.ts similarity index 97% rename from src/components/date_picker/super_date_picker/date_modes.test.js rename to src/components/date_picker/super_date_picker/date_modes.test.ts index e37eb42970f..ac506442433 100644 --- a/src/components/date_picker/super_date_picker/date_modes.test.js +++ b/src/components/date_picker/super_date_picker/date_modes.test.ts @@ -10,7 +10,7 @@ jest.mock('@elastic/datemath', () => { moment.now = () => offset + Date.now(); return { ...datemath, - parse: (text, options) => + parse: (text: string, options: any) => datemath.parse(text, { forceNow: anchoredDate, // For `toAbsoluteString` momentInstance: moment, // For `toRelativeString` diff --git a/src/components/date_picker/super_date_picker/date_modes.js b/src/components/date_picker/super_date_picker/date_modes.ts similarity index 59% rename from src/components/date_picker/super_date_picker/date_modes.js rename to src/components/date_picker/super_date_picker/date_modes.ts index 16f9924bf0a..f2db4a24b90 100644 --- a/src/components/date_picker/super_date_picker/date_modes.js +++ b/src/components/date_picker/super_date_picker/date_modes.ts @@ -3,14 +3,24 @@ import { parseRelativeParts, toRelativeStringFromParts, } from './relative_utils'; +import { + AbsoluteDateMode, + RelativeDateMode, + NowDateMode, + ShortDate, +} from '../types'; -export const DATE_MODES = { +export const DATE_MODES: { + ABSOLUTE: AbsoluteDateMode; + RELATIVE: RelativeDateMode; + NOW: NowDateMode; +} = { ABSOLUTE: 'absolute', RELATIVE: 'relative', NOW: 'now', }; -export function getDateMode(value) { +export function getDateMode(value: ShortDate) { if (value === 'now') { return DATE_MODES.NOW; } @@ -22,7 +32,7 @@ export function getDateMode(value) { return DATE_MODES.ABSOLUTE; } -export function toAbsoluteString(value, roundUp) { +export function toAbsoluteString(value: string, roundUp: boolean = false) { const valueAsMoment = dateMath.parse(value, { roundUp }); if (!valueAsMoment) { return value; @@ -30,6 +40,6 @@ export function toAbsoluteString(value, roundUp) { return valueAsMoment.toISOString(); } -export function toRelativeString(value) { +export function toRelativeString(value: string) { 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.tsx similarity index 50% rename from src/components/date_picker/super_date_picker/date_popover/absolute_tab.js rename to src/components/date_picker/super_date_picker/date_popover/absolute_tab.tsx index ee106752024..aa1a5c16db3 100644 --- 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.tsx @@ -1,54 +1,89 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; +import React, { Component, ChangeEventHandler } from 'react'; -import moment from 'moment'; +import moment, { Moment, LocaleSpecifier } from 'moment'; // eslint-disable-line import/named import dateMath from '@elastic/datemath'; import { EuiDatePicker } from '../../date_picker'; import { EuiFormRow, EuiFieldText, EuiFormLabel } from '../../../form'; import { toSentenceCase } from '../../../../services/string/to_case'; +import { EuiDatePopoverContentProps } from './date_popover_content'; -export class EuiAbsoluteTab extends Component { - constructor(props) { +export interface EuiAbsoluteTabProps { + dateFormat: string; + timeFormat: string; + locale?: LocaleSpecifier; + value: string; + onChange: EuiDatePopoverContentProps['onChange']; + roundUp: boolean; + position: 'start' | 'end'; +} + +interface EuiAbsoluteTabState { + isTextInvalid: boolean; + sentenceCasedPosition: string; + textInputValue: string; + valueAsMoment: Moment | null; +} + +export class EuiAbsoluteTab extends Component< + EuiAbsoluteTabProps, + EuiAbsoluteTabState +> { + state: EuiAbsoluteTabState; + + constructor(props: EuiAbsoluteTabProps) { super(props); + const sentenceCasedPosition = toSentenceCase(props.position); + const parsedValue = dateMath.parse(props.value, { roundUp: props.roundUp }); const valueAsMoment = parsedValue && parsedValue.isValid() ? parsedValue : moment(); - const sentenceCasedPosition = toSentenceCase(props.position); + + const textInputValue = valueAsMoment + .locale(this.props.locale || 'en') + .format(this.props.dateFormat); this.state = { - valueAsMoment, - textInputValue: valueAsMoment - .locale(this.props.locale || 'en') - .format(this.props.dateFormat), isTextInvalid: false, sentenceCasedPosition, + textInputValue, + valueAsMoment, }; } - handleChange = date => { - this.props.onChange(date.toISOString()); + handleChange: EuiDatePopoverContentProps['onChange'] = (date, event) => { + const { onChange } = this.props; + if (date === null) { + return; + } + onChange(typeof date === 'string' ? date : date.toISOString(), event); + + const valueAsMoment = moment(date); this.setState({ - valueAsMoment: date, - textInputValue: date.format(this.props.dateFormat), + valueAsMoment, + textInputValue: valueAsMoment.format(this.props.dateFormat), isTextInvalid: false, }); }; - handleTextChange = evt => { - const date = moment(evt.target.value, this.props.dateFormat, true); - const updatedState = { - textInputValue: evt.target.value, - isTextInvalid: !date.isValid(), - }; - if (date.isValid()) { - this.props.onChange(date.toISOString()); - updatedState.valueAsMoment = date; + handleTextChange: ChangeEventHandler = event => { + const { onChange } = this.props; + const valueAsMoment = moment( + event.target.value, + this.props.dateFormat, + true + ); + const dateIsValid = valueAsMoment.isValid(); + if (dateIsValid) { + onChange(valueAsMoment, event); } - - this.setState(updatedState); + this.setState({ + textInputValue: event.target.value as string, + isTextInvalid: !dateIsValid, + valueAsMoment: dateIsValid ? valueAsMoment : null, + }); }; render() { @@ -89,13 +124,3 @@ export class EuiAbsoluteTab extends Component { ); } } - -EuiAbsoluteTab.propTypes = { - dateFormat: PropTypes.string.isRequired, - timeFormat: PropTypes.string.isRequired, - locale: PropTypes.string, - value: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired, - roundUp: PropTypes.bool.isRequired, - position: PropTypes.oneOf(['start', 'end']), -}; 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.tsx similarity index 61% rename from src/components/date_picker/super_date_picker/date_popover/date_popover_button.js rename to src/components/date_picker/super_date_picker/date_popover/date_popover_button.tsx index 74868cb7585..48c9be68407 100644 --- 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.tsx @@ -1,13 +1,40 @@ -import PropTypes from 'prop-types'; -import React from 'react'; +import React, { + FunctionComponent, + ButtonHTMLAttributes, + MouseEventHandler, +} from 'react'; import classNames from 'classnames'; -import { EuiPopover } from '../../../popover'; +import { EuiPopover, EuiPopoverProps } from '../../../popover'; import { formatTimeString } from '../pretty_duration'; -import { EuiDatePopoverContent } from './date_popover_content'; +import { + EuiDatePopoverContent, + EuiDatePopoverContentProps, +} from './date_popover_content'; +import { LocaleSpecifier } from 'moment'; // eslint-disable-line import/named -export function EuiDatePopoverButton(props) { +export interface EuiDatePopoverButtonProps { + className?: string; + buttonProps?: ButtonHTMLAttributes; + dateFormat: string; + isDisabled?: boolean; + isInvalid?: boolean; + isOpen: boolean; + needsUpdating?: boolean; + locale?: LocaleSpecifier; + onChange: NonNullable; + onPopoverClose: EuiPopoverProps['closePopover']; + onPopoverToggle: MouseEventHandler; + position: 'start' | 'end'; + roundUp?: boolean; + timeFormat: string; + value: string; +} + +export const EuiDatePopoverButton: FunctionComponent< + EuiDatePopoverButtonProps +> = props => { const { position, isDisabled, @@ -77,19 +104,6 @@ export function EuiDatePopoverButton(props) { /> ); -} - -EuiDatePopoverButton.propTypes = { - position: PropTypes.oneOf(['start', 'end']), - isInvalid: PropTypes.bool, - isDisabled: PropTypes.bool, - needsUpdating: PropTypes.bool, - value: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired, - dateFormat: PropTypes.string.isRequired, - timeFormat: PropTypes.string.isRequired, - roundUp: PropTypes.bool, - isOpen: PropTypes.bool.isRequired, - onPopoverToggle: PropTypes.func.isRequired, - onPopoverClose: PropTypes.func.isRequired, }; + +EuiDatePopoverButton.displayName = 'EuiDatePopoverButton'; 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.tsx similarity index 71% rename from src/components/date_picker/super_date_picker/date_popover/date_popover_content.js rename to src/components/date_picker/super_date_picker/date_popover/date_popover_content.tsx index 333bc2071af..b5e295fc20d 100644 --- 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.tsx @@ -1,7 +1,6 @@ -import PropTypes from 'prop-types'; -import React from 'react'; +import React, { FunctionComponent } from 'react'; -import { EuiTabbedContent } from '../../../tabs'; +import { EuiTabbedContent, EuiTabbedContentProps } from '../../../tabs'; import { EuiText } from '../../../text'; import { EuiButton } from '../../../button'; @@ -14,17 +13,33 @@ import { toAbsoluteString, toRelativeString, } from '../date_modes'; +import { Moment, LocaleSpecifier } from 'moment'; // eslint-disable-line import/named -export function EuiDatePopoverContent({ +export interface EuiDatePopoverContentProps { + value: string; + onChange( + date: Moment | string | null, + event?: React.SyntheticEvent + ): void; + roundUp?: boolean; + dateFormat: string; + timeFormat: string; + locale?: LocaleSpecifier; + position: 'start' | 'end'; +} + +export const EuiDatePopoverContent: FunctionComponent< + EuiDatePopoverContentProps +> = ({ value, - roundUp, + roundUp = false, onChange, dateFormat, timeFormat, locale, position, -}) { - const onTabClick = selectedTab => { +}) => { + const onTabClick: EuiTabbedContentProps['onTabClick'] = selectedTab => { switch (selectedTab.id) { case DATE_MODES.ABSOLUTE: onChange(toAbsoluteString(value, roundUp)); @@ -85,7 +100,9 @@ export function EuiDatePopoverContent({

onChange('now')} + onClick={() => { + onChange('now'); + }} fullWidth size="s" fill> @@ -98,32 +115,21 @@ export function EuiDatePopoverContent({ }, ]; - const initialSelectedTab = () => - renderTabs.filter(tabs => tabs.id === getDateMode(value))[0]; + const initialSelectedTab = renderTabs.find( + tab => tab.id === getDateMode(value) + ); return ( ); -} - -EuiDatePopoverContent.propTypes = { - value: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired, - roundUp: PropTypes.bool, - dateFormat: PropTypes.string.isRequired, - timeFormat: PropTypes.string.isRequired, - locale: PropTypes.string, - position: PropTypes.oneOf(['start', 'end']), }; -EuiDatePopoverContent.defaultProps = { - roundUp: false, -}; +EuiDatePopoverContent.displayName = 'EuiDatePopoverContent'; diff --git a/src/components/date_picker/super_date_picker/date_popover/index.ts b/src/components/date_picker/super_date_picker/date_popover/index.ts new file mode 100644 index 00000000000..4f86a5e3e62 --- /dev/null +++ b/src/components/date_picker/super_date_picker/date_popover/index.ts @@ -0,0 +1,10 @@ +export { EuiAbsoluteTab, EuiAbsoluteTabProps } from './absolute_tab'; +export { + EuiDatePopoverButton, + EuiDatePopoverButtonProps, +} from './date_popover_button'; +export { + EuiDatePopoverContent, + EuiDatePopoverContentProps, +} from './date_popover_content'; +export { EuiRelativeTab, EuiRelativeTabProps } from './relative_tab'; 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.tsx similarity index 66% rename from src/components/date_picker/super_date_picker/date_popover/relative_tab.js rename to src/components/date_picker/super_date_picker/date_popover/relative_tab.tsx index 2fe03b16a7b..7351a9735b1 100644 --- 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.tsx @@ -1,5 +1,4 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; +import React, { Component, ChangeEventHandler } from 'react'; import dateMath from '@elastic/datemath'; import { toSentenceCase } from '../../../../services/string/to_case'; import { htmlIdGenerator } from '../../../../services'; @@ -12,6 +11,7 @@ import { EuiFieldText, EuiSwitch, EuiFormLabel, + EuiSwitchEvent, } from '../../../form'; import { EuiSpacer } from '../../../spacer'; @@ -23,58 +23,83 @@ import { } from '../relative_utils'; import { EuiScreenReaderOnly } from '../../../accessibility'; import { EuiI18n } from '../../../i18n'; +import { RelativeParts, TimeUnitId } from '../../types'; +import { LocaleSpecifier } from 'moment'; // eslint-disable-line import/named +import { EuiDatePopoverContentProps } from './date_popover_content'; -export class EuiRelativeTab extends Component { - constructor(props) { - super(props); - const sentenceCasedPosition = toSentenceCase(props.position); +export interface EuiRelativeTabProps { + dateFormat: string; + locale?: LocaleSpecifier; + value: string; + onChange: EuiDatePopoverContentProps['onChange']; + roundUp?: boolean; + position: 'start' | 'end'; +} - this.state = { - ...parseRelativeParts(this.props.value), - sentenceCasedPosition, - }; - } +interface EuiRelativeTabState + extends Pick { + count: number | undefined; + sentenceCasedPosition: string; +} + +export class EuiRelativeTab extends Component< + EuiRelativeTabProps, + EuiRelativeTabState +> { + state: EuiRelativeTabState = { + ...parseRelativeParts(this.props.value), + sentenceCasedPosition: toSentenceCase(this.props.position), + }; generateId = htmlIdGenerator(); - onCountChange = evt => { - const sanitizedValue = parseInt(evt.target.value, 10); + onCountChange: ChangeEventHandler = event => { + const sanitizedValue = parseInt(event.target.value, 10); this.setState( { - count: isNaN(sanitizedValue) ? '' : sanitizedValue, + count: isNaN(sanitizedValue) ? undefined : sanitizedValue, }, this.handleChange ); }; - onUnitChange = evt => { + onUnitChange: ChangeEventHandler = event => { this.setState( { - unit: evt.target.value, + unit: event.target.value, }, this.handleChange ); }; - onRoundChange = evt => { + onRoundChange = (event: EuiSwitchEvent) => { this.setState( { - round: evt.target.checked, + round: event.target.checked, }, this.handleChange ); }; handleChange = () => { - if (this.state.count === '' || this.state.count < 0) { + const { count, round, roundUnit, unit } = this.state; + const { onChange } = this.props; + if (count === undefined || count < 0) { return; } - this.props.onChange(toRelativeStringFromParts(this.state)); + const date = toRelativeStringFromParts({ + count, + round, + roundUnit, + unit, + }); + onChange(date); }; render() { + const { count, unit } = this.state; const relativeDateInputNumberDescriptionId = this.generateId(); - const isInvalid = this.state.count < 0; + const isInvalid = count === undefined || count < 0; const parsedValue = dateMath.parse(this.props.value, { roundUp: this.props.roundUp, }); @@ -94,7 +119,7 @@ export class EuiRelativeTab extends Component { 'euiRelativeTab.numberInputLabel', ]} defaults={['Must be >= 0', 'Time span amount']}> - {([numberInputError, numberInputLabel]) => ( + {([numberInputError, numberInputLabel]: string[]) => ( @@ -103,7 +128,7 @@ export class EuiRelativeTab extends Component { aria-label={numberInputLabel} aria-describedby={relativeDateInputNumberDescriptionId} data-test-subj={'superDatePickerRelativeDateInputNumber'} - value={this.state.count} + value={count} onChange={this.onCountChange} isInvalid={isInvalid} /> @@ -115,14 +140,14 @@ export class EuiRelativeTab extends Component { - {unitInputLabel => ( + {(unitInputLabel: string) => ( @@ -134,8 +159,8 @@ export class EuiRelativeTab extends Component { - {roundingLabel => ( + values={{ unit: timeUnits[unit.substring(0, 1) as TimeUnitId] }}> + {(roundingLabel: string) => ( } /> - -

+ +

@@ -173,12 +198,3 @@ export class EuiRelativeTab extends Component { ); } } - -EuiRelativeTab.propTypes = { - dateFormat: PropTypes.string.isRequired, - locale: PropTypes.string, - value: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired, - roundUp: PropTypes.bool, - position: PropTypes.oneOf(['start', 'end']), -}; diff --git a/src/components/date_picker/super_date_picker/index.js b/src/components/date_picker/super_date_picker/index.js deleted file mode 100644 index 978ec874cc6..00000000000 --- a/src/components/date_picker/super_date_picker/index.js +++ /dev/null @@ -1,5 +0,0 @@ -export { EuiSuperDatePicker } from './super_date_picker'; - -export { EuiSuperUpdateButton } from './super_update_button'; - -export { prettyDuration, commonDurationRanges } from './pretty_duration'; diff --git a/src/components/date_picker/super_date_picker/index.ts b/src/components/date_picker/super_date_picker/index.ts new file mode 100644 index 00000000000..dca40703a6a --- /dev/null +++ b/src/components/date_picker/super_date_picker/index.ts @@ -0,0 +1,17 @@ +export * from './date_popover'; +export * from './quick_select_popover'; +export { AsyncInterval } from './async_interval'; + +export { + EuiSuperDatePicker, + EuiSuperDatePickerProps, + OnTimeChangeProps, + OnRefreshProps, +} from './super_date_picker'; + +export { + EuiSuperUpdateButton, + EuiSuperUpdateButtonProps, +} from './super_update_button'; + +export { prettyDuration, commonDurationRanges } from './pretty_duration'; 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.ts similarity index 100% rename from src/components/date_picker/super_date_picker/pretty_duration.test.js rename to src/components/date_picker/super_date_picker/pretty_duration.test.ts diff --git a/src/components/date_picker/super_date_picker/pretty_duration.js b/src/components/date_picker/super_date_picker/pretty_duration.ts similarity index 77% rename from src/components/date_picker/super_date_picker/pretty_duration.js rename to src/components/date_picker/super_date_picker/pretty_duration.ts index 21a49ce872f..5f2c208507a 100644 --- a/src/components/date_picker/super_date_picker/pretty_duration.js +++ b/src/components/date_picker/super_date_picker/pretty_duration.ts @@ -1,12 +1,13 @@ import dateMath from '@elastic/datemath'; -import moment from 'moment'; +import moment, { LocaleSpecifier } from 'moment'; // eslint-disable-line import/named import { timeUnits, timeUnitsPlural } from './time_units'; import { getDateMode, DATE_MODES } from './date_modes'; import { parseRelativeParts } from './relative_utils'; +import { DurationRange, TimeUnitId, ShortDate } from '../types'; const ISO_FORMAT = 'YYYY-MM-DDTHH:mm:ss.SSSZ'; -export const commonDurationRanges = [ +export const commonDurationRanges: DurationRange[] = [ { start: 'now/d', end: 'now/d', label: 'Today' }, { start: 'now/w', end: 'now/w', label: 'This week' }, { start: 'now/M', end: 'now/M', label: 'This month' }, @@ -17,13 +18,13 @@ export const commonDurationRanges = [ { start: 'now/y', end: 'now', label: 'Year to date' }, ]; -function cantLookup(timeFrom, timeTo, dateFormat) { +function cantLookup(timeFrom: string, timeTo: string, dateFormat: string) { const displayFrom = formatTimeString(timeFrom, dateFormat); const displayTo = formatTimeString(timeTo, dateFormat, true); return `${displayFrom} to ${displayTo}`; } -function isRelativeToNow(timeFrom, timeTo) { +function isRelativeToNow(timeFrom: ShortDate, timeTo: ShortDate) { const fromDateMode = getDateMode(timeFrom); const toDateMode = getDateMode(timeTo); const isLast = @@ -34,10 +35,10 @@ function isRelativeToNow(timeFrom, timeTo) { } export function formatTimeString( - timeString, - dateFormat, + timeString: string, + dateFormat: string, roundUp = false, - locale = 'en' + locale: LocaleSpecifier = 'en' ) { const timeAsMoment = moment(timeString, ISO_FORMAT, true); if (timeAsMoment.isValid()) { @@ -56,7 +57,12 @@ export function formatTimeString( return timeString; } -export function prettyDuration(timeFrom, timeTo, quickRanges = [], dateFormat) { +export function prettyDuration( + timeFrom: ShortDate, + timeTo: ShortDate, + quickRanges: DurationRange[] = [], + dateFormat: string +) { const matchingQuickRange = quickRanges.find( ({ start: quickFrom, end: quickTo }) => { return timeFrom === quickFrom && timeTo === quickTo; @@ -76,13 +82,13 @@ export function prettyDuration(timeFrom, timeTo, quickRanges = [], dateFormat) { timeTense = 'Next'; relativeParts = parseRelativeParts(timeTo); } - const countTimeUnit = relativeParts.unit.substring(0, 1); + const countTimeUnit = relativeParts.unit.substring(0, 1) as TimeUnitId; const countTimeUnitFullName = relativeParts.count > 1 ? timeUnitsPlural[countTimeUnit] : timeUnits[countTimeUnit]; let text = `${timeTense} ${relativeParts.count} ${countTimeUnitFullName}`; - if (relativeParts.round) { + if (relativeParts.round && relativeParts.roundUnit) { text += ` rounded to the ${timeUnits[relativeParts.roundUnit]}`; } return text; @@ -91,7 +97,11 @@ export function prettyDuration(timeFrom, timeTo, quickRanges = [], dateFormat) { return cantLookup(timeFrom, timeTo, dateFormat); } -export function showPrettyDuration(timeFrom, timeTo, quickRanges = []) { +export function showPrettyDuration( + timeFrom: ShortDate, + timeTo: ShortDate, + quickRanges: DurationRange[] = [] +) { const matchingQuickRange = quickRanges.find( ({ start: quickFrom, end: quickTo }) => { return timeFrom === quickFrom && timeTo === quickTo; 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.tsx.snap similarity index 97% rename from src/components/date_picker/super_date_picker/quick_select_popover/__snapshots__/quick_select.test.js.snap rename to src/components/date_picker/super_date_picker/quick_select_popover/__snapshots__/quick_select.test.tsx.snap index 5edeb5a2be3..f8a9171643f 100644 --- 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.tsx.snap @@ -106,10 +106,10 @@ exports[`EuiQuickSelect is rendered 1`] = ` - -

+ +

- -

+ +

= ({ applyTime, commonlyUsedRanges }) => { const legendId = generateId(); const links = commonlyUsedRanges.map(({ start, end, label }) => { const applyCommonlyUsed = () => { applyTime({ start, end }); }; + const dataTestSubj = label + ? `superDatePickerCommonlyUsed_${label.replace(' ', '_')}` + : undefined; return ( - + {label} @@ -57,9 +61,6 @@ export function EuiCommonlyUsedTimeRanges({ applyTime, commonlyUsedRanges }) { ); -} - -EuiCommonlyUsedTimeRanges.propTypes = { - applyTime: PropTypes.func.isRequired, - commonlyUsedRanges: PropTypes.arrayOf(commonlyUsedRangeShape).isRequired, }; + +EuiCommonlyUsedTimeRanges.displayName = 'EuiCommonlyUsedTimeRanges'; diff --git a/src/components/date_picker/super_date_picker/quick_select_popover/index.ts b/src/components/date_picker/super_date_picker/quick_select_popover/index.ts new file mode 100644 index 00000000000..d5bacaf9163 --- /dev/null +++ b/src/components/date_picker/super_date_picker/quick_select_popover/index.ts @@ -0,0 +1,14 @@ +export { + EuiCommonlyUsedTimeRanges, + EuiCommonlyUsedTimeRangesProps, +} from './commonly_used_time_ranges'; +export { + EuiQuickSelectPopover, + EuiQuickSelectPopoverProps, +} from './quick_select_popover'; +export { EuiQuickSelect, EuiQuickSelectProps } from './quick_select'; +export { EuiRecentlyUsed, EuiRecentlyUsedProps } from './recently_used'; +export { + EuiRefreshInterval, + EuiRefreshIntervalProps, +} from './refresh_interval'; 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.tsx similarity index 100% rename from src/components/date_picker/super_date_picker/quick_select_popover/quick_select.test.js rename to src/components/date_picker/super_date_picker/quick_select_popover/quick_select.test.tsx index f5b950bbee8..9c319f06ebf 100644 --- 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.tsx @@ -6,8 +6,8 @@ import { EuiQuickSelect } from './quick_select'; const noop = () => {}; const defaultProps = { applyTime: noop, - start: 'now-15m', end: 'now', + start: 'now-15m', }; // Mock the htmlIdGenerator to generate predictable ids for snapshot tests @@ -28,8 +28,8 @@ describe('EuiQuickSelect', () => { {...defaultProps} prevQuickSelect={{ timeTense: 'Next', - timeValue: 32, timeUnits: 'M', + timeValue: 32, }} /> ); 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.tsx similarity index 75% rename from src/components/date_picker/super_date_picker/quick_select_popover/quick_select.js rename to src/components/date_picker/super_date_picker/quick_select_popover/quick_select.tsx index 5e7de8ff3f5..f792c027c69 100644 --- 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.tsx @@ -1,5 +1,8 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; +import React, { + Component, + ChangeEventHandler, + KeyboardEventHandler, +} from 'react'; import moment from 'moment'; import dateMath from '@elastic/datemath'; import { htmlIdGenerator } from '../../../../services'; @@ -12,7 +15,8 @@ import { EuiHorizontalRule } from '../../../horizontal_rule'; import { EuiI18n } from '../../../i18n'; import { timeUnits } from '../time_units'; import { EuiScreenReaderOnly } from '../../../accessibility'; -import { parseTimeParts } from './quick_select_utils'; +import { ApplyTime, QuickSelect, TimeUnitId } from '../../types'; +import { keysOf } from '../../../common'; const LAST = 'last'; const NEXT = 'next'; @@ -21,50 +25,73 @@ const timeTenseOptions = [ { value: LAST, text: 'Last' }, { value: NEXT, text: 'Next' }, ]; -const timeUnitsOptions = Object.keys(timeUnits).map(key => { +const timeUnitsOptions = keysOf(timeUnits).map(key => { return { value: key, text: `${timeUnits[key]}s` }; }); -export class EuiQuickSelect extends Component { - constructor(props) { - super(props); +const defaultQuickSelect: QuickSelect = { + timeTense: LAST, + timeValue: 15, + timeUnits: 'm', +}; - const { timeTense, timeValue, timeUnits } = this.props.prevQuickSelect; - const { - timeTenseDefault, - timeValueDefault, - timeUnitsDefault, - } = parseTimeParts(this.props.start, this.props.end); - this.state = { - timeTense: timeTense ? timeTense : timeTenseDefault, - timeValue: timeValue ? timeValue : timeValueDefault, - timeUnits: timeUnits ? timeUnits : timeUnitsDefault, - }; - } +type EuiQuickSelectState = QuickSelect; + +export interface EuiQuickSelectProps { + applyTime: ApplyTime; + start: string; + end: string; + prevQuickSelect: EuiQuickSelectState; +} + +export class EuiQuickSelect extends Component< + EuiQuickSelectProps, + EuiQuickSelectState +> { + static defaultProps = { + prevQuickSelect: defaultQuickSelect, + }; + + state: EuiQuickSelectState = { + timeTense: + this.props.prevQuickSelect && this.props.prevQuickSelect.timeTense + ? this.props.prevQuickSelect.timeTense + : defaultQuickSelect.timeTense, + timeValue: + this.props.prevQuickSelect && this.props.prevQuickSelect.timeValue + ? this.props.prevQuickSelect.timeValue + : defaultQuickSelect.timeValue, + timeUnits: + this.props.prevQuickSelect && this.props.prevQuickSelect.timeUnits + ? this.props.prevQuickSelect.timeUnits + : defaultQuickSelect.timeUnits, + }; generateId = htmlIdGenerator(); - onTimeTenseChange = evt => { + onTimeTenseChange: ChangeEventHandler = event => { this.setState({ - timeTense: evt.target.value, + timeTense: event.target.value, }); }; - onTimeValueChange = evt => { - const sanitizedValue = parseInt(evt.target.value, 10); + onTimeValueChange: ChangeEventHandler = event => { + const sanitizedValue = parseInt(event.target.value, 10); this.setState({ - timeValue: isNaN(sanitizedValue) ? '' : sanitizedValue, + timeValue: isNaN(sanitizedValue) ? 0 : sanitizedValue, }); }; - onTimeUnitsChange = evt => { + onTimeUnitsChange: ChangeEventHandler = event => { this.setState({ - timeUnits: evt.target.value, + timeUnits: event.target.value as TimeUnitId, }); }; - handleKeyDown = ({ key }) => { - if (key === 'Enter') this.applyQuickSelect(); + handleKeyDown: KeyboardEventHandler = ({ key }) => { + if (key === 'Enter') { + this.applyQuickSelect(); + } }; applyQuickSelect = () => { @@ -130,13 +157,17 @@ export class EuiQuickSelect extends Component { const { timeTense, timeValue, timeUnits } = this.state; const timeSelectionId = this.generateId(); const legendId = this.generateId(); + const matchedTimeUnit = timeUnitsOptions.find( + ({ value }) => value === timeUnits + ); + const timeUnit = matchedTimeUnit ? matchedTimeUnit.text : ''; return (

- {legendText => ( + {(legendText: string) => ( // Legend needs to be the first thing in a fieldset, but we want the visible title within the flex. // So we hide it, but allow screen readers to see it @@ -155,7 +186,7 @@ export class EuiQuickSelect extends Component { - {quickSelectTitle => ( + {(quickSelectTitle: string) => (
{quickSelectTitle}
@@ -168,7 +199,7 @@ export class EuiQuickSelect extends Component { - {previousLabel => ( + {(previousLabel: string) => ( - {nextLabel => ( + {(nextLabel: string) => ( - {tenseLabel => ( + {(tenseLabel: string) => ( - {valueLabel => ( + {(valueLabel: string) => ( - {unitLabel => ( + {(unitLabel: string) => ( + disabled={timeValue <= 0}> - -

+ +

option.value === timeUnits - ).text, + timeUnit, }} />

@@ -274,14 +303,3 @@ export class EuiQuickSelect extends Component { ); } } - -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_popover.test.js b/src/components/date_picker/super_date_picker/quick_select_popover/quick_select_popover.test.tsx similarity index 85% rename from src/components/date_picker/super_date_picker/quick_select_popover/quick_select_popover.test.js rename to src/components/date_picker/super_date_picker/quick_select_popover/quick_select_popover.test.tsx index 20a5f4952f2..dd0275db6d0 100644 --- a/src/components/date_picker/super_date_picker/quick_select_popover/quick_select_popover.test.js +++ b/src/components/date_picker/super_date_picker/quick_select_popover/quick_select_popover.test.tsx @@ -1,11 +1,14 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { EuiQuickSelectPopover } from './quick_select_popover'; +import { + EuiQuickSelectPopover, + EuiQuickSelectPopoverProps, +} from './quick_select_popover'; const noop = () => {}; -const defaultProps = { +const defaultProps: EuiQuickSelectPopoverProps = { applyTime: noop, applyRefreshInterval: noop, start: 'now-15m', 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.tsx similarity index 59% rename from src/components/date_picker/super_date_picker/quick_select_popover/quick_select_popover.js rename to src/components/date_picker/super_date_picker/quick_select_popover/quick_select_popover.tsx index 1b26436902f..080c0000497 100644 --- 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.tsx @@ -1,10 +1,4 @@ -import PropTypes from 'prop-types'; import React, { Component, Fragment } from 'react'; -import { - commonlyUsedRangeShape, - recentlyUsedRangeShape, - quickSelectPanelShape, -} from '../types'; import { EuiButtonEmpty } from '../../../button'; import { EuiIcon } from '../../../icon'; @@ -18,9 +12,39 @@ import { EuiQuickSelect } from './quick_select'; import { EuiCommonlyUsedTimeRanges } from './commonly_used_time_ranges'; import { EuiRecentlyUsed } from './recently_used'; import { EuiRefreshInterval } from './refresh_interval'; +import { + DurationRange, + ApplyRefreshInterval, + ApplyTime, + QuickSelect, + QuickSelectPanel, +} from '../../types'; + +export interface EuiQuickSelectPopoverProps { + applyRefreshInterval?: ApplyRefreshInterval; + applyTime: ApplyTime; + commonlyUsedRanges: DurationRange[]; + customQuickSelectPanels?: QuickSelectPanel[]; + dateFormat: string; + end: string; + isAutoRefreshOnly: boolean; + isDisabled: boolean; + isPaused: boolean; + recentlyUsedRanges: DurationRange[]; + refreshInterval: number; + start: string; +} + +interface EuiQuickSelectPopoverState { + isOpen: boolean; + prevQuickSelect?: QuickSelect; +} -export class EuiQuickSelectPopover extends Component { - state = { +export class EuiQuickSelectPopover extends Component< + EuiQuickSelectPopoverProps, + EuiQuickSelectPopoverState +> { + state: EuiQuickSelectPopoverState = { isOpen: false, }; @@ -34,7 +58,12 @@ export class EuiQuickSelectPopover extends Component { })); }; - applyTime = ({ start, end, quickSelect, keepPopoverOpen = false }) => { + applyTime: ApplyTime = ({ + start, + end, + quickSelect, + keepPopoverOpen = false, + }) => { this.props.applyTime({ start, end, @@ -48,7 +77,17 @@ export class EuiQuickSelectPopover extends Component { }; renderDateTimeSections = () => { - if (this.props.isAutoRefreshOnly) { + const { + commonlyUsedRanges, + dateFormat, + end, + isAutoRefreshOnly, + recentlyUsedRanges, + start, + } = this.props; + const { prevQuickSelect } = this.state; + + if (isAutoRefreshOnly) { return null; } @@ -56,19 +95,19 @@ export class EuiQuickSelectPopover extends Component { {this.renderCustomQuickSelectPanels()} @@ -76,11 +115,12 @@ export class EuiQuickSelectPopover extends Component { }; renderCustomQuickSelectPanels = () => { - if (!this.props.customQuickSelectPanels) { + const { customQuickSelectPanels } = this.props; + if (!customQuickSelectPanels) { return null; } - return this.props.customQuickSelectPanels.map(({ title, content }) => { + return customQuickSelectPanels.map(({ title, content }) => { return ( @@ -97,6 +137,15 @@ export class EuiQuickSelectPopover extends Component { }; render() { + const { + applyRefreshInterval, + isAutoRefreshOnly, + isDisabled, + isPaused, + refreshInterval, + } = this.props; + const { isOpen } = this.state; + const quickSelectButton = ( - + ); @@ -122,7 +165,7 @@ export class EuiQuickSelectPopover extends Component { {this.renderDateTimeSections()} ); } } - -EuiQuickSelectPopover.propTypes = { - applyTime: PropTypes.func.isRequired, - start: PropTypes.string.isRequired, - end: PropTypes.string.isRequired, - applyRefreshInterval: PropTypes.func, - isDisabled: PropTypes.bool.isRequired, - isPaused: PropTypes.bool.isRequired, - refreshInterval: PropTypes.number.isRequired, - commonlyUsedRanges: PropTypes.arrayOf(commonlyUsedRangeShape).isRequired, - dateFormat: PropTypes.string.isRequired, - recentlyUsedRanges: PropTypes.arrayOf(recentlyUsedRangeShape).isRequired, - isAutoRefreshOnly: PropTypes.bool.isRequired, - customQuickSelectPanels: PropTypes.arrayOf(quickSelectPanelShape), -}; diff --git a/src/components/date_picker/super_date_picker/quick_select_popover/quick_select_utils.test.js b/src/components/date_picker/super_date_picker/quick_select_popover/quick_select_utils.test.ts similarity index 71% rename from src/components/date_picker/super_date_picker/quick_select_popover/quick_select_utils.test.js rename to src/components/date_picker/super_date_picker/quick_select_popover/quick_select_utils.test.ts index 1a104afd8ef..c4990762cf9 100644 --- a/src/components/date_picker/super_date_picker/quick_select_popover/quick_select_utils.test.js +++ b/src/components/date_picker/super_date_picker/quick_select_popover/quick_select_utils.test.ts @@ -5,27 +5,27 @@ describe('parseTimeParts', () => { it('should parse now', () => { const out = parseTimeParts('now', 'now+5m'); expect(out).toEqual({ - timeValueDefault: 5, - timeUnitsDefault: 'm', - timeTenseDefault: 'next', + timeTense: 'next', + timeUnits: 'm', + timeValue: 5, }); }); it('should parse now-2h', () => { const out = parseTimeParts('now-2h', 'now+5m'); expect(out).toEqual({ - timeValueDefault: 2, - timeUnitsDefault: 'h', - timeTenseDefault: 'last', + timeTense: 'last', + timeUnits: 'h', + timeValue: 2, }); }); it('should parse now+2h', () => { const out = parseTimeParts('now+2h', 'now+5m'); expect(out).toEqual({ - timeValueDefault: 2, - timeUnitsDefault: 'h', - timeTenseDefault: 'next', + timeTense: 'next', + timeUnits: 'h', + timeValue: 2, }); }); @@ -42,9 +42,9 @@ describe('parseTimeParts', () => { it('should parse now/d', () => { const out = parseTimeParts('now/d', 'now+5m'); expect(out).toEqual({ - timeValueDefault: 6, - timeUnitsDefault: 'h', - timeTenseDefault: 'last', + timeTense: 'last', + timeUnits: 'h', + timeValue: 6, }); }); }); diff --git a/src/components/date_picker/super_date_picker/quick_select_popover/quick_select_utils.js b/src/components/date_picker/super_date_picker/quick_select_popover/quick_select_utils.ts similarity index 52% rename from src/components/date_picker/super_date_picker/quick_select_popover/quick_select_utils.js rename to src/components/date_picker/super_date_picker/quick_select_popover/quick_select_utils.ts index 8b88f7ebb62..db44c410ae3 100644 --- a/src/components/date_picker/super_date_picker/quick_select_popover/quick_select_utils.js +++ b/src/components/date_picker/super_date_picker/quick_select_popover/quick_select_utils.ts @@ -1,34 +1,31 @@ -/** - * This function returns time value, time unit and time tense for a given time string. - * For example: for `now-40m` it will parse output as time value to `40` - * time unit to `m` and time unit to `last`. - * If given a datetime string it will return a default value. - * If the given string is in the format such as `now/d` it will parse the string to moment object - * and find the time value, time unit and time tense using moment - * This function accepts two strings start and end time. I the start value is now then it uses - * the end value to parse. - * - * @param {string} start start time - * @param {string} start end time - * @returns {object} time value, time unit and time tense - */ - import moment from 'moment'; import dateMath from '@elastic/datemath'; import { isString } from '../../../../services/predicate'; import { relativeUnitsFromLargestToSmallest } from '../relative_options'; import { DATE_MODES } from '../date_modes'; +import { QuickSelect, TimeUnitId } from '../../types'; const LAST = 'last'; const NEXT = 'next'; -const isNow = value => value === DATE_MODES.NOW; +const isNow = (value: string) => value === DATE_MODES.NOW; -export const parseTimeParts = (start, end) => { - const results = { - timeValueDefault: 15, - timeUnitsDefault: 'm', - timeTenseDefault: LAST, +/** + * This function returns time value, time unit and time tense for a given time string. + * + * For example: for `now-40m` it will parse output as time value to `40` time unit to `m` and time unit to `last`. + * + * If given a datetime string it will return a default value. + * + * If the given string is in the format such as `now/d` it will parse the string to moment object and find the time value, time unit and time tense using moment + * + * This function accepts two strings start and end time. I the start value is now then it uses the end value to parse. + */ +export const parseTimeParts = (start: string, end: string): QuickSelect => { + const results: QuickSelect = { + timeTense: LAST, + timeUnits: 'm', + timeValue: 15, }; const value = isNow(start) ? end : start; @@ -42,17 +39,14 @@ export const parseTimeParts = (start, end) => { } const operator = matches[2]; - const timeValue = matches[3]; - const timeUnitsDefault = matches[4]; - - if (timeValue && timeUnitsDefault && operator) { - const timeValueDefault = parseInt(timeValue, 10); - const timeTenseDefault = operator === '+' ? NEXT : LAST; + const matchedTimeValue = matches[3]; + const timeUnits = matches[4] as TimeUnitId; + if (matchedTimeValue && timeUnits && operator) { return { - timeValueDefault, - timeUnitsDefault, - timeTenseDefault, + timeTense: operator === '+' ? NEXT : LAST, + timeUnits, + timeValue: parseInt(matchedTimeValue, 10), }; } @@ -60,13 +54,17 @@ export const parseTimeParts = (start, end) => { let unitOp = ''; for (let i = 0; i < relativeUnitsFromLargestToSmallest.length; i++) { const as = duration.as(relativeUnitsFromLargestToSmallest[i]); - if (as < 0) unitOp = '+'; + if (as < 0) { + unitOp = '+'; + } if (Math.abs(as) > 1) { - results.timeValueDefault = Math.round(Math.abs(as)); - results.timeUnitsDefault = relativeUnitsFromLargestToSmallest[i]; - results.timeTenseDefault = unitOp === '+' ? NEXT : LAST; - break; + return { + timeValue: Math.round(Math.abs(as)), + timeUnits: relativeUnitsFromLargestToSmallest[i], + timeTense: unitOp === '+' ? NEXT : LAST, + }; } } + return results; }; 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.tsx similarity index 70% rename from src/components/date_picker/super_date_picker/quick_select_popover/recently_used.js rename to src/components/date_picker/super_date_picker/quick_select_popover/recently_used.tsx index b949f596bf0..397bd564678 100644 --- 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.tsx @@ -1,6 +1,4 @@ -import PropTypes from 'prop-types'; -import React, { Fragment } from 'react'; -import { commonlyUsedRangeShape, recentlyUsedRangeShape } from '../types'; +import React, { Fragment, FunctionComponent } from 'react'; import { prettyDuration } from '../pretty_duration'; import { EuiFlexGroup, EuiFlexItem } from '../../../flex'; @@ -9,13 +7,21 @@ import { EuiSpacer } from '../../../spacer'; import { EuiLink } from '../../../link'; import { EuiText } from '../../../text'; import { EuiHorizontalRule } from '../../../horizontal_rule'; +import { DurationRange, ApplyTime } from '../../types'; -export function EuiRecentlyUsed({ +export interface EuiRecentlyUsedProps { + applyTime: ApplyTime; + commonlyUsedRanges: DurationRange[]; + dateFormat: string; + recentlyUsedRanges?: DurationRange[]; +} + +export const EuiRecentlyUsed: FunctionComponent = ({ applyTime, commonlyUsedRanges, dateFormat, - recentlyUsedRanges, -}) { + recentlyUsedRanges = [], +}) => { if (recentlyUsedRanges.length === 0) { return null; } @@ -47,15 +53,6 @@ export function EuiRecentlyUsed({ ); -} - -EuiRecentlyUsed.propTypes = { - applyTime: PropTypes.func.isRequired, - commonlyUsedRanges: PropTypes.arrayOf(commonlyUsedRangeShape).isRequired, - dateFormat: PropTypes.string.isRequired, - recentlyUsedRanges: PropTypes.arrayOf(recentlyUsedRangeShape), }; -EuiRecentlyUsed.defaultProps = { - recentlyUsedRanges: [], -}; +EuiRecentlyUsed.displayName = 'EuiRecentlyUsed'; 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.tsx similarity index 60% rename from src/components/date_picker/super_date_picker/quick_select_popover/refresh_interval.js rename to src/components/date_picker/super_date_picker/quick_select_popover/refresh_interval.tsx index 0ddabdf1f66..d0c5593f6dd 100644 --- 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.tsx @@ -1,5 +1,8 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; +import React, { + Component, + ChangeEventHandler, + KeyboardEventHandler, +} from 'react'; import { timeUnits, timeUnitsPlural } from '../time_units'; import { EuiI18n } from '../../../i18n'; import { EuiFlexGroup, EuiFlexItem } from '../../../flex'; @@ -9,23 +12,24 @@ import { EuiSelect, EuiFieldNumber } from '../../../form'; import { EuiButton } from '../../../button'; import { htmlIdGenerator } from '../../../../services'; import { EuiScreenReaderOnly } from '../../../accessibility'; - -const refreshUnitsOptions = Object.keys(timeUnits) - .filter(timeUnit => { - return timeUnit === 'h' || timeUnit === 'm' || timeUnit === 's'; - }) - .map(timeUnit => { - return { value: timeUnit, text: timeUnitsPlural[timeUnit] }; - }); +import { + Milliseconds, + TimeUnitId, + RelativeOption, + ApplyRefreshInterval, +} from '../../types'; +import { keysOf } from '../../../common'; + +const refreshUnitsOptions: RelativeOption[] = keysOf(timeUnits) + .filter(timeUnit => timeUnit === 'h' || timeUnit === 'm' || timeUnit === 's') + .map(timeUnit => ({ value: timeUnit, text: timeUnitsPlural[timeUnit] })); const MILLISECONDS_IN_SECOND = 1000; const MILLISECONDS_IN_MINUTE = MILLISECONDS_IN_SECOND * 60; const MILLISECONDS_IN_HOUR = MILLISECONDS_IN_MINUTE * 60; -function fromMilliseconds(milliseconds) { - function round(value) { - return parseFloat(value.toFixed(2)); - } +function fromMilliseconds(milliseconds: Milliseconds): EuiRefreshIntervalState { + const round = (value: number) => parseFloat(value.toFixed(2)); if (milliseconds > MILLISECONDS_IN_HOUR) { return { units: 'h', @@ -46,7 +50,7 @@ function fromMilliseconds(milliseconds) { }; } -function toMilliseconds(units, value) { +function toMilliseconds(units: TimeUnitId, value: Milliseconds) { switch (units) { case 'h': return Math.round(value * MILLISECONDS_IN_HOUR); @@ -58,21 +62,27 @@ function toMilliseconds(units, value) { } } -export class EuiRefreshInterval extends Component { - constructor(props) { - super(props); +export interface EuiRefreshIntervalProps { + applyRefreshInterval?: ApplyRefreshInterval; + isPaused: boolean; + refreshInterval: Milliseconds; +} - const { value, units } = fromMilliseconds(props.refreshInterval); - this.state = { - value, - units, - }; - } +interface EuiRefreshIntervalState { + value: number | ''; + units: TimeUnitId; +} + +export class EuiRefreshInterval extends Component< + EuiRefreshIntervalProps, + EuiRefreshIntervalState +> { + state: EuiRefreshIntervalState = fromMilliseconds(this.props.refreshInterval); generateId = htmlIdGenerator(); - onValueChange = evt => { - const sanitizedValue = parseFloat(evt.target.value); + onValueChange: ChangeEventHandler = event => { + const sanitizedValue = parseFloat(event.target.value); this.setState( { value: isNaN(sanitizedValue) ? '' : sanitizedValue, @@ -81,64 +91,77 @@ export class EuiRefreshInterval extends Component { ); }; - onUnitsChange = evt => { + onUnitsChange: ChangeEventHandler = event => { this.setState( { - units: evt.target.value, + units: event.target.value as TimeUnitId, }, this.applyRefreshInterval ); }; startRefresh = () => { + const { applyRefreshInterval } = this.props; const { value, units } = this.state; - const isValid = value !== '' && value > 0; - if (isValid) { - this.props.applyRefreshInterval({ + + if (value !== '' && value > 0 && applyRefreshInterval !== undefined) { + applyRefreshInterval({ refreshInterval: toMilliseconds(units, value), isPaused: false, }); } }; - handleKeyDown = ({ key }) => { + handleKeyDown: KeyboardEventHandler = ({ key }) => { if (key === 'Enter') { this.startRefresh(); } }; applyRefreshInterval = () => { - if (this.state.value === '') { + const { applyRefreshInterval, isPaused } = this.props; + const { units, value } = this.state; + if (value === '') { + return; + } + if (!applyRefreshInterval) { return; } - const valueInMilliSeconds = toMilliseconds( - this.state.units, - this.state.value - ); + const refreshInterval = toMilliseconds(units, value); - this.props.applyRefreshInterval({ - refreshInterval: valueInMilliSeconds, - isPaused: valueInMilliSeconds <= 0 ? true : this.props.isPaused, + applyRefreshInterval({ + refreshInterval, + isPaused: refreshInterval <= 0 ? true : isPaused, }); }; toggleRefresh = () => { - this.props.applyRefreshInterval({ - refreshInterval: toMilliseconds(this.state.units, this.state.value), - isPaused: !this.props.isPaused, + const { applyRefreshInterval, isPaused } = this.props; + const { units, value } = this.state; + + if (!applyRefreshInterval || value === '') { + return; + } + applyRefreshInterval({ + refreshInterval: toMilliseconds(units, value), + isPaused: !isPaused, }); }; render() { + const { applyRefreshInterval, isPaused } = this.props; + const { value, units } = this.state; const legendId = this.generateId(); const refreshSelectionId = this.generateId(); - const { value, units } = this.state; - if (!this.props.applyRefreshInterval) { + if (!applyRefreshInterval) { return null; } + const options = refreshUnitsOptions.find(({ value }) => value === units); + const optionText = options ? options.text : ''; + return (
@@ -177,13 +200,13 @@ export class EuiRefreshInterval extends Component { - {this.props.isPaused ? ( + aria-describedby={refreshSelectionId}> + {isPaused ? ( ) : ( @@ -191,16 +214,14 @@ export class EuiRefreshInterval extends Component { - -

+ +

option.value === units - ).text, + optionText, }} />

@@ -209,9 +230,3 @@ export class EuiRefreshInterval extends Component { ); } } - -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.ts b/src/components/date_picker/super_date_picker/relative_options.ts index 435ff6d5434..8da3a252812 100644 --- a/src/components/date_picker/super_date_picker/relative_options.ts +++ b/src/components/date_picker/super_date_picker/relative_options.ts @@ -1,4 +1,6 @@ -export const relativeOptions = [ +import { RelativeOption, TimeUnitId } from '../types'; + +export const relativeOptions: RelativeOption[] = [ { text: 'Seconds ago', value: 's' }, { text: 'Minutes ago', value: 'm' }, { text: 'Hours ago', value: 'h' }, @@ -16,11 +18,8 @@ export const relativeOptions = [ { text: 'Years from now', value: 'y+' }, ]; -export const relativeUnitsFromLargestToSmallest = relativeOptions - .filter(({ value }) => { - return !value.includes('+'); - }) - .map(({ value }) => { - return value; - }) - .reverse(); +const timeUnitIds = relativeOptions + .map(({ value }) => value) + .filter(value => !value.includes('+')) as TimeUnitId[]; + +export const relativeUnitsFromLargestToSmallest = timeUnitIds.reverse(); 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.ts similarity index 100% rename from src/components/date_picker/super_date_picker/relative_utils.test.js rename to src/components/date_picker/super_date_picker/relative_utils.test.ts diff --git a/src/components/date_picker/super_date_picker/relative_utils.js b/src/components/date_picker/super_date_picker/relative_utils.ts similarity index 72% rename from src/components/date_picker/super_date_picker/relative_utils.js rename to src/components/date_picker/super_date_picker/relative_utils.ts index adbdb4e8b85..8e5e9bf3b46 100644 --- a/src/components/date_picker/super_date_picker/relative_utils.js +++ b/src/components/date_picker/super_date_picker/relative_utils.ts @@ -4,10 +4,11 @@ import moment from 'moment'; import { get } from '../../../services/objects'; import { isString } from '../../../services/predicate'; import { relativeUnitsFromLargestToSmallest } from './relative_options'; +import { TimeUnitId, RelativeParts } from '../types'; const ROUND_DELIMETER = '/'; -export function parseRelativeParts(value) { +export function parseRelativeParts(value: string): RelativeParts { const matches = isString(value) && value.match(/now(([\-\+])([0-9]+)([smhdwMy])(\/[smhdwMy])?)?/); @@ -19,11 +20,15 @@ export function parseRelativeParts(value) { if (count && unit) { const isRounded = roundBy ? true : false; + const roundUnit = + isRounded && roundBy + ? (roundBy.replace(ROUND_DELIMETER, '') as TimeUnitId) + : undefined; return { count: parseInt(count, 10), unit: operator === '+' ? `${unit}+` : unit, round: isRounded, - roundUnit: isRounded ? roundBy.replace(ROUND_DELIMETER, '') : undefined, + ...(roundUnit ? { roundUnit } : {}), }; } @@ -31,10 +36,10 @@ export function parseRelativeParts(value) { 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)); + const asRelative = duration.as(relativeUnitsFromLargestToSmallest[i]); + if (asRelative < 0) unitOp = '+'; + if (Math.abs(asRelative) > 1) { + results.count = Math.round(Math.abs(asRelative)); results.unit = relativeUnitsFromLargestToSmallest[i] + unitOp; results.round = false; break; @@ -43,7 +48,7 @@ export function parseRelativeParts(value) { return results; } -export function toRelativeStringFromParts(relativeParts) { +export const toRelativeStringFromParts = (relativeParts: RelativeParts) => { const count = get(relativeParts, 'count', 0); const isRounded = get(relativeParts, 'round', false); @@ -57,4 +62,4 @@ export function toRelativeStringFromParts(relativeParts) { const round = isRounded ? `${ROUND_DELIMETER}${unit}` : ''; return `now${operator}${count}${unit}${round}`; -} +}; 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.tsx similarity index 77% rename from src/components/date_picker/super_date_picker/super_date_picker.test.js rename to src/components/date_picker/super_date_picker/super_date_picker.test.tsx index b21e3857a2d..949e7309c31 100644 --- 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.tsx @@ -14,7 +14,9 @@ describe('EuiSuperDatePicker', () => { test('refresh is disabled by default', () => { // By default we expect `asyncInterval` to be not set. - const componentPaused = mount(); + const componentPaused = mount( + + ); const instancePaused = componentPaused.instance(); expect(instancePaused.asyncInterval).toBe(undefined); expect(componentPaused.prop('isPaused')).toBe(true); @@ -24,7 +26,7 @@ describe('EuiSuperDatePicker', () => { // If refresh is enabled via `isPaused/onRefresh` we expect // `asyncInterval` to be present and `asyncInterval.isStopped` to be `false`. const onRefresh = jest.fn(); - const componentRefresh = mount( + const componentRefresh = mount( { ); const instanceRefresh = componentRefresh.instance(); expect(typeof instanceRefresh.asyncInterval).toBe('object'); - expect(instanceRefresh.asyncInterval.isStopped).toBe(false); + expect(instanceRefresh.asyncInterval!.isStopped).toBe(false); expect(componentRefresh.prop('isPaused')).toBe(false); // If we update the prop `isPaused` we expect the interval to be stopped too. componentRefresh.setProps({ isPaused: true }); const instanceUpdatedPaused = componentRefresh.instance(); expect(typeof instanceUpdatedPaused.asyncInterval).toBe('object'); - expect(instanceUpdatedPaused.asyncInterval.isStopped).toBe(true); + expect(instanceUpdatedPaused.asyncInterval!.isStopped).toBe(true); expect(componentRefresh.prop('isPaused')).toBe(true); // Let's start refresh again for a final sanity check. componentRefresh.setProps({ isPaused: false }); const instanceUpdatedRefresh = componentRefresh.instance(); expect(typeof instanceUpdatedRefresh.asyncInterval).toBe('object'); - expect(instanceUpdatedRefresh.asyncInterval.isStopped).toBe(false); + expect(instanceUpdatedRefresh.asyncInterval!.isStopped).toBe(false); expect(componentRefresh.prop('isPaused')).toBe(false); }); @@ -56,7 +58,7 @@ describe('EuiSuperDatePicker', () => { const onRefresh = jest.fn(); - const componentRefresh = mount( + const componentRefresh = mount( { ); const instanceRefresh = componentRefresh.instance(); + expect(typeof instanceRefresh.asyncInterval).toBe('object'); jest.advanceTimersByTime(10); - await instanceRefresh.asyncInterval.__pendingFn; + await instanceRefresh.asyncInterval!.__pendingFn; jest.advanceTimersByTime(10); - await instanceRefresh.asyncInterval.__pendingFn; + await instanceRefresh.asyncInterval!.__pendingFn; expect(onRefresh).toBeCalledTimes(2); @@ -82,7 +85,7 @@ describe('EuiSuperDatePicker', () => { const onRefresh = jest.fn(); - const componentRefresh = mount( + const componentRefresh = mount( { const instanceRefresh = componentRefresh.instance(); jest.advanceTimersByTime(10); - await instanceRefresh.asyncInterval.__pendingFn; + expect(typeof instanceRefresh.asyncInterval).toBe('object'); + await instanceRefresh.asyncInterval!.__pendingFn; componentRefresh.setProps({ isPaused: true, refreshInterval: 0 }); jest.advanceTimersByTime(10); - await instanceRefresh.asyncInterval.__pendingFn; + await instanceRefresh.asyncInterval!.__pendingFn; expect(onRefresh).toBeCalledTimes(1); 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.tsx similarity index 61% rename from src/components/date_picker/super_date_picker/super_date_picker.js rename to src/components/date_picker/super_date_picker/super_date_picker.tsx index b6e37dc6c31..1d770187765 100644 --- a/src/components/date_picker/super_date_picker/super_date_picker.js +++ b/src/components/date_picker/super_date_picker/super_date_picker.tsx @@ -1,11 +1,5 @@ -import PropTypes from 'prop-types'; import React, { Component } from 'react'; import classNames from 'classnames'; -import { - commonlyUsedRangeShape, - recentlyUsedRangeShape, - quickSelectPanelShape, -} from './types'; import { prettyDuration, showPrettyDuration, @@ -25,10 +19,104 @@ import { EuiFlexGroup, EuiFlexItem } from '../../flex'; import { AsyncInterval } from './async_interval'; import { EuiI18n } from '../../i18n'; import { EuiI18nConsumer } from '../../context'; +import { CommonProps } from '../../common'; +import { + ShortDate, + Milliseconds, + DurationRange, + ApplyTime, + ApplyRefreshInterval, + QuickSelectPanel, +} from '../types'; +import { EuiDatePopoverContentProps } from './date_popover/date_popover_content'; +import { LocaleSpecifier } from 'moment'; // eslint-disable-line import/named export { prettyDuration, commonDurationRanges }; -function isRangeInvalid(start, end) { +export interface OnTimeChangeProps extends DurationRange { + isInvalid: boolean; + isQuickSelection: boolean; +} + +export interface OnRefreshProps extends DurationRange { + refreshInterval: number; +} + +export type EuiSuperDatePickerProps = CommonProps & { + commonlyUsedRanges: DurationRange[]; + customQuickSelectPanels?: QuickSelectPanel[]; + + /** + * Specifies the formatted used when displaying dates and/or datetimes + */ + dateFormat: string; + end: ShortDate; + + /** + * Set isAutoRefreshOnly to true to limit the component to only display auto refresh content. + */ + isAutoRefreshOnly: boolean; + isDisabled: boolean; + isLoading?: boolean; + isPaused: boolean; + + /** + * Used to localize e.g. month names, passed to `moment` + */ + locale?: LocaleSpecifier; + + /** + * Callback for when the refresh interval is fired. + * EuiSuperDatePicker will only manage a refresh interval timer when onRefresh callback is supplied + * If a promise is returned, the next refresh interval will not start until the promise has resolved. + * If the promise rejects the refresh interval will stop and the error thrown + */ + onRefresh?: (props: OnRefreshProps) => void; + + /** + * Callback for when the refresh interval changes. + * Supply onRefreshChange to show refresh interval inputs in quick select popover + */ + onRefreshChange?: ApplyRefreshInterval; + + /** + * Callback for when the time changes. + */ + onTimeChange: (props: OnTimeChangeProps) => void; + recentlyUsedRanges: DurationRange[]; + + /** + * Refresh interval in milliseconds + */ + refreshInterval: Milliseconds; + + /** + * Set showUpdateButton to false to immediately invoke onTimeChange for all start and end changes. + */ + showUpdateButton: boolean; + start: ShortDate; + + /** + * Specifies the formatted used when displaying times + */ + timeFormat: string; +}; + +interface EuiSuperDatePickerState { + end: ShortDate; + hasChanged: boolean; + isEndDatePopoverOpen: boolean; + isInvalid: boolean; + isStartDatePopoverOpen: boolean; + prevProps: { + end: ShortDate; + start: ShortDate; + }; + showPrettyDuration: boolean; + start: ShortDate; +} + +function isRangeInvalid(start: ShortDate, end: ShortDate) { if (start === 'now' && end === 'now') { return true; } @@ -50,91 +138,48 @@ function isRangeInvalid(start, end) { return false; } -export class EuiSuperDatePicker extends Component { - static propTypes = { - isLoading: PropTypes.bool, - isDisabled: 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, isQuickSelection, isInvalid } - */ - 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, - - /** - * Callback for when the refresh interval is fired. Called with { start, end, refreshInterval } - * EuiSuperDatePicker will only manage a refresh interval timer when onRefresh callback is supplied - * If a promise is returned, the next refresh interval will not start until the promise has resolved. - * If the promise rejects the refresh interval will stop and the error thrown - */ - onRefresh: 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), - /** - * Used to localize e.g. month names, passed to `moment` - */ - locale: PropTypes.string, - /** - * Specifies the formatted used when displaying dates and/or datetimes - */ - dateFormat: PropTypes.string, - /** - * Specifies the formatted used when displaying times - */ - timeFormat: 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), - /** - * Set showUpdateButton to false to immediately invoke onTimeChange for all start and end changes. - */ - showUpdateButton: PropTypes.bool, - /** - * Set isAutoRefreshOnly to true to limit the component to only display auto refresh content. - */ - isAutoRefreshOnly: PropTypes.bool, - customQuickSelectPanels: PropTypes.arrayOf(quickSelectPanelShape), - }; - +export class EuiSuperDatePicker extends Component< + EuiSuperDatePickerProps, + EuiSuperDatePickerState +> { static defaultProps = { - start: 'now-15m', - end: 'now', - isPaused: true, - isDisabled: false, - refreshInterval: 0, commonlyUsedRanges: commonDurationRanges, dateFormat: 'MMM D, YYYY @ HH:mm:ss.SSS', - timeFormat: 'HH:mm', + end: 'now', + isAutoRefreshOnly: false, + isDisabled: false, + isPaused: true, recentlyUsedRanges: [], + refreshInterval: 0, showUpdateButton: true, - isAutoRefreshOnly: false, + start: 'now-15m', + timeFormat: 'HH:mm', + }; + + asyncInterval?: AsyncInterval; + + state: EuiSuperDatePickerState = { + prevProps: { + start: this.props.start, + end: this.props.end, + }, + start: this.props.start, + end: this.props.end, + isInvalid: isRangeInvalid(this.props.start, this.props.end), + hasChanged: false, + showPrettyDuration: showPrettyDuration( + this.props.start, + this.props.end, + this.props.commonlyUsedRanges + ), + isStartDatePopoverOpen: false, + isEndDatePopoverOpen: false, }; - static getDerivedStateFromProps(nextProps, prevState) { + static getDerivedStateFromProps( + nextProps: EuiSuperDatePickerProps, + prevState: EuiSuperDatePickerState + ) { if ( nextProps.start !== prevState.prevProps.start || nextProps.end !== prevState.prevProps.end @@ -159,27 +204,7 @@ export class EuiSuperDatePicker extends Component { return null; } - constructor(props) { - super(props); - - const { start, end, commonlyUsedRanges } = this.props; - - this.state = { - prevProps: { - start: props.start, - end: props.end, - }, - start, - end, - isInvalid: isRangeInvalid(start, end), - hasChanged: false, - showPrettyDuration: showPrettyDuration(start, end, commonlyUsedRanges), - isStartDatePopoverOpen: false, - isEndDatePopoverOpen: false, - }; - } - - setTime = ({ start, end }) => { + setTime = ({ end, start }: DurationRange) => { const isInvalid = isRangeInvalid(start, end); this.setState({ @@ -216,11 +241,11 @@ export class EuiSuperDatePicker extends Component { this.stopInterval(); }; - setStart = start => { + setStart: EuiDatePopoverContentProps['onChange'] = (start: ShortDate) => { this.setTime({ start, end: this.state.end }); }; - setEnd = end => { + setEnd: EuiDatePopoverContentProps['onChange'] = (end: ShortDate) => { this.setTime({ start: this.state.start, end }); }; @@ -233,14 +258,10 @@ export class EuiSuperDatePicker extends Component { }); }; - applyQuickTime = ({ start, end }) => { - this.setState(prevState => ({ - showPrettyDuration: showPrettyDuration( - start, - end, - prevState.commonlyUsedRanges - ), - })); + applyQuickTime: ApplyTime = ({ start, end }) => { + this.setState({ + showPrettyDuration: showPrettyDuration(start, end, commonDurationRanges), + }); this.props.onTimeChange({ start, end, @@ -273,7 +294,7 @@ export class EuiSuperDatePicker extends Component { this.setState({ isEndDatePopoverOpen: false }); }; - onRefreshChange = ({ refreshInterval, isPaused }) => { + onRefreshChange: ApplyRefreshInterval = ({ refreshInterval, isPaused }) => { this.stopInterval(); if (!isPaused) { this.startInterval(refreshInterval); @@ -289,7 +310,7 @@ export class EuiSuperDatePicker extends Component { } }; - startInterval = refreshInterval => { + startInterval = (refreshInterval: number) => { const { onRefresh } = this.props; if (onRefresh) { const handler = () => { @@ -301,10 +322,27 @@ export class EuiSuperDatePicker extends Component { }; renderDatePickerRange = () => { - const { start, end, hasChanged, isInvalid } = this.state; - const { isDisabled } = this.props; - - if (this.props.isAutoRefreshOnly) { + const { + end, + hasChanged, + isEndDatePopoverOpen, + isInvalid, + isStartDatePopoverOpen, + showPrettyDuration, + start, + } = this.state; + const { + commonlyUsedRanges, + dateFormat, + isAutoRefreshOnly, + isDisabled, + isPaused, + locale, + refreshInterval, + timeFormat, + } = this.props; + + if (isAutoRefreshOnly) { return ( } readOnly> - {prettyInterval(this.props.isPaused, this.props.refreshInterval)} + {prettyInterval(Boolean(isPaused), refreshInterval)} ); } if ( - this.state.showPrettyDuration && - !this.state.isStartDatePopoverOpen && - !this.state.isEndDatePopoverOpen + showPrettyDuration && + !isStartDatePopoverOpen && + !isEndDatePopoverOpen ) { return ( - {prettyDuration( - start, - end, - this.props.commonlyUsedRanges, - this.props.dateFormat - )} + {prettyDuration(start, end, commonlyUsedRanges, dateFormat)} ); const flexWrapperClasses = classNames('euiSuperDatePicker__flexWrapper', { - 'euiSuperDatePicker__flexWrapper--noUpdateButton': !this.props - .showUpdateButton, - 'euiSuperDatePicker__flexWrapper--isAutoRefreshOnly': this.props - .isAutoRefreshOnly, + 'euiSuperDatePicker__flexWrapper--noUpdateButton': !showUpdateButton, + 'euiSuperDatePicker__flexWrapper--isAutoRefreshOnly': isAutoRefreshOnly, }); return ( @@ -469,7 +515,7 @@ export class EuiSuperDatePicker extends Component { {this.renderDatePickerRange()} diff --git a/src/components/date_picker/super_date_picker/super_update_button.test.js b/src/components/date_picker/super_date_picker/super_update_button.test.tsx similarity index 100% rename from src/components/date_picker/super_date_picker/super_update_button.test.js rename to src/components/date_picker/super_date_picker/super_update_button.test.tsx diff --git a/src/components/date_picker/super_date_picker/super_update_button.js b/src/components/date_picker/super_date_picker/super_update_button.tsx similarity index 75% rename from src/components/date_picker/super_date_picker/super_update_button.js rename to src/components/date_picker/super_date_picker/super_update_button.tsx index 893abd3f08e..274547868cb 100644 --- a/src/components/date_picker/super_date_picker/super_update_button.js +++ b/src/components/date_picker/super_date_picker/super_update_button.tsx @@ -1,29 +1,29 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; +import React, { Component, MouseEventHandler, Ref } from 'react'; import classNames from 'classnames'; import { EuiButton } from '../../button'; import { EuiI18n } from '../../i18n'; -import { EuiToolTip } from '../../tool_tip'; - -export class EuiSuperUpdateButton extends Component { - static propTypes = { - needsUpdate: PropTypes.bool, - isLoading: PropTypes.bool, - isDisabled: PropTypes.bool, - onClick: PropTypes.func.isRequired, - - /** - * Passes props to `EuiToolTip` - */ - toolTipProps: PropTypes.object, - - /** - * Show the "Click to apply" tooltip - */ - showTooltip: PropTypes.bool, - }; +import { EuiToolTip, EuiToolTipProps } from '../../tool_tip'; + +export interface EuiSuperUpdateButtonProps { + className?: string; + isDisabled: boolean; + isLoading: boolean; + needsUpdate: boolean; + onClick: MouseEventHandler; + + /** + * Passes props to `EuiToolTip` + */ + toolTipProps?: EuiToolTipProps; + + /** + * Show the "Click to apply" tooltip + */ + showTooltip: boolean; +} +export class EuiSuperUpdateButton extends Component { static defaultProps = { needsUpdate: false, isLoading: false, @@ -31,6 +31,10 @@ export class EuiSuperUpdateButton extends Component { showTooltip: false, }; + _isMounted = false; + tooltipTimeout: number | undefined; + tooltip: EuiToolTip | null = null; + componentWillUnmount() { this._isMounted = false; } @@ -42,17 +46,20 @@ export class EuiSuperUpdateButton extends Component { componentDidUpdate() { if ( this.props.showTooltip && + this.props.needsUpdate && !this.props.isDisabled && !this.props.isLoading ) { this.showTooltip(); - this.tooltipTimeout = setTimeout(() => { + this.tooltipTimeout = (setTimeout(() => { this.hideTooltip(); - }, 2000); + }, 2000) as unknown) as (number | undefined); } } - setTootipRef = node => (this.tooltip = node); + setTootipRef: Ref = node => { + this.tooltip = node; + }; showTooltip = () => { if (!this._isMounted || !this.tooltip) { diff --git a/src/components/date_picker/super_date_picker/time_units.js b/src/components/date_picker/super_date_picker/time_units.js deleted file mode 100644 index 9e058dcc500..00000000000 --- a/src/components/date_picker/super_date_picker/time_units.js +++ /dev/null @@ -1,19 +0,0 @@ -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/time_units.ts b/src/components/date_picker/super_date_picker/time_units.ts new file mode 100644 index 00000000000..e606cb926fd --- /dev/null +++ b/src/components/date_picker/super_date_picker/time_units.ts @@ -0,0 +1,21 @@ +import { TimeUnitId, TimeUnitLabel, TimeUnitLabelPlural } from '../types'; + +export const timeUnits: { [id in TimeUnitId]: TimeUnitLabel } = { + s: 'second', + m: 'minute', + h: 'hour', + d: 'day', + w: 'week', + M: 'month', + y: 'year', +}; + +export const timeUnitsPlural: { [id in TimeUnitId]: TimeUnitLabelPlural } = { + 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 deleted file mode 100644 index 2f334117777..00000000000 --- a/src/components/date_picker/super_date_picker/types.js +++ /dev/null @@ -1,17 +0,0 @@ -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, -}); - -export const quickSelectPanelShape = PropTypes.shape({ - title: PropTypes.string.isRequired, - content: PropTypes.node.isRequired, -}); diff --git a/src/components/date_picker/types.ts b/src/components/date_picker/types.ts new file mode 100644 index 00000000000..d071c825c93 --- /dev/null +++ b/src/components/date_picker/types.ts @@ -0,0 +1,74 @@ +import { ReactElement } from 'react'; + +export interface DurationRange { + end: ShortDate; + label?: string; + start: ShortDate; +} + +export type TimeUnitId = 's' | 'm' | 'h' | 'd' | 'w' | 'M' | 'y'; +export type TimeUnitFromNowId = 's+' | 'm+' | 'h+' | 'd+' | 'w+' | 'M+' | 'y+'; +export type TimeUnitLabel = + | 'second' + | 'minute' + | 'hour' + | 'day' + | 'week' + | 'month' + | 'year'; +export type TimeUnitLabelPlural = + | 'seconds' + | 'minutes' + | 'hours' + | 'days' + | 'weeks' + | 'months' + | 'years'; +export type AbsoluteDateMode = 'absolute'; +export type RelativeDateMode = 'relative'; +export type NowDateMode = 'now'; +export type DateMode = AbsoluteDateMode | RelativeDateMode | NowDateMode; + +/** + * String as either datemath (e.g.: now, now-15m, now-15m/m) or + * absolute date in the format 'YYYY-MM-DDTHH:mm:ss.SSSZ' + */ +export type ShortDate = NowDateMode | string; + +export type Milliseconds = number; + +export interface RelativeParts { + count: number; + round: boolean; + roundUnit?: TimeUnitId; + unit: string; +} + +export interface RelativeOption { + text: string; + value: TimeUnitId | TimeUnitFromNowId; +} + +export type OnRefreshChangeProps = { + isPaused: boolean; + refreshInterval: number; +}; + +export type ApplyRefreshInterval = (args: OnRefreshChangeProps) => void; + +export interface QuickSelect { + timeTense: string; + timeValue: number; + timeUnits: TimeUnitId; +} + +interface ApplyTimeArgs extends DurationRange { + keepPopoverOpen?: boolean; + quickSelect?: QuickSelect; +} +export type ApplyTime = (args: ApplyTimeArgs) => void; + +export interface QuickSelectPanel { + title: string; + content: ReactElement; +} diff --git a/src/components/index.d.ts b/src/components/index.d.ts index 3f9059aff10..03b97167aa4 100644 --- a/src/components/index.d.ts +++ b/src/components/index.d.ts @@ -1,7 +1,6 @@ -/* eslint-disable @typescript-eslint/triple-slash-reference */ -/// - declare module '@elastic/eui' { // @ts-ignore export * from '@elastic/eui/src/components/common'; // eslint-disable-line import/no-unresolved + // @ts-ignore + export * from '@elastic/eui/src/components/date_picker/react-datepicker'; // eslint-disable-line import/no-unresolved }