Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[EuiSuperDatePicker] Allow the Absolute tab to accept standardized ISO 8601, RFC 2822, and Unix timestamps #7331

Merged
merged 9 commits into from
Nov 2, 2023
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* 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 { 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', () => {
it('parses the passed `dateFormat` prop', () => {
const { getByTestSubject } = render(
<EuiAbsoluteTab {...props} dateFormat="MMM Do YY" />
);
const input = getByTestSubject('superDatePickerAbsoluteDateInput');

fireEvent.change(input, {
target: { value: '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(<EuiAbsoluteTab {...props} />);
const input = getByTestSubject('superDatePickerAbsoluteDateInput');

fireEvent.change(input, {
target: { value: '1970-01-01T12:00:00+00:00' },
});
expect(input).not.toBeInvalid();
assertOutput(input as HTMLInputElement);
});

test('RFC 2822', () => {
const { getByTestSubject } = render(<EuiAbsoluteTab {...props} />);
const input = getByTestSubject('superDatePickerAbsoluteDateInput');

fireEvent.change(input, {
target: { value: 'Thu, 1 Jan 1970 12:00:00 +0000' },
});
expect(input).not.toBeInvalid();
assertOutput(input as HTMLInputElement);
});

test('unix timestamp', () => {
const { getByTestSubject } = render(<EuiAbsoluteTab {...props} />);
const input = getByTestSubject('superDatePickerAbsoluteDateInput');

fireEvent.change(input, { target: { value: Date.now().toString() } });
expect(input).not.toBeInvalid();

fireEvent.change(input, { target: { value: '43200' } });
expect(input).not.toBeInvalid();
assertOutput(input as HTMLInputElement);
});
});

it('flags all other date formats as invalid', () => {
const { getByTestSubject } = render(<EuiAbsoluteTab {...props} />);
const input = getByTestSubject('superDatePickerAbsoluteDateInput');

fireEvent.change(input, { target: { value: '01-01-1970' } });
expect(input).toHaveValue('01-01-1970');
expect(input).toBeInvalid();

fireEvent.change(input, { target: { value: 'asdfasdf' } });
expect(input).toHaveValue('asdfasdf');
expect(input).toBeInvalid();

fireEvent.change(input, { target: { value: '' } });
expect(input).toHaveValue('');
expect(input).toBeInvalid();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,21 @@ import moment, { Moment, LocaleSpecifier } from 'moment'; // eslint-disable-line

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we also include 'x' here to support integer millisecond-precision timestamps? I'd say they're as common as floating point timestamps, and supporting them both when using moment is really easy

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried adding both, and unfortunately Moment doesn't seem to be able to handle it :( It errors on one or the other depending on the order in which you put them in the array.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh no, that's unfortunate :( Let's leave it as is then!

];

export interface EuiAbsoluteTabProps {
dateFormat: string;
timeFormat: string;
Expand Down Expand Up @@ -75,21 +85,38 @@ export class EuiAbsoluteTab extends Component<
};

handleTextChange: ChangeEventHandler<HTMLInputElement> = (event) => {
const { onChange } = this.props;
const valueAsMoment = moment(
event.target.value,
this.props.dateFormat,
true
);
const dateIsValid = valueAsMoment.isValid();
const { onChange, dateFormat } = this.props;
const userInput = event.target.value;

const invalidDateState = {
textInputValue: userInput,
isTextInvalid: true,
valueAsMoment: null,
};
if (!userInput) {
return this.setState(invalidDateState);
}

// Attempt to parse with passed `dateFormat`
let valueAsMoment = moment(userInput, dateFormat, true);
let dateIsValid = valueAsMoment.isValid();

// If not valid, try a few other other standardized formats
if (!dateIsValid) {
valueAsMoment = moment(userInput, ALLOWED_USER_DATE_FORMATS, true);
dateIsValid = valueAsMoment.isValid();
}

if (dateIsValid) {
onChange(valueAsMoment.toISOString(), event);
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() {
Expand All @@ -98,7 +125,7 @@ export class EuiAbsoluteTab extends Component<
const { valueAsMoment, isTextInvalid, textInputValue } = this.state;

return (
<div>
<>
<EuiDatePicker
inline
showTimeSelect
Expand All @@ -112,8 +139,8 @@ export class EuiAbsoluteTab extends Component<
/>
<EuiI18n
token="euiAbsoluteTab.dateFormatError"
default="Expected format: {dateFormat}"
values={{ dateFormat }}
default="Allowed formats: {dateFormat}, ISO 8601, RFC 2822, or Unix timestamp"
values={{ dateFormat: <EuiCode>{dateFormat}</EuiCode> }}
>
{(dateFormatError: string) => (
<EuiFormRow
Expand All @@ -126,13 +153,13 @@ export class EuiAbsoluteTab extends Component<
isInvalid={isTextInvalid}
value={textInputValue}
onChange={this.handleTextChange}
data-test-subj={'superDatePickerAbsoluteDateInput'}
data-test-subj="superDatePickerAbsoluteDateInput"
prepend={<EuiFormLabel>{labelPrefix}</EuiFormLabel>}
/>
</EuiFormRow>
)}
</EuiI18n>
</div>
</>
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -92,13 +93,17 @@ export type EuiSuperDatePickerProps = CommonProps & {
isDisabled?: boolean | { display: ReactNode };

isLoading?: boolean;
/**
* @default true
*/
isPaused?: boolean;

/**
* Sets the overall width by adding sensible min and max widths.
* - `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';

Expand Down Expand Up @@ -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';

Expand Down
1 change: 1 addition & 0 deletions upcoming_changelogs/7331.md
Original file line number Diff line number Diff line change
@@ -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
Loading