diff --git a/src/components/date_picker/super_date_picker/date_popover/absolute_tab.test.tsx b/src/components/date_picker/super_date_picker/date_popover/absolute_tab.test.tsx new file mode 100644 index 00000000000..9b061b15702 --- /dev/null +++ b/src/components/date_picker/super_date_picker/date_popover/absolute_tab.test.tsx @@ -0,0 +1,107 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { act, fireEvent } from '@testing-library/react'; +import { render } from '../../../../test/rtl'; + +import { EuiAbsoluteTab } from './absolute_tab'; + +// Mock EuiDatePicker - 3rd party datepicker lib causes render issues +jest.mock('../../date_picker', () => ({ + EuiDatePicker: () => 'EuiDatePicker', +})); + +describe('EuiAbsoluteTab', () => { + const props = { + dateFormat: 'MMM D, YYYY @ HH:mm:ss.SSS', + timeFormat: 'HH:mm', + value: '', + onChange: () => {}, + roundUp: false, + position: 'start' as const, + labelPrefix: 'Start date', + }; + + describe('user input', () => { + beforeAll(() => jest.useFakeTimers()); + afterAll(() => jest.useRealTimers()); + + const changeInput = (input: HTMLElement, value: string) => { + fireEvent.change(input, { target: { value } }); + act(() => { + jest.advanceTimersByTime(1000); // Debounce timer + }); + }; + + it('parses the passed `dateFormat` prop', () => { + const { getByTestSubject } = render( + + ); + const input = getByTestSubject('superDatePickerAbsoluteDateInput'); + + changeInput(input, 'Jan 31st 01'); + expect(input).not.toBeInvalid(); + expect(input).toHaveValue('Jan 31st 01'); + }); + + describe('allows several other common date formats, and autoformats them to the `dateFormat` prop', () => { + const assertOutput = (input: HTMLInputElement) => { + // Exclude hours from assertion, because moment uses local machine timezone + expect(input.value).toContain('Jan 1, 1970'); + }; + + test('ISO 8601', () => { + const { getByTestSubject } = render(); + const input = getByTestSubject('superDatePickerAbsoluteDateInput'); + + changeInput(input, '1970-01-01T12:00:00+00:00'); + expect(input).not.toBeInvalid(); + assertOutput(input as HTMLInputElement); + }); + + test('RFC 2822', () => { + const { getByTestSubject } = render(); + const input = getByTestSubject('superDatePickerAbsoluteDateInput'); + + changeInput(input, 'Thu, 1 Jan 1970 12:00:00 +0000'); + expect(input).not.toBeInvalid(); + assertOutput(input as HTMLInputElement); + }); + + test('unix timestamp', () => { + const { getByTestSubject } = render(); + const input = getByTestSubject('superDatePickerAbsoluteDateInput'); + + changeInput(input, Date.now().toString()); + expect(input).not.toBeInvalid(); + + changeInput(input, '43200'); + expect(input).not.toBeInvalid(); + assertOutput(input as HTMLInputElement); + }); + }); + + it('flags all other date formats as invalid', () => { + const { getByTestSubject } = render(); + const input = getByTestSubject('superDatePickerAbsoluteDateInput'); + + changeInput(input, '01-01-1970'); + expect(input).toHaveValue('01-01-1970'); + expect(input).toBeInvalid(); + + changeInput(input, 'asdfasdf'); + expect(input).toHaveValue('asdfasdf'); + expect(input).toBeInvalid(); + + changeInput(input, ''); + expect(input).toHaveValue(''); + expect(input).toBeInvalid(); + }); + }); +}); diff --git a/src/components/date_picker/super_date_picker/date_popover/absolute_tab.tsx b/src/components/date_picker/super_date_picker/date_popover/absolute_tab.tsx index be56c08e08c..9ef766f77de 100644 --- a/src/components/date_picker/super_date_picker/date_popover/absolute_tab.tsx +++ b/src/components/date_picker/super_date_picker/date_popover/absolute_tab.tsx @@ -6,17 +6,27 @@ * Side Public License, v 1. */ -import React, { Component, ChangeEventHandler } from 'react'; +import React, { Component, ChangeEvent } from 'react'; import moment, { Moment, LocaleSpecifier } from 'moment'; // eslint-disable-line import/named import dateMath from '@elastic/datemath'; -import { EuiDatePicker, EuiDatePickerProps } from '../../date_picker'; import { EuiFormRow, EuiFieldText, EuiFormLabel } from '../../../form'; +import { EuiCode } from '../../../code'; import { EuiI18n } from '../../../i18n'; + +import { EuiDatePicker, EuiDatePickerProps } from '../../date_picker'; import { EuiDatePopoverContentProps } from './date_popover_content'; +// Allow users to paste in and have the datepicker parse multiple common date formats, +// in addition to the configured displayed `dateFormat` prop +const ALLOWED_USER_DATE_FORMATS = [ + moment.ISO_8601, + moment.RFC_2822, + 'X', // Unix timestamp in seconds +]; + export interface EuiAbsoluteTabProps { dateFormat: string; timeFormat: string; @@ -59,12 +69,12 @@ export class EuiAbsoluteTab extends Component< }; } - handleChange: EuiDatePickerProps['onChange'] = (date, event) => { + handleChange: EuiDatePickerProps['onChange'] = (date) => { const { onChange } = this.props; if (date === null) { return; } - onChange(date.toISOString(), event); + onChange(date.toISOString()); const valueAsMoment = moment(date); this.setState({ @@ -74,22 +84,50 @@ export class EuiAbsoluteTab extends Component< }); }; - handleTextChange: ChangeEventHandler = (event) => { - const { onChange } = this.props; - const valueAsMoment = moment( - event.target.value, - this.props.dateFormat, - true - ); - const dateIsValid = valueAsMoment.isValid(); + debouncedTypeTimeout: ReturnType | undefined; + + handleTextChange = (event: ChangeEvent) => { + this.setState({ textInputValue: event.target.value }); + + // Add a debouncer that gives the user some time to finish typing + // before attempting to parse the text as a timestamp. Otherwise, + // typing a single digit gets parsed as a unix timestamp 😬 + clearTimeout(this.debouncedTypeTimeout); + this.debouncedTypeTimeout = setTimeout(this.parseUserDateInput, 1000); // 1 second debounce + }; + + parseUserDateInput = () => { + const { onChange, dateFormat } = this.props; + const { textInputValue } = this.state; + + const invalidDateState = { + isTextInvalid: true, + valueAsMoment: null, + }; + if (!textInputValue) { + return this.setState(invalidDateState); + } + + // Attempt to parse with passed `dateFormat` + let valueAsMoment = moment(textInputValue, dateFormat, true); + let dateIsValid = valueAsMoment.isValid(); + + // If not valid, try a few other other standardized formats + if (!dateIsValid) { + valueAsMoment = moment(textInputValue, ALLOWED_USER_DATE_FORMATS, true); + dateIsValid = valueAsMoment.isValid(); + } + if (dateIsValid) { - onChange(valueAsMoment.toISOString(), event); + onChange(valueAsMoment.toISOString()); + this.setState({ + textInputValue: valueAsMoment.format(this.props.dateFormat), + isTextInvalid: false, + valueAsMoment: valueAsMoment, + }); + } else { + this.setState(invalidDateState); } - this.setState({ - textInputValue: event.target.value, - isTextInvalid: !dateIsValid, - valueAsMoment: dateIsValid ? valueAsMoment : null, - }); }; render() { @@ -98,7 +136,7 @@ export class EuiAbsoluteTab extends Component< const { valueAsMoment, isTextInvalid, textInputValue } = this.state; return ( -
+ <> {dateFormat} }} > {(dateFormatError: string) => ( {labelPrefix}} /> )} -
+ ); } } diff --git a/src/components/date_picker/super_date_picker/date_popover/date_popover_button.tsx b/src/components/date_picker/super_date_picker/date_popover/date_popover_button.tsx index 5122482e826..536e1e178fb 100644 --- a/src/components/date_picker/super_date_picker/date_popover/date_popover_button.tsx +++ b/src/components/date_picker/super_date_picker/date_popover/date_popover_button.tsx @@ -34,7 +34,7 @@ export interface EuiDatePopoverButtonProps { isOpen: boolean; needsUpdating?: boolean; locale?: LocaleSpecifier; - onChange: NonNullable; + onChange: EuiDatePopoverContentProps['onChange']; onPopoverClose: EuiPopoverProps['closePopover']; onPopoverToggle: MouseEventHandler; position: 'start' | 'end'; diff --git a/src/components/date_picker/super_date_picker/date_popover/date_popover_content.tsx b/src/components/date_picker/super_date_picker/date_popover/date_popover_content.tsx index 98dfb45f5de..6a1a2a60f91 100644 --- a/src/components/date_picker/super_date_picker/date_popover/date_popover_content.tsx +++ b/src/components/date_picker/super_date_picker/date_popover/date_popover_content.tsx @@ -27,7 +27,7 @@ import { LocaleSpecifier } from 'moment'; // eslint-disable-line import/named export interface EuiDatePopoverContentProps { value: string; - onChange(date: string | null, event?: React.SyntheticEvent): void; + onChange: (date: string) => void; roundUp?: boolean; dateFormat: string; timeFormat: string; diff --git a/src/components/date_picker/super_date_picker/super_date_picker.tsx b/src/components/date_picker/super_date_picker/super_date_picker.tsx index cf5ce985b0a..6e75c2e6396 100644 --- a/src/components/date_picker/super_date_picker/super_date_picker.tsx +++ b/src/components/date_picker/super_date_picker/super_date_picker.tsx @@ -75,6 +75,7 @@ export type EuiSuperDatePickerProps = CommonProps & { /** * Specifies the formatted used when displaying dates and/or datetimes + * @default 'MMM D, YYYY @ HH:mm:ss.SSS' */ dateFormat?: string; @@ -92,6 +93,9 @@ export type EuiSuperDatePickerProps = CommonProps & { isDisabled?: boolean | { display: ReactNode }; isLoading?: boolean; + /** + * @default true + */ isPaused?: boolean; /** @@ -99,6 +103,7 @@ export type EuiSuperDatePickerProps = CommonProps & { * - `auto`: fits width to internal content / time string. * - `restricted`: static width that fits the longest possible time string. * - `full`: expands to 100% of the container. + * @default 'restricted' */ width?: 'restricted' | 'full' | 'auto'; @@ -139,20 +144,29 @@ export type EuiSuperDatePickerProps = CommonProps & { /** * Refresh interval in milliseconds + * @default 1000 */ refreshInterval?: Milliseconds; + /** + * @default 'now-15m' + */ start?: ShortDate; + /** + * @default 'now' + */ end?: ShortDate; /** * Specifies the formatted used when displaying times + * @default 'HH:mm' */ timeFormat?: string; utcOffset?: number; /** * Set showUpdateButton to false to immediately invoke onTimeChange for all start and end changes. + * @default true */ showUpdateButton?: boolean | 'iconOnly'; diff --git a/upcoming_changelogs/7331.md b/upcoming_changelogs/7331.md new file mode 100644 index 00000000000..b7aff7b5697 --- /dev/null +++ b/upcoming_changelogs/7331.md @@ -0,0 +1 @@ +- For greater flexibility, `EuiSuperDatePicker` now allows users to paste ISO 8601, RFC 2822, and Unix timestamps in the `Absolute` tab input, in addition to timestamps in the `dateFormat` prop