Skip to content

Commit

Permalink
[EuiSuperDatePicker] Allow passing canRoundRelativeUnits={false}, w…
Browse files Browse the repository at this point in the history
…hich turns off relative unit rounding (#7502)
  • Loading branch information
cee-chen authored Feb 5, 2024
1 parent c6db605 commit 203c6c9
Show file tree
Hide file tree
Showing 8 changed files with 178 additions and 11 deletions.
1 change: 1 addition & 0 deletions changelogs/upcoming/7502.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Updated `EuiSuperDatePicker` with a new `canRoundRelativeUnits` prop, which defaults to true (current behavior). To preserve displaying the unit that users select for relative time, set this to false.
7 changes: 7 additions & 0 deletions src-docs/src/views/super_date_picker/playground.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ export const superDatePickerConfig = () => {
value: true,
};

propsToUse.canRoundRelativeUnits = {
...propsToUse.canRoundRelativeUnits,
type: PropTypes.Boolean,
defaultValue: true,
value: true,
};

propsToUse.locale = {
...propsToUse.locale,
type: PropTypes.String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export interface EuiDatePopoverButtonProps {
onPopoverClose: EuiPopoverProps['closePopover'];
onPopoverToggle: MouseEventHandler<HTMLButtonElement>;
position: 'start' | 'end';
canRoundRelativeUnits?: boolean;
roundUp?: boolean;
timeFormat: string;
value: string;
Expand All @@ -56,6 +57,7 @@ export const EuiDatePopoverButton: FunctionComponent<
needsUpdating,
value,
buttonProps,
canRoundRelativeUnits,
roundUp,
onChange,
locale,
Expand All @@ -82,12 +84,11 @@ export const EuiDatePopoverButton: FunctionComponent<
},
]);

const formattedValue = useFormatTimeString(
value,
dateFormat,
const formattedValue = useFormatTimeString(value, dateFormat, {
roundUp,
locale
);
locale,
canRoundRelativeUnits,
});
let title = formattedValue;

const invalidTitle = useEuiI18n(
Expand Down Expand Up @@ -133,6 +134,7 @@ export const EuiDatePopoverButton: FunctionComponent<
<EuiDatePopoverContent
value={value}
roundUp={roundUp}
canRoundRelativeUnits={canRoundRelativeUnits}
onChange={onChange}
dateFormat={dateFormat}
timeFormat={timeFormat}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { LocaleSpecifier } from 'moment'; // eslint-disable-line import/named
export interface EuiDatePopoverContentProps {
value: string;
onChange: (date: string) => void;
canRoundRelativeUnits?: boolean;
roundUp?: boolean;
dateFormat: string;
timeFormat: string;
Expand All @@ -41,6 +42,7 @@ export const EuiDatePopoverContent: FunctionComponent<
EuiDatePopoverContentProps
> = ({
value,
canRoundRelativeUnits = true,
roundUp = false,
onChange,
dateFormat,
Expand Down Expand Up @@ -108,7 +110,9 @@ export const EuiDatePopoverContent: FunctionComponent<
<EuiRelativeTab
dateFormat={dateFormat}
locale={locale}
value={toAbsoluteString(value, roundUp)}
value={
canRoundRelativeUnits ? toAbsoluteString(value, roundUp) : value
}
onChange={onChange}
roundUp={roundUp}
position={position}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
usePrettyDuration,
PrettyDuration,
showPrettyDuration,
useFormatTimeString,
} from './pretty_duration';

const dateFormat = 'MMMM Do YYYY, HH:mm:ss.SSS';
Expand Down Expand Up @@ -123,3 +124,53 @@ describe('showPrettyDuration', () => {
).toBe(false);
});
});

describe('useFormatTimeString', () => {
it('it takes a time string and formats it into a humanized date', () => {
expect(
renderHook(() => useFormatTimeString('now-3s', dateFormat)).result.current
).toEqual('~ a few seconds ago');
expect(
renderHook(() => useFormatTimeString('now+1m', dateFormat)).result.current
).toEqual('~ in a minute');
expect(
renderHook(() => useFormatTimeString('now+100w', dateFormat)).result
.current
).toEqual('~ in 2 years');
});

it("always parses the 'now' string as-is", () => {
expect(
renderHook(() => useFormatTimeString('now', dateFormat)).result.current
).toEqual('now');
});

describe('options', () => {
test('locale', () => {
expect(
renderHook(() =>
useFormatTimeString('now+15m', dateFormat, { locale: 'ja' })
).result.current
).toBe('~ 15分後');
});

describe('canRoundRelativeUnits', () => {
const option = { canRoundRelativeUnits: false };

it("allows skipping moment.fromNow()'s default rounding", () => {
expect(
renderHook(() => useFormatTimeString('now-3s', dateFormat, option))
.result.current
).toEqual('3 seconds ago');
expect(
renderHook(() => useFormatTimeString('now+1m', dateFormat, option))
.result.current
).toEqual('in a minute');
expect(
renderHook(() => useFormatTimeString('now+100w', dateFormat, option))
.result.current
).toEqual('in 100 weeks');
});
});
});
});
39 changes: 34 additions & 5 deletions src/components/date_picker/super_date_picker/pretty_duration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import React from 'react';
import dateMath from '@elastic/datemath';
import moment, { LocaleSpecifier } from 'moment'; // eslint-disable-line import/named
import moment, { LocaleSpecifier, RelativeTimeKey } from 'moment'; // eslint-disable-line import/named
import { useEuiI18n } from '../../i18n';
import { getDateMode, DATE_MODES } from './date_modes';
import { parseRelativeParts } from './relative_utils';
Expand Down Expand Up @@ -146,9 +146,18 @@ const ISO_FORMAT = 'YYYY-MM-DDTHH:mm:ss.SSSZ';
export const useFormatTimeString = (
timeString: string,
dateFormat: string,
roundUp = false,
locale: LocaleSpecifier = 'en'
options?: {
locale?: LocaleSpecifier;
roundUp?: boolean;
canRoundRelativeUnits?: boolean;
}
): string => {
const {
locale = 'en',
roundUp = false,
canRoundRelativeUnits = true,
} = options || {};

// i18n'd strings
const nowDisplay = useEuiI18n('euiPrettyDuration.now', 'now');
const invalidDateDisplay = useEuiI18n(
Expand All @@ -171,7 +180,27 @@ export const useFormatTimeString = (
}

if (moment.isMoment(tryParse)) {
return `~ ${tryParse.locale(locale).fromNow()}`;
if (canRoundRelativeUnits) {
return `~ ${tryParse.locale(locale).fromNow()}`;
} else {
// To force a specific unit to be used, we need to skip moment.fromNow()
// entirely and write our own custom moment formatted output.
const { count, unit: _unit } = parseRelativeParts(timeString);
const isFuture = _unit.endsWith('+');
const unit = isFuture ? _unit.slice(0, -1) : _unit; // We want just the unit letter without the trailing +

// @see https://momentjs.com/docs/#/customization/relative-time/
const relativeUnitKey = (
count === 1 ? unit : unit + unit
) as RelativeTimeKey;

// @see https://momentjs.com/docs/#/i18n/locale-data/
return moment.localeData().pastFuture(
isFuture ? count : count * -1,
moment.localeData().relativeTime(count, false, relativeUnitKey, false)
// Booleans don't seem to actually matter for output, .pastFuture() handles that
);
}
}

return timeString;
Expand Down Expand Up @@ -246,7 +275,7 @@ export const usePrettyDuration = ({
* If it's none of the above, display basic fallback copy
*/
const displayFrom = useFormatTimeString(timeFrom, dateFormat);
const displayTo = useFormatTimeString(timeTo, dateFormat, true);
const displayTo = useFormatTimeString(timeTo, dateFormat, { roundUp: true });
const fallbackDuration = useEuiI18n(
'euiPrettyDuration.fallbackDuration',
'{displayFrom} to {displayTo}',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ const findInternalInstance = (
};

describe('EuiSuperDatePicker', () => {
// RTL doesn't automatically clean up portals/datepicker popovers between tests
afterEach(() => {
const portals = document.querySelectorAll('[data-euiportal]');
portals.forEach((portal) => portal.parentNode?.removeChild(portal));
});

shouldRenderCustomStyles(<EuiSuperDatePicker onTimeChange={noop} />, {
skip: { style: true },
});
Expand Down Expand Up @@ -313,5 +319,59 @@ describe('EuiSuperDatePicker', () => {
expect(container.firstChild).toMatchSnapshot();
});
});

describe('canRoundRelativeUnits', () => {
const props = {
onTimeChange: noop,
start: 'now-300m',
end: 'now',
};

it('defaults to true, which will round relative units up to the next largest unit', () => {
const { getByTestSubject } = render(
<EuiSuperDatePicker {...props} canRoundRelativeUnits={true} />
);
fireEvent.click(getByTestSubject('superDatePickerShowDatesButton'));

const startButton = getByTestSubject(
'superDatePickerstartDatePopoverButton'
);
expect(startButton).toHaveTextContent('~ 5 hours ago');

const countInput = getByTestSubject(
'superDatePickerRelativeDateInputNumber'
);
expect(countInput).toHaveValue(5);

const unitSelect = getByTestSubject(
'superDatePickerRelativeDateInputUnitSelector'
);
expect(unitSelect).toHaveValue('h');

fireEvent.change(countInput, { target: { value: 300 } });
fireEvent.change(unitSelect, { target: { value: 'd' } });
expect(startButton).toHaveTextContent('~ 10 months ago');
});

it('when false, allows preserving the unit set in the start/end time timestamp', () => {
const { getByTestSubject } = render(
<EuiSuperDatePicker {...props} canRoundRelativeUnits={false} />
);
fireEvent.click(getByTestSubject('superDatePickerShowDatesButton'));

const startButton = getByTestSubject(
'superDatePickerstartDatePopoverButton'
);
expect(startButton).toHaveTextContent('300 minutes ago');

const unitSelect = getByTestSubject(
'superDatePickerRelativeDateInputUnitSelector'
);
expect(unitSelect).toHaveValue('m');

fireEvent.change(unitSelect, { target: { value: 'd' } });
expect(startButton).toHaveTextContent('300 days ago');
});
});
});
});
13 changes: 13 additions & 0 deletions src/components/date_picker/super_date_picker/super_date_picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,15 @@ export type EuiSuperDatePickerProps = CommonProps & {
* Props passed to the update button #EuiSuperUpdateButtonProps
*/
updateButtonProps?: EuiSuperUpdateButtonProps;

/**
* By default, relative units will be rounded up to next largest unit of time
* (for example, 90 minutes will become ~ 2 hours).
*
* If you do not want this behavior and instead wish to keep the exact units
* input by the user, set this flag to `false`.
*/
canRoundRelativeUnits?: boolean;
};

type EuiSuperDatePickerInternalProps = EuiSuperDatePickerProps & {
Expand Down Expand Up @@ -241,6 +250,7 @@ export class EuiSuperDatePickerInternal extends Component<
recentlyUsedRanges: [],
refreshInterval: 1000,
showUpdateButton: true,
canRoundRelativeUnits: true,
start: 'now-15m',
timeFormat: 'HH:mm',
width: 'restricted',
Expand Down Expand Up @@ -468,6 +478,7 @@ export class EuiSuperDatePickerInternal extends Component<
isQuickSelectOnly,
showUpdateButton,
commonlyUsedRanges,
canRoundRelativeUnits,
timeOptions,
dateFormat,
refreshInterval,
Expand Down Expand Up @@ -562,6 +573,7 @@ export class EuiSuperDatePickerInternal extends Component<
utcOffset={utcOffset}
timeFormat={timeFormat}
locale={locale || contextLocale}
canRoundRelativeUnits={canRoundRelativeUnits}
isOpen={this.state.isStartDatePopoverOpen}
onPopoverToggle={this.onStartDatePopoverToggle}
onPopoverClose={this.onStartDatePopoverClose}
Expand All @@ -582,6 +594,7 @@ export class EuiSuperDatePickerInternal extends Component<
utcOffset={utcOffset}
timeFormat={timeFormat}
locale={locale || contextLocale}
canRoundRelativeUnits={canRoundRelativeUnits}
roundUp
isOpen={this.state.isEndDatePopoverOpen}
onPopoverToggle={this.onEndDatePopoverToggle}
Expand Down

0 comments on commit 203c6c9

Please sign in to comment.