diff --git a/src-docs/src/views/pretty_duration/pretty_duration.js b/src-docs/src/views/pretty_duration/pretty_duration.js index 735b2c49999b..1c61120313cf 100644 --- a/src-docs/src/views/pretty_duration/pretty_duration.js +++ b/src-docs/src/views/pretty_duration/pretty_duration.js @@ -4,7 +4,7 @@ import { EuiSpacer, EuiCodeBlock, EuiText, - prettyDuration, + usePrettyDuration, } from '../../../../src/components'; const examples = [ @@ -50,6 +50,12 @@ const examples = [ ], dateFormat: 'MMMM Do YYYY, HH:mm:ss.SSS', }, + { + // Example that will use a default common quick range label + start: 'now/w', + end: 'now', + dateFormat: 'MMMM Do YYYY, HH:mm:ss.SSS', + }, ]; export default function prettyDurationExample() { @@ -57,16 +63,35 @@ export default function prettyDurationExample() { {examples.map(({ start, end, quickRanges, dateFormat }, idx) => (
- - prettyDuration('{start}', '{end}',{' '} - {JSON.stringify(quickRanges)}, ' - {dateFormat}') - + {` + +usePrettyDuration({ + timeFrom: '${start}', + timeTo: '${end}', + dateFormat: '${dateFormat}', + quickRanges: ${JSON.stringify(quickRanges)}, +})`} -

{prettyDuration(start, end, quickRanges, dateFormat)}

+

+ {usePrettyDuration({ + timeFrom: start, + timeTo: end, + quickRanges, + dateFormat, + })} +

diff --git a/src-docs/src/views/pretty_duration/pretty_duration_example.js b/src-docs/src/views/pretty_duration/pretty_duration_example.js index d97a32b4a912..e4604553c3b9 100644 --- a/src-docs/src/views/pretty_duration/pretty_duration_example.js +++ b/src-docs/src/views/pretty_duration/pretty_duration_example.js @@ -6,10 +6,10 @@ import { EuiAccordion, EuiCode, EuiCodeBlock, - EuiSpacer, - commonDurationRanges, } from '../../../../src/components'; +import { RenderI18nTimeOptions } from '../../../../src/components/date_picker/super_date_picker/time_options'; + import PrettyDuration from './pretty_duration'; const prettyDurationSource = require('!!raw-loader!./pretty_duration'); @@ -26,37 +26,50 @@ export const PrettyDurationExample = { text: (

- Use prettyDuration to convert a start and end - date string to a human-friendly format. + Use the {''} component (for JSX + display) or usePrettyDuration() hook (for + attribute strings) to convert a start and end date string to a + human-friendly format. Both utilities take the following props:

-

- Start and end values for the duration are passed as the first and - second arguments, respectively. These can be timestamps ( - 2018-01-17T18:57:57.149Z) or relative times ( - now-15m). -

+
    +
  • +

    + timeFrom and timeTo accept + start and end date values for the duration. These can be + timestamps (2018-01-17T18:57:57.149Z) or + relative times (now-15m). +

    +
  • -

    - An array of quick range values is passed as the third argument. - These are used to pretty format custom ranges. EUI exports - commonDurationRanges which can be passed here. -

    +
  • +

    + dateFormat specifies the output date/time + format. +

    +
  • - - - {JSON.stringify(commonDurationRanges, null, 2)} - - - - - -

    - The output date/time format is specified by the fourth argument. -

    +
  • +

    + quickRanges optionally accepts an array of + quick range values that are used to pretty format custom ranges. + If no custom quick ranges are passed, EUI will default to a set + of common duration ranges defined below. +

    + + + + {({ commonDurationRanges }) => + JSON.stringify(commonDurationRanges, null, 2) + } + + + +
  • +
), demo: , diff --git a/src-docs/src/views/super_date_picker/super_date_picker_custom_quick_select.tsx b/src-docs/src/views/super_date_picker/super_date_picker_custom_quick_select.tsx index 7310922282ea..39569ff92543 100644 --- a/src-docs/src/views/super_date_picker/super_date_picker_custom_quick_select.tsx +++ b/src-docs/src/views/super_date_picker/super_date_picker_custom_quick_select.tsx @@ -43,7 +43,7 @@ export default () => { }; const [recentlyUsedRanges, setRecentlyUsedRanges] = useState< - EuiSuperDatePickerProps['recentlyUsedRanges'] + NonNullable >([]); const [isLoading, setIsLoading] = useState(false); const [start, setStart] = useState('now-30m'); diff --git a/src-docs/src/views/super_date_picker/super_date_picker_pattern.tsx b/src-docs/src/views/super_date_picker/super_date_picker_pattern.tsx index d4c7dfbdc979..c9eb92002595 100644 --- a/src-docs/src/views/super_date_picker/super_date_picker_pattern.tsx +++ b/src-docs/src/views/super_date_picker/super_date_picker_pattern.tsx @@ -23,7 +23,7 @@ const sampleItems = [ export default () => { const [recentlyUsedRanges, setRecentlyUsedRanges] = useState< - EuiSuperDatePickerProps['recentlyUsedRanges'] + NonNullable >([]); const [isLoading, setIsLoading] = useState(false); const [refreshInterval, setRefreshInterval] = useState(1000); diff --git a/src/components/date_picker/auto_refresh/auto_refresh.tsx b/src/components/date_picker/auto_refresh/auto_refresh.tsx index 7602ddb988eb..c9963f4cf135 100644 --- a/src/components/date_picker/auto_refresh/auto_refresh.tsx +++ b/src/components/date_picker/auto_refresh/auto_refresh.tsx @@ -16,7 +16,7 @@ import { import { EuiInputPopover, EuiPopover } from '../../popover'; import { useEuiI18n } from '../../i18n'; -import { prettyInterval } from '../super_date_picker/pretty_interval'; +import { usePrettyInterval } from '../super_date_picker/pretty_interval'; import { EuiRefreshInterval, EuiRefreshIntervalProps, @@ -75,7 +75,7 @@ export const EuiAutoRefresh: FunctionComponent = ({ } readOnly={readOnly} disabled={isDisabled} - value={prettyInterval(Boolean(isPaused), refreshInterval)} + value={usePrettyInterval(Boolean(isPaused), refreshInterval)} {...rest} /> } @@ -124,7 +124,7 @@ export const EuiAutoRefreshButton: FunctionComponent const autoRefeshLabelOn = useEuiI18n( 'euiAutoRefresh.buttonLabelOn', 'Auto refresh is on and set to {prettyInterval}', - { prettyInterval: prettyInterval(Boolean(isPaused), refreshInterval) } + { prettyInterval: usePrettyInterval(Boolean(isPaused), refreshInterval) } ); return ( @@ -140,7 +140,7 @@ export const EuiAutoRefreshButton: FunctionComponent isDisabled={isDisabled} {...rest} > - {prettyInterval(Boolean(isPaused), refreshInterval, shortHand)} + {usePrettyInterval(Boolean(isPaused), refreshInterval, shortHand)} } isOpen={isPopoverOpen} diff --git a/src/components/date_picker/auto_refresh/refresh_interval.tsx b/src/components/date_picker/auto_refresh/refresh_interval.tsx index d5f614e0b3ee..9330fb1d7e19 100644 --- a/src/components/date_picker/auto_refresh/refresh_interval.tsx +++ b/src/components/date_picker/auto_refresh/refresh_interval.tsx @@ -12,24 +12,15 @@ import React, { KeyboardEventHandler, } from 'react'; import { EuiI18n } from '../../i18n'; -import { keysOf } from '../../common'; import { EuiFlexGroup, EuiFlexItem } from '../../flex'; import { EuiSelect, EuiFieldNumber, EuiFormLabel, EuiSwitch } from '../../form'; import { htmlIdGenerator } from '../../../services'; import { EuiScreenReaderOnly } from '../../accessibility'; import { - Milliseconds, - TimeUnitId, - RelativeOption, - ApplyRefreshInterval, -} from '../types'; -import { timeUnits, timeUnitsPlural } from '../super_date_picker/time_units'; - -const refreshUnitsOptions: RelativeOption[] = keysOf(timeUnits) - .filter( - (timeUnit) => timeUnit === 'h' || timeUnit === 'm' || timeUnit === 's' - ) - .map((timeUnit) => ({ value: timeUnit, text: timeUnitsPlural[timeUnit] })); + RenderI18nTimeOptions, + TimeOptions, +} from '../super_date_picker/time_options'; +import { Milliseconds, TimeUnitId, ApplyRefreshInterval } from '../types'; const MILLISECONDS_IN_SECOND = 1000; const MILLISECONDS_IN_MINUTE = MILLISECONDS_IN_SECOND * 60; @@ -174,7 +165,9 @@ export class EuiRefreshInterval extends Component< }); }; - render() { + renderScreenReaderText = ( + refreshUnitsOptions: TimeOptions['refreshUnitsOptions'] + ) => { const { isPaused } = this.props; const { value, units } = this.state; @@ -202,63 +195,76 @@ export class EuiRefreshInterval extends Component< ); return ( -
- - - - - - } - /> - - - - - - - - - -

{fullDescription}

-
-
+ +

{fullDescription}

+
+ ); + }; + + render() { + const { isPaused } = this.props; + const { value, units } = this.state; + + return ( + + {({ refreshUnitsOptions }) => ( +
+ + + + + + } + /> + + + + + + + + + {this.renderScreenReaderText(refreshUnitsOptions)} +
+ )} +
); } } diff --git a/src/components/date_picker/super_date_picker/__snapshots__/super_date_picker.test.tsx.snap b/src/components/date_picker/super_date_picker/__snapshots__/super_date_picker.test.tsx.snap index 936de55269a3..942022815b0b 100644 --- a/src/components/date_picker/super_date_picker/__snapshots__/super_date_picker.test.tsx.snap +++ b/src/components/date_picker/super_date_picker/__snapshots__/super_date_picker.test.tsx.snap @@ -64,6 +64,173 @@ exports[`EuiSuperDatePicker is rendered 1`] = ` recentlyUsedRanges={Array []} refreshInterval={1000} start="now-15m" + timeOptions={ + Object { + "commonDurationRanges": Array [ + Object { + "end": "now/d", + "label": "Today", + "start": "now/d", + }, + Object { + "end": "now/w", + "label": "This week", + "start": "now/w", + }, + Object { + "end": "now/M", + "label": "This month", + "start": "now/M", + }, + Object { + "end": "now/y", + "label": "This year", + "start": "now/y", + }, + Object { + "end": "now-1d/d", + "label": "Yesterday", + "start": "now-1d/d", + }, + Object { + "end": "now", + "label": "Week to date", + "start": "now/w", + }, + Object { + "end": "now", + "label": "Month to date", + "start": "now/M", + }, + Object { + "end": "now", + "label": "Year to date", + "start": "now/y", + }, + ], + "refreshUnitsOptions": Array [ + Object { + "text": "Seconds", + "value": "s", + }, + Object { + "text": "Minutes", + "value": "m", + }, + Object { + "text": "Hours", + "value": "h", + }, + ], + "relativeOptions": Array [ + Object { + "text": "Seconds ago", + "value": "s", + }, + Object { + "text": "Minutes ago", + "value": "m", + }, + Object { + "text": "Hours ago", + "value": "h", + }, + Object { + "text": "Days ago", + "value": "d", + }, + Object { + "text": "Weeks ago", + "value": "w", + }, + Object { + "text": "Months ago", + "value": "M", + }, + Object { + "text": "Years ago", + "value": "y", + }, + Object { + "text": "Seconds from now", + "value": "s+", + }, + Object { + "text": "Minutes from now", + "value": "m+", + }, + Object { + "text": "Hours from now", + "value": "h+", + }, + Object { + "text": "Days from now", + "value": "d+", + }, + Object { + "text": "Weeks from now", + "value": "w+", + }, + Object { + "text": "Months from now", + "value": "M+", + }, + Object { + "text": "Years from now", + "value": "y+", + }, + ], + "relativeRoundingLabels": Object { + "M": "Round to the month", + "d": "Round to the day", + "h": "Round to the hour", + "m": "Round to the minute", + "s": "Round to the second", + "w": "Round to the week", + "y": "Round to the year", + }, + "timeTenseOptions": Array [ + Object { + "text": "Last", + "value": "last", + }, + Object { + "text": "Next", + "value": "next", + }, + ], + "timeUnitsOptions": Array [ + Object { + "text": "Seconds", + "value": "s", + }, + Object { + "text": "Minutes", + "value": "m", + }, + Object { + "text": "Hours", + "value": "h", + }, + Object { + "text": "Days", + "value": "d", + }, + Object { + "text": "Weeks", + "value": "w", + }, + Object { + "text": "Months", + "value": "M", + }, + Object { + "text": "Years", + "value": "y", + }, + ], + } + } /> } > @@ -80,7 +247,55 @@ exports[`EuiSuperDatePicker is rendered 1`] = ` disabled={false} onClick={[Function]} > - Last 15 minutes + @@ -174,6 +389,173 @@ exports[`EuiSuperDatePicker props accepts data-test-subj and passes to EuiFormCo recentlyUsedRanges={Array []} refreshInterval={1000} start="now-15m" + timeOptions={ + Object { + "commonDurationRanges": Array [ + Object { + "end": "now/d", + "label": "Today", + "start": "now/d", + }, + Object { + "end": "now/w", + "label": "This week", + "start": "now/w", + }, + Object { + "end": "now/M", + "label": "This month", + "start": "now/M", + }, + Object { + "end": "now/y", + "label": "This year", + "start": "now/y", + }, + Object { + "end": "now-1d/d", + "label": "Yesterday", + "start": "now-1d/d", + }, + Object { + "end": "now", + "label": "Week to date", + "start": "now/w", + }, + Object { + "end": "now", + "label": "Month to date", + "start": "now/M", + }, + Object { + "end": "now", + "label": "Year to date", + "start": "now/y", + }, + ], + "refreshUnitsOptions": Array [ + Object { + "text": "Seconds", + "value": "s", + }, + Object { + "text": "Minutes", + "value": "m", + }, + Object { + "text": "Hours", + "value": "h", + }, + ], + "relativeOptions": Array [ + Object { + "text": "Seconds ago", + "value": "s", + }, + Object { + "text": "Minutes ago", + "value": "m", + }, + Object { + "text": "Hours ago", + "value": "h", + }, + Object { + "text": "Days ago", + "value": "d", + }, + Object { + "text": "Weeks ago", + "value": "w", + }, + Object { + "text": "Months ago", + "value": "M", + }, + Object { + "text": "Years ago", + "value": "y", + }, + Object { + "text": "Seconds from now", + "value": "s+", + }, + Object { + "text": "Minutes from now", + "value": "m+", + }, + Object { + "text": "Hours from now", + "value": "h+", + }, + Object { + "text": "Days from now", + "value": "d+", + }, + Object { + "text": "Weeks from now", + "value": "w+", + }, + Object { + "text": "Months from now", + "value": "M+", + }, + Object { + "text": "Years from now", + "value": "y+", + }, + ], + "relativeRoundingLabels": Object { + "M": "Round to the month", + "d": "Round to the day", + "h": "Round to the hour", + "m": "Round to the minute", + "s": "Round to the second", + "w": "Round to the week", + "y": "Round to the year", + }, + "timeTenseOptions": Array [ + Object { + "text": "Last", + "value": "last", + }, + Object { + "text": "Next", + "value": "next", + }, + ], + "timeUnitsOptions": Array [ + Object { + "text": "Seconds", + "value": "s", + }, + Object { + "text": "Minutes", + "value": "m", + }, + Object { + "text": "Hours", + "value": "h", + }, + Object { + "text": "Days", + "value": "d", + }, + Object { + "text": "Weeks", + "value": "w", + }, + Object { + "text": "Months", + "value": "M", + }, + Object { + "text": "Years", + "value": "y", + }, + ], + } + } /> } > @@ -190,7 +572,55 @@ exports[`EuiSuperDatePicker props accepts data-test-subj and passes to EuiFormCo disabled={false} onClick={[Function]} > - Last 15 minutes + @@ -284,6 +714,173 @@ exports[`EuiSuperDatePicker props compressed is rendered 1`] = ` recentlyUsedRanges={Array []} refreshInterval={1000} start="now-15m" + timeOptions={ + Object { + "commonDurationRanges": Array [ + Object { + "end": "now/d", + "label": "Today", + "start": "now/d", + }, + Object { + "end": "now/w", + "label": "This week", + "start": "now/w", + }, + Object { + "end": "now/M", + "label": "This month", + "start": "now/M", + }, + Object { + "end": "now/y", + "label": "This year", + "start": "now/y", + }, + Object { + "end": "now-1d/d", + "label": "Yesterday", + "start": "now-1d/d", + }, + Object { + "end": "now", + "label": "Week to date", + "start": "now/w", + }, + Object { + "end": "now", + "label": "Month to date", + "start": "now/M", + }, + Object { + "end": "now", + "label": "Year to date", + "start": "now/y", + }, + ], + "refreshUnitsOptions": Array [ + Object { + "text": "Seconds", + "value": "s", + }, + Object { + "text": "Minutes", + "value": "m", + }, + Object { + "text": "Hours", + "value": "h", + }, + ], + "relativeOptions": Array [ + Object { + "text": "Seconds ago", + "value": "s", + }, + Object { + "text": "Minutes ago", + "value": "m", + }, + Object { + "text": "Hours ago", + "value": "h", + }, + Object { + "text": "Days ago", + "value": "d", + }, + Object { + "text": "Weeks ago", + "value": "w", + }, + Object { + "text": "Months ago", + "value": "M", + }, + Object { + "text": "Years ago", + "value": "y", + }, + Object { + "text": "Seconds from now", + "value": "s+", + }, + Object { + "text": "Minutes from now", + "value": "m+", + }, + Object { + "text": "Hours from now", + "value": "h+", + }, + Object { + "text": "Days from now", + "value": "d+", + }, + Object { + "text": "Weeks from now", + "value": "w+", + }, + Object { + "text": "Months from now", + "value": "M+", + }, + Object { + "text": "Years from now", + "value": "y+", + }, + ], + "relativeRoundingLabels": Object { + "M": "Round to the month", + "d": "Round to the day", + "h": "Round to the hour", + "m": "Round to the minute", + "s": "Round to the second", + "w": "Round to the week", + "y": "Round to the year", + }, + "timeTenseOptions": Array [ + Object { + "text": "Last", + "value": "last", + }, + Object { + "text": "Next", + "value": "next", + }, + ], + "timeUnitsOptions": Array [ + Object { + "text": "Seconds", + "value": "s", + }, + Object { + "text": "Minutes", + "value": "m", + }, + Object { + "text": "Hours", + "value": "h", + }, + Object { + "text": "Days", + "value": "d", + }, + Object { + "text": "Weeks", + "value": "w", + }, + Object { + "text": "Months", + "value": "M", + }, + Object { + "text": "Years", + "value": "y", + }, + ], + } + } /> } > @@ -300,7 +897,55 @@ exports[`EuiSuperDatePicker props compressed is rendered 1`] = ` disabled={false} onClick={[Function]} > - Last 15 minutes + @@ -430,6 +1075,173 @@ exports[`EuiSuperDatePicker props isQuickSelectOnly is rendered 1`] = ` recentlyUsedRanges={Array []} refreshInterval={1000} start="now-15m" + timeOptions={ + Object { + "commonDurationRanges": Array [ + Object { + "end": "now/d", + "label": "Today", + "start": "now/d", + }, + Object { + "end": "now/w", + "label": "This week", + "start": "now/w", + }, + Object { + "end": "now/M", + "label": "This month", + "start": "now/M", + }, + Object { + "end": "now/y", + "label": "This year", + "start": "now/y", + }, + Object { + "end": "now-1d/d", + "label": "Yesterday", + "start": "now-1d/d", + }, + Object { + "end": "now", + "label": "Week to date", + "start": "now/w", + }, + Object { + "end": "now", + "label": "Month to date", + "start": "now/M", + }, + Object { + "end": "now", + "label": "Year to date", + "start": "now/y", + }, + ], + "refreshUnitsOptions": Array [ + Object { + "text": "Seconds", + "value": "s", + }, + Object { + "text": "Minutes", + "value": "m", + }, + Object { + "text": "Hours", + "value": "h", + }, + ], + "relativeOptions": Array [ + Object { + "text": "Seconds ago", + "value": "s", + }, + Object { + "text": "Minutes ago", + "value": "m", + }, + Object { + "text": "Hours ago", + "value": "h", + }, + Object { + "text": "Days ago", + "value": "d", + }, + Object { + "text": "Weeks ago", + "value": "w", + }, + Object { + "text": "Months ago", + "value": "M", + }, + Object { + "text": "Years ago", + "value": "y", + }, + Object { + "text": "Seconds from now", + "value": "s+", + }, + Object { + "text": "Minutes from now", + "value": "m+", + }, + Object { + "text": "Hours from now", + "value": "h+", + }, + Object { + "text": "Days from now", + "value": "d+", + }, + Object { + "text": "Weeks from now", + "value": "w+", + }, + Object { + "text": "Months from now", + "value": "M+", + }, + Object { + "text": "Years from now", + "value": "y+", + }, + ], + "relativeRoundingLabels": Object { + "M": "Round to the month", + "d": "Round to the day", + "h": "Round to the hour", + "m": "Round to the minute", + "s": "Round to the second", + "w": "Round to the week", + "y": "Round to the year", + }, + "timeTenseOptions": Array [ + Object { + "text": "Last", + "value": "last", + }, + Object { + "text": "Next", + "value": "next", + }, + ], + "timeUnitsOptions": Array [ + Object { + "text": "Seconds", + "value": "s", + }, + Object { + "text": "Minutes", + "value": "m", + }, + Object { + "text": "Hours", + "value": "h", + }, + Object { + "text": "Days", + "value": "d", + }, + Object { + "text": "Weeks", + "value": "w", + }, + Object { + "text": "Months", + "value": "M", + }, + Object { + "text": "Years", + "value": "y", + }, + ], + } + } /> } /> @@ -522,6 +1334,173 @@ exports[`EuiSuperDatePicker props showUpdateButton can be false 1`] = ` recentlyUsedRanges={Array []} refreshInterval={1000} start="now-15m" + timeOptions={ + Object { + "commonDurationRanges": Array [ + Object { + "end": "now/d", + "label": "Today", + "start": "now/d", + }, + Object { + "end": "now/w", + "label": "This week", + "start": "now/w", + }, + Object { + "end": "now/M", + "label": "This month", + "start": "now/M", + }, + Object { + "end": "now/y", + "label": "This year", + "start": "now/y", + }, + Object { + "end": "now-1d/d", + "label": "Yesterday", + "start": "now-1d/d", + }, + Object { + "end": "now", + "label": "Week to date", + "start": "now/w", + }, + Object { + "end": "now", + "label": "Month to date", + "start": "now/M", + }, + Object { + "end": "now", + "label": "Year to date", + "start": "now/y", + }, + ], + "refreshUnitsOptions": Array [ + Object { + "text": "Seconds", + "value": "s", + }, + Object { + "text": "Minutes", + "value": "m", + }, + Object { + "text": "Hours", + "value": "h", + }, + ], + "relativeOptions": Array [ + Object { + "text": "Seconds ago", + "value": "s", + }, + Object { + "text": "Minutes ago", + "value": "m", + }, + Object { + "text": "Hours ago", + "value": "h", + }, + Object { + "text": "Days ago", + "value": "d", + }, + Object { + "text": "Weeks ago", + "value": "w", + }, + Object { + "text": "Months ago", + "value": "M", + }, + Object { + "text": "Years ago", + "value": "y", + }, + Object { + "text": "Seconds from now", + "value": "s+", + }, + Object { + "text": "Minutes from now", + "value": "m+", + }, + Object { + "text": "Hours from now", + "value": "h+", + }, + Object { + "text": "Days from now", + "value": "d+", + }, + Object { + "text": "Weeks from now", + "value": "w+", + }, + Object { + "text": "Months from now", + "value": "M+", + }, + Object { + "text": "Years from now", + "value": "y+", + }, + ], + "relativeRoundingLabels": Object { + "M": "Round to the month", + "d": "Round to the day", + "h": "Round to the hour", + "m": "Round to the minute", + "s": "Round to the second", + "w": "Round to the week", + "y": "Round to the year", + }, + "timeTenseOptions": Array [ + Object { + "text": "Last", + "value": "last", + }, + Object { + "text": "Next", + "value": "next", + }, + ], + "timeUnitsOptions": Array [ + Object { + "text": "Seconds", + "value": "s", + }, + Object { + "text": "Minutes", + "value": "m", + }, + Object { + "text": "Hours", + "value": "h", + }, + Object { + "text": "Days", + "value": "d", + }, + Object { + "text": "Weeks", + "value": "w", + }, + Object { + "text": "Months", + "value": "M", + }, + Object { + "text": "Years", + "value": "y", + }, + ], + } + } /> } > @@ -538,7 +1517,55 @@ exports[`EuiSuperDatePicker props showUpdateButton can be false 1`] = ` disabled={false} onClick={[Function]} > - Last 15 minutes + @@ -610,6 +1637,173 @@ exports[`EuiSuperDatePicker props showUpdateButton can be iconOnly 1`] = ` recentlyUsedRanges={Array []} refreshInterval={1000} start="now-15m" + timeOptions={ + Object { + "commonDurationRanges": Array [ + Object { + "end": "now/d", + "label": "Today", + "start": "now/d", + }, + Object { + "end": "now/w", + "label": "This week", + "start": "now/w", + }, + Object { + "end": "now/M", + "label": "This month", + "start": "now/M", + }, + Object { + "end": "now/y", + "label": "This year", + "start": "now/y", + }, + Object { + "end": "now-1d/d", + "label": "Yesterday", + "start": "now-1d/d", + }, + Object { + "end": "now", + "label": "Week to date", + "start": "now/w", + }, + Object { + "end": "now", + "label": "Month to date", + "start": "now/M", + }, + Object { + "end": "now", + "label": "Year to date", + "start": "now/y", + }, + ], + "refreshUnitsOptions": Array [ + Object { + "text": "Seconds", + "value": "s", + }, + Object { + "text": "Minutes", + "value": "m", + }, + Object { + "text": "Hours", + "value": "h", + }, + ], + "relativeOptions": Array [ + Object { + "text": "Seconds ago", + "value": "s", + }, + Object { + "text": "Minutes ago", + "value": "m", + }, + Object { + "text": "Hours ago", + "value": "h", + }, + Object { + "text": "Days ago", + "value": "d", + }, + Object { + "text": "Weeks ago", + "value": "w", + }, + Object { + "text": "Months ago", + "value": "M", + }, + Object { + "text": "Years ago", + "value": "y", + }, + Object { + "text": "Seconds from now", + "value": "s+", + }, + Object { + "text": "Minutes from now", + "value": "m+", + }, + Object { + "text": "Hours from now", + "value": "h+", + }, + Object { + "text": "Days from now", + "value": "d+", + }, + Object { + "text": "Weeks from now", + "value": "w+", + }, + Object { + "text": "Months from now", + "value": "M+", + }, + Object { + "text": "Years from now", + "value": "y+", + }, + ], + "relativeRoundingLabels": Object { + "M": "Round to the month", + "d": "Round to the day", + "h": "Round to the hour", + "m": "Round to the minute", + "s": "Round to the second", + "w": "Round to the week", + "y": "Round to the year", + }, + "timeTenseOptions": Array [ + Object { + "text": "Last", + "value": "last", + }, + Object { + "text": "Next", + "value": "next", + }, + ], + "timeUnitsOptions": Array [ + Object { + "text": "Seconds", + "value": "s", + }, + Object { + "text": "Minutes", + "value": "m", + }, + Object { + "text": "Hours", + "value": "h", + }, + Object { + "text": "Days", + "value": "d", + }, + Object { + "text": "Weeks", + "value": "w", + }, + Object { + "text": "Months", + "value": "M", + }, + Object { + "text": "Years", + "value": "y", + }, + ], + } + } /> } > @@ -626,7 +1820,55 @@ exports[`EuiSuperDatePicker props showUpdateButton can be iconOnly 1`] = ` disabled={false} onClick={[Function]} > - Last 15 minutes + @@ -719,6 +1961,173 @@ exports[`EuiSuperDatePicker props width can be auto 1`] = ` recentlyUsedRanges={Array []} refreshInterval={1000} start="now-15m" + timeOptions={ + Object { + "commonDurationRanges": Array [ + Object { + "end": "now/d", + "label": "Today", + "start": "now/d", + }, + Object { + "end": "now/w", + "label": "This week", + "start": "now/w", + }, + Object { + "end": "now/M", + "label": "This month", + "start": "now/M", + }, + Object { + "end": "now/y", + "label": "This year", + "start": "now/y", + }, + Object { + "end": "now-1d/d", + "label": "Yesterday", + "start": "now-1d/d", + }, + Object { + "end": "now", + "label": "Week to date", + "start": "now/w", + }, + Object { + "end": "now", + "label": "Month to date", + "start": "now/M", + }, + Object { + "end": "now", + "label": "Year to date", + "start": "now/y", + }, + ], + "refreshUnitsOptions": Array [ + Object { + "text": "Seconds", + "value": "s", + }, + Object { + "text": "Minutes", + "value": "m", + }, + Object { + "text": "Hours", + "value": "h", + }, + ], + "relativeOptions": Array [ + Object { + "text": "Seconds ago", + "value": "s", + }, + Object { + "text": "Minutes ago", + "value": "m", + }, + Object { + "text": "Hours ago", + "value": "h", + }, + Object { + "text": "Days ago", + "value": "d", + }, + Object { + "text": "Weeks ago", + "value": "w", + }, + Object { + "text": "Months ago", + "value": "M", + }, + Object { + "text": "Years ago", + "value": "y", + }, + Object { + "text": "Seconds from now", + "value": "s+", + }, + Object { + "text": "Minutes from now", + "value": "m+", + }, + Object { + "text": "Hours from now", + "value": "h+", + }, + Object { + "text": "Days from now", + "value": "d+", + }, + Object { + "text": "Weeks from now", + "value": "w+", + }, + Object { + "text": "Months from now", + "value": "M+", + }, + Object { + "text": "Years from now", + "value": "y+", + }, + ], + "relativeRoundingLabels": Object { + "M": "Round to the month", + "d": "Round to the day", + "h": "Round to the hour", + "m": "Round to the minute", + "s": "Round to the second", + "w": "Round to the week", + "y": "Round to the year", + }, + "timeTenseOptions": Array [ + Object { + "text": "Last", + "value": "last", + }, + Object { + "text": "Next", + "value": "next", + }, + ], + "timeUnitsOptions": Array [ + Object { + "text": "Seconds", + "value": "s", + }, + Object { + "text": "Minutes", + "value": "m", + }, + Object { + "text": "Hours", + "value": "h", + }, + Object { + "text": "Days", + "value": "d", + }, + Object { + "text": "Weeks", + "value": "w", + }, + Object { + "text": "Months", + "value": "M", + }, + Object { + "text": "Years", + "value": "y", + }, + ], + } + } /> } > @@ -735,7 +2144,55 @@ exports[`EuiSuperDatePicker props width can be auto 1`] = ` disabled={false} onClick={[Function]} > - Last 15 minutes + @@ -828,6 +2285,173 @@ exports[`EuiSuperDatePicker props width can be full 1`] = ` recentlyUsedRanges={Array []} refreshInterval={1000} start="now-15m" + timeOptions={ + Object { + "commonDurationRanges": Array [ + Object { + "end": "now/d", + "label": "Today", + "start": "now/d", + }, + Object { + "end": "now/w", + "label": "This week", + "start": "now/w", + }, + Object { + "end": "now/M", + "label": "This month", + "start": "now/M", + }, + Object { + "end": "now/y", + "label": "This year", + "start": "now/y", + }, + Object { + "end": "now-1d/d", + "label": "Yesterday", + "start": "now-1d/d", + }, + Object { + "end": "now", + "label": "Week to date", + "start": "now/w", + }, + Object { + "end": "now", + "label": "Month to date", + "start": "now/M", + }, + Object { + "end": "now", + "label": "Year to date", + "start": "now/y", + }, + ], + "refreshUnitsOptions": Array [ + Object { + "text": "Seconds", + "value": "s", + }, + Object { + "text": "Minutes", + "value": "m", + }, + Object { + "text": "Hours", + "value": "h", + }, + ], + "relativeOptions": Array [ + Object { + "text": "Seconds ago", + "value": "s", + }, + Object { + "text": "Minutes ago", + "value": "m", + }, + Object { + "text": "Hours ago", + "value": "h", + }, + Object { + "text": "Days ago", + "value": "d", + }, + Object { + "text": "Weeks ago", + "value": "w", + }, + Object { + "text": "Months ago", + "value": "M", + }, + Object { + "text": "Years ago", + "value": "y", + }, + Object { + "text": "Seconds from now", + "value": "s+", + }, + Object { + "text": "Minutes from now", + "value": "m+", + }, + Object { + "text": "Hours from now", + "value": "h+", + }, + Object { + "text": "Days from now", + "value": "d+", + }, + Object { + "text": "Weeks from now", + "value": "w+", + }, + Object { + "text": "Months from now", + "value": "M+", + }, + Object { + "text": "Years from now", + "value": "y+", + }, + ], + "relativeRoundingLabels": Object { + "M": "Round to the month", + "d": "Round to the day", + "h": "Round to the hour", + "m": "Round to the minute", + "s": "Round to the second", + "w": "Round to the week", + "y": "Round to the year", + }, + "timeTenseOptions": Array [ + Object { + "text": "Last", + "value": "last", + }, + Object { + "text": "Next", + "value": "next", + }, + ], + "timeUnitsOptions": Array [ + Object { + "text": "Seconds", + "value": "s", + }, + Object { + "text": "Minutes", + "value": "m", + }, + Object { + "text": "Hours", + "value": "h", + }, + Object { + "text": "Days", + "value": "d", + }, + Object { + "text": "Weeks", + "value": "w", + }, + Object { + "text": "Months", + "value": "M", + }, + Object { + "text": "Years", + "value": "y", + }, + ], + } + } /> } > @@ -844,7 +2468,55 @@ exports[`EuiSuperDatePicker props width can be full 1`] = ` disabled={false} onClick={[Function]} > - Last 15 minutes + 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 0c37bb5f52c1..9bb4b1e1b5d0 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 @@ -14,7 +14,7 @@ import dateMath from '@elastic/datemath'; import { EuiDatePicker, EuiDatePickerProps } from '../../date_picker'; import { EuiFormRow, EuiFieldText, EuiFormLabel } from '../../../form'; -import { toSentenceCase } from '../../../../services/string/to_case'; +import { EuiI18n } from '../../../i18n'; import { EuiDatePopoverContentProps } from './date_popover_content'; export interface EuiAbsoluteTabProps { @@ -25,12 +25,12 @@ export interface EuiAbsoluteTabProps { onChange: EuiDatePopoverContentProps['onChange']; roundUp: boolean; position: 'start' | 'end'; + labelPrefix: string; utcOffset?: number; } interface EuiAbsoluteTabState { isTextInvalid: boolean; - sentenceCasedPosition: string; textInputValue: string; valueAsMoment: Moment | null; } @@ -44,8 +44,6 @@ export class EuiAbsoluteTab extends Component< 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(); @@ -56,7 +54,6 @@ export class EuiAbsoluteTab extends Component< this.state = { isTextInvalid: false, - sentenceCasedPosition, textInputValue, valueAsMoment, }; @@ -96,13 +93,14 @@ export class EuiAbsoluteTab extends Component< }; render() { - const { dateFormat, timeFormat, locale, utcOffset } = this.props; const { - valueAsMoment, - isTextInvalid, - textInputValue, - sentenceCasedPosition, - } = this.state; + dateFormat, + timeFormat, + locale, + utcOffset, + labelPrefix, + } = this.props; + const { valueAsMoment, isTextInvalid, textInputValue } = this.state; return (
@@ -117,20 +115,28 @@ export class EuiAbsoluteTab extends Component< locale={locale} utcOffset={utcOffset} /> - - {sentenceCasedPosition} date} - /> - + {(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 dec4d90f9997..a12b78b630aa 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 @@ -17,7 +17,8 @@ import { LocaleSpecifier } from 'moment'; // eslint-disable-line import/named import { useEuiI18n } from '../../../i18n'; import { EuiPopover, EuiPopoverProps } from '../../../popover'; -import { formatTimeString } from '../pretty_duration'; +import { TimeOptions } from '../time_options'; +import { useFormatTimeString } from '../pretty_duration'; import { EuiDatePopoverContent, EuiDatePopoverContentProps, @@ -41,6 +42,7 @@ export interface EuiDatePopoverButtonProps { value: string; utcOffset?: number; compressed?: boolean; + timeOptions: TimeOptions; } export const EuiDatePopoverButton: FunctionComponent = ( @@ -63,6 +65,7 @@ export const EuiDatePopoverButton: FunctionComponent onPopoverToggle, onPopoverClose, compressed, + timeOptions, ...rest } = props; @@ -78,7 +81,12 @@ export const EuiDatePopoverButton: FunctionComponent }, ]); - const formattedValue = formatTimeString(value, dateFormat, roundUp, locale); + const formattedValue = useFormatTimeString( + value, + dateFormat, + roundUp, + locale + ); let title = formattedValue; const invalidTitle = useEuiI18n( @@ -130,6 +138,7 @@ export const EuiDatePopoverButton: FunctionComponent locale={locale} position={position} utcOffset={utcOffset} + timeOptions={timeOptions} /> ); 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 8fe49449bbb8..422b4d06b49c 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 @@ -8,6 +8,7 @@ import React, { FunctionComponent } from 'react'; +import { EuiI18n, useEuiI18n } from '../../../i18n'; import { EuiTabbedContent, EuiTabbedContentProps } from '../../../tabs'; import { EuiText } from '../../../text'; import { EuiButton } from '../../../button'; @@ -15,6 +16,7 @@ import { EuiButton } from '../../../button'; import { EuiAbsoluteTab } from './absolute_tab'; import { EuiRelativeTab } from './relative_tab'; +import { TimeOptions } from '../time_options'; import { getDateMode, DATE_MODES, @@ -32,6 +34,7 @@ export interface EuiDatePopoverContentProps { locale?: LocaleSpecifier; position: 'start' | 'end'; utcOffset?: number; + timeOptions: TimeOptions; } export const EuiDatePopoverContent: FunctionComponent = ({ @@ -43,6 +46,7 @@ export const EuiDatePopoverContent: FunctionComponent { const onTabClick: EuiTabbedContentProps['onTabClick'] = (selectedTab) => { switch (selectedTab.id) { @@ -55,12 +59,30 @@ export const EuiDatePopoverContent: FunctionComponent ), 'data-test-subj': 'superDatePickerAbsoluteTab', - 'aria-label': `${ariaLabel} Absolute`, + 'aria-label': `${labelPrefix}: ${absoluteLabel}`, }, { id: DATE_MODES.RELATIVE, - name: 'Relative', + name: relativeLabel, content: ( ), 'data-test-subj': 'superDatePickerRelativeTab', - 'aria-label': `${ariaLabel} Relative`, + 'aria-label': `${labelPrefix}: ${relativeLabel}`, }, { id: DATE_MODES.NOW, - name: 'Now', + name: nowLabel, content: (

- Setting the time to "now" means that on every refresh this - time will be set to the time of the refresh. +

- Set {position} date and time to now + {position === 'start' ? ( + + ) : ( + + )}
), 'data-test-subj': 'superDatePickerNowTab', - 'aria-label': `${ariaLabel} Now`, + 'aria-label': `${labelPrefix}: ${nowLabel}`, }, ]; diff --git a/src/components/date_picker/super_date_picker/date_popover/relative_tab.tsx b/src/components/date_picker/super_date_picker/date_popover/relative_tab.tsx index d4eb4ff1d33d..08c19ee00854 100644 --- a/src/components/date_picker/super_date_picker/date_popover/relative_tab.tsx +++ b/src/components/date_picker/super_date_picker/date_popover/relative_tab.tsx @@ -8,7 +8,6 @@ import React, { Component, ChangeEventHandler } from 'react'; import dateMath from '@elastic/datemath'; -import { toSentenceCase } from '../../../../services/string/to_case'; import { htmlIdGenerator } from '../../../../services'; import { EuiFlexGroup, EuiFlexItem } from '../../../flex'; import { @@ -23,8 +22,7 @@ import { } from '../../../form'; import { EuiSpacer } from '../../../spacer'; -import { timeUnits } from '../time_units'; -import { relativeOptions } from '../relative_options'; +import { TimeOptions } from '../time_options'; import { parseRelativeParts, toRelativeStringFromParts, @@ -44,12 +42,13 @@ export interface EuiRelativeTabProps { onChange: EuiDatePopoverContentProps['onChange']; roundUp?: boolean; position: 'start' | 'end'; + labelPrefix: string; + timeOptions: TimeOptions; } interface EuiRelativeTabState extends Pick { count: number | undefined; - sentenceCasedPosition: string; } export class EuiRelativeTab extends Component< @@ -58,7 +57,6 @@ export class EuiRelativeTab extends Component< > { state: EuiRelativeTabState = { ...parseRelativeParts(this.props.value), - sentenceCasedPosition: toSentenceCase(this.props.position), }; relativeDateInputNumberDescriptionId = htmlIdGenerator()(); @@ -107,6 +105,7 @@ export class EuiRelativeTab extends Component< }; render() { + const { relativeOptions, relativeRoundingLabels } = this.props.timeOptions; const { count, unit } = this.state; const invalidDate = this.props.value === INVALID_DATE; const invalidValue = count === undefined || count < 0; @@ -204,15 +203,7 @@ export class EuiRelativeTab extends Component< compressed value={formattedValue} readOnly - prepend={ - - - - } + prepend={{this.props.labelPrefix}} />

@@ -225,20 +216,12 @@ export class EuiRelativeTab extends Component< - - {(roundingLabel: string) => ( - - )} - + ); diff --git a/src/components/date_picker/super_date_picker/index.ts b/src/components/date_picker/super_date_picker/index.ts index 397e96c4a780..a9c9fefb3f05 100644 --- a/src/components/date_picker/super_date_picker/index.ts +++ b/src/components/date_picker/super_date_picker/index.ts @@ -20,4 +20,4 @@ export { EuiSuperDatePicker } from './super_date_picker'; export type { EuiSuperUpdateButtonProps } from './super_update_button'; export { EuiSuperUpdateButton } from './super_update_button'; -export { prettyDuration, commonDurationRanges } from './pretty_duration'; +export { PrettyDuration, usePrettyDuration } from './pretty_duration'; diff --git a/src/components/date_picker/super_date_picker/pretty_duration.test.ts b/src/components/date_picker/super_date_picker/pretty_duration.test.tsx similarity index 57% rename from src/components/date_picker/super_date_picker/pretty_duration.test.ts rename to src/components/date_picker/super_date_picker/pretty_duration.test.tsx index cd91fbbce75f..a3818586f4a6 100644 --- a/src/components/date_picker/super_date_picker/pretty_duration.test.ts +++ b/src/components/date_picker/super_date_picker/pretty_duration.test.tsx @@ -6,7 +6,15 @@ * Side Public License, v 1. */ -import { prettyDuration, showPrettyDuration } from './pretty_duration'; +import React from 'react'; +import { shallow } from 'enzyme'; +import { testCustomHook } from '../../../test/internal'; + +import { + usePrettyDuration, + PrettyDuration, + showPrettyDuration, +} from './pretty_duration'; const dateFormat = 'MMMM Do YYYY, HH:mm:ss.SSS'; const quickRanges = [ @@ -17,45 +25,73 @@ const quickRanges = [ }, ]; -describe('prettyDuration', () => { +describe('usePrettyDuration', () => { test('quick range', () => { const timeFrom = 'now-15m'; const timeTo = 'now'; - expect(prettyDuration(timeFrom, timeTo, quickRanges, dateFormat)).toBe( - 'quick range 15 minutes custom display' - ); + expect( + testCustomHook(() => + usePrettyDuration({ timeFrom, timeTo, quickRanges, dateFormat }) + ).return + ).toBe('quick range 15 minutes custom display'); }); test('last', () => { const timeFrom = 'now-16m'; const timeTo = 'now'; - expect(prettyDuration(timeFrom, timeTo, quickRanges, dateFormat)).toBe( - 'Last 16 minutes' - ); + expect( + testCustomHook(() => + usePrettyDuration({ timeFrom, timeTo, quickRanges, dateFormat }) + ).return + ).toBe('Last 16 minutes'); }); test('last that is rounded', () => { const timeFrom = 'now-1M/w'; const timeTo = 'now'; - expect(prettyDuration(timeFrom, timeTo, quickRanges, dateFormat)).toBe( - 'Last 1 month rounded to the week' - ); + expect( + testCustomHook(() => + usePrettyDuration({ timeFrom, timeTo, quickRanges, dateFormat }) + ).return + ).toBe('Last 1 month rounded to the week'); }); test('next', () => { const timeFrom = 'now'; const timeTo = 'now+16m'; - expect(prettyDuration(timeFrom, timeTo, quickRanges, dateFormat)).toBe( - 'Next 16 minutes' - ); + expect( + testCustomHook(() => + usePrettyDuration({ timeFrom, timeTo, quickRanges, dateFormat }) + ).return + ).toBe('Next 16 minutes'); }); test('from is in past', () => { const timeFrom = 'now-17m'; const timeTo = 'now-15m'; - expect(prettyDuration(timeFrom, timeTo, quickRanges, dateFormat)).toBe( - '~ 17 minutes ago to ~ 15 minutes ago' + expect( + testCustomHook(() => + usePrettyDuration({ timeFrom, timeTo, quickRanges, dateFormat }) + ).return + ).toBe('~ 17 minutes ago to ~ 15 minutes ago'); + }); +}); + +describe('PrettyDuration', () => { + it('renders the returned string from usePrettyDuration', () => { + const component = shallow( + ); + expect(component).toMatchInlineSnapshot(` + + Next 15 minutes + + `); }); }); diff --git a/src/components/date_picker/super_date_picker/pretty_duration.ts b/src/components/date_picker/super_date_picker/pretty_duration.ts deleted file mode 100644 index 29f496cd6d03..000000000000 --- a/src/components/date_picker/super_date_picker/pretty_duration.ts +++ /dev/null @@ -1,127 +0,0 @@ -/* - * 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 dateMath from '@elastic/datemath'; -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: 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' }, - { start: 'now/y', end: 'now/y', label: 'This year' }, - { start: 'now-1d/d', end: 'now-1d/d', label: 'Yesterday' }, - { start: 'now/w', end: 'now', label: 'Week to date' }, - { start: 'now/M', end: 'now', label: 'Month to date' }, - { start: 'now/y', end: 'now', label: 'Year to date' }, -]; - -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: ShortDate, timeTo: ShortDate) { - const fromDateMode = getDateMode(timeFrom); - const toDateMode = getDateMode(timeTo); - const isLast = - fromDateMode === DATE_MODES.RELATIVE && toDateMode === DATE_MODES.NOW; - const isNext = - fromDateMode === DATE_MODES.NOW && toDateMode === DATE_MODES.RELATIVE; - return isLast || isNext; -} - -export function formatTimeString( - timeString: string, - dateFormat: string, - roundUp = false, - locale: LocaleSpecifier = 'en' -): string { - const timeAsMoment = moment(timeString, ISO_FORMAT, true); - if (timeAsMoment.isValid()) { - return timeAsMoment.locale(locale).format(dateFormat); - } - - if (timeString === 'now') { - return 'now'; - } - - const tryParse = dateMath.parse(timeString, { roundUp: roundUp }); - if (!moment(tryParse).isValid()) { - return 'Invalid Date'; - } - - if (moment.isMoment(tryParse)) { - return `~ ${tryParse.locale(locale).fromNow()}`; - } - - return timeString; -} - -export function prettyDuration( - timeFrom: ShortDate, - timeTo: ShortDate, - quickRanges: DurationRange[] = [], - dateFormat: string -) { - const matchingQuickRange = quickRanges.find( - ({ start: quickFrom, end: quickTo }) => { - return timeFrom === quickFrom && timeTo === quickTo; - } - ); - if (matchingQuickRange && matchingQuickRange.label) { - return matchingQuickRange.label; - } - - if (isRelativeToNow(timeFrom, timeTo)) { - let timeTense; - let relativeParts; - if (getDateMode(timeTo) === DATE_MODES.NOW) { - timeTense = 'Last'; - relativeParts = parseRelativeParts(timeFrom); - } else { - timeTense = 'Next'; - relativeParts = parseRelativeParts(timeTo); - } - const countTimeUnit = relativeParts.unit.substring(0, 1) as TimeUnitId; - const countTimeUnitFullName = - relativeParts.count > 1 - ? timeUnitsPlural[countTimeUnit] - : timeUnits[countTimeUnit]; - let text = `${timeTense} ${relativeParts.count} ${countTimeUnitFullName}`; - if (relativeParts.round && relativeParts.roundUnit) { - text += ` rounded to the ${timeUnits[relativeParts.roundUnit]}`; - } - return text; - } - - return cantLookup(timeFrom, timeTo, dateFormat); -} - -export function showPrettyDuration( - timeFrom: ShortDate, - timeTo: ShortDate, - quickRanges: DurationRange[] = [] -) { - const matchingQuickRange = quickRanges.find( - ({ start: quickFrom, end: quickTo }) => { - return timeFrom === quickFrom && timeTo === quickTo; - } - ); - if (matchingQuickRange) { - return true; - } - - return isRelativeToNow(timeFrom, timeTo); -} diff --git a/src/components/date_picker/super_date_picker/pretty_duration.tsx b/src/components/date_picker/super_date_picker/pretty_duration.tsx new file mode 100644 index 000000000000..e64d1b9f5680 --- /dev/null +++ b/src/components/date_picker/super_date_picker/pretty_duration.tsx @@ -0,0 +1,308 @@ +/* + * 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 dateMath from '@elastic/datemath'; +import moment, { LocaleSpecifier } from 'moment'; // eslint-disable-line import/named +import { useEuiI18n } from '../../i18n'; +import { getDateMode, DATE_MODES } from './date_modes'; +import { parseRelativeParts } from './relative_utils'; +import { useI18nTimeOptions } from './time_options'; +import { + DurationRange, + TimeUnitAllId, + ShortDate, + RelativeParts, +} from '../types'; + +/** + * Pretty duration i18n strings + * Units should not be simply concatenated because different languages + * will have different grammar/positions for time than English + */ + +const useRelativeDurationI18n = (duration: number) => ({ + s: useEuiI18n( + 'euiPrettyDuration.lastDurationSeconds', + ({ duration }) => `Last ${duration} second${duration === 1 ? '' : 's'}`, + { duration } + ), + 's+': useEuiI18n( + 'euiPrettyDuration.nextDurationSeconds', + ({ duration }) => `Next ${duration} second${duration === 1 ? '' : 's'}`, + { duration } + ), + m: useEuiI18n( + 'euiPrettyDuration.lastDurationMinutes', + ({ duration }) => `Last ${duration} minute${duration === 1 ? '' : 's'}`, + { duration } + ), + 'm+': useEuiI18n( + 'euiPrettyDuration.nextDurationMinutes', + ({ duration }) => `Next ${duration} minute${duration === 1 ? '' : 's'}`, + { duration } + ), + h: useEuiI18n( + 'euiPrettyDuration.lastDurationHours', + ({ duration }) => `Last ${duration} hour${duration === 1 ? '' : 's'}`, + { duration } + ), + 'h+': useEuiI18n( + 'euiPrettyDuration.nextDurationHours', + ({ duration }) => `Next ${duration} hour${duration === 1 ? '' : 's'}`, + { duration } + ), + d: useEuiI18n( + 'euiPrettyDuration.lastDurationDays', + ({ duration }) => `Last ${duration} day${duration === 1 ? '' : 's'}`, + { duration } + ), + 'd+': useEuiI18n( + 'euiPrettyDuration.nexttDurationDays', + ({ duration }) => `Next ${duration} day${duration === 1 ? '' : 's'}`, + { duration } + ), + w: useEuiI18n( + 'euiPrettyDuration.lastDurationWeeks', + ({ duration }) => `Last ${duration} week${duration === 1 ? '' : 's'}`, + { duration } + ), + 'w+': useEuiI18n( + 'euiPrettyDuration.nextDurationWeeks', + ({ duration }) => `Next ${duration} week${duration === 1 ? '' : 's'}`, + { duration } + ), + M: useEuiI18n( + 'euiPrettyDuration.lastDurationMonths', + ({ duration }) => `Last ${duration} month${duration === 1 ? '' : 's'}`, + { duration } + ), + 'M+': useEuiI18n( + 'euiPrettyDuration.nextDurationMonths', + ({ duration }) => `Next ${duration} month${duration === 1 ? '' : 's'}`, + { duration } + ), + y: useEuiI18n( + 'euiPrettyDuration.lastDurationYears', + ({ duration }) => `Last ${duration} year${duration === 1 ? '' : 's'}`, + { duration } + ), + 'y+': useEuiI18n( + 'euiPrettyDuration.nextDurationYears', + ({ duration }) => `Next ${duration} year${duration === 1 ? '' : 's'}`, + { duration } + ), +}); + +const useRelativeDurationRoundedI18n = (prettyDuration: string) => ({ + s: useEuiI18n( + 'euiPrettyDuration.durationRoundedToSecond', + '{prettyDuration} rounded to the second', + { prettyDuration } + ), + m: useEuiI18n( + 'euiPrettyDuration.durationRoundedToMinute', + '{prettyDuration} rounded to the minute', + { prettyDuration } + ), + h: useEuiI18n( + 'euiPrettyDuration.durationRoundedToHour', + '{prettyDuration} rounded to the hour', + { prettyDuration } + ), + d: useEuiI18n( + 'euiPrettyDuration.durationRoundedToDay', + '{prettyDuration} rounded to the day', + { prettyDuration } + ), + w: useEuiI18n( + 'euiPrettyDuration.durationRoundedToWeek', + '{prettyDuration} rounded to the week', + { prettyDuration } + ), + M: useEuiI18n( + 'euiPrettyDuration.durationRoundedToMonth', + '{prettyDuration} rounded to the month', + { prettyDuration } + ), + y: useEuiI18n( + 'euiPrettyDuration.durationRoundedToYear', + '{prettyDuration} rounded to the year', + { prettyDuration } + ), +}); + +/** + * Reusable format time string util + */ + +const ISO_FORMAT = 'YYYY-MM-DDTHH:mm:ss.SSSZ'; + +export const useFormatTimeString = ( + timeString: string, + dateFormat: string, + roundUp = false, + locale: LocaleSpecifier = 'en' +): string => { + // i18n'd strings + const nowDisplay = useEuiI18n('euiPrettyDuration.now', 'now'); + const invalidDateDisplay = useEuiI18n( + 'euiPrettyDuration.invalid', + 'Invalid date' + ); + + const timeAsMoment = moment(timeString, ISO_FORMAT, true); + if (timeAsMoment.isValid()) { + return timeAsMoment.locale(locale).format(dateFormat); + } + + if (timeString === 'now') { + return nowDisplay; + } + + const tryParse = dateMath.parse(timeString, { roundUp: roundUp }); + if (!moment(tryParse).isValid()) { + return invalidDateDisplay; + } + + if (moment.isMoment(tryParse)) { + return `~ ${tryParse.locale(locale).fromNow()}`; + } + + return timeString; +}; + +/** + * Pretty duration hook+component + */ + +interface PrettyDurationProps { + timeFrom: ShortDate; + timeTo: ShortDate; + quickRanges?: DurationRange[]; + dateFormat: string; +} + +export const usePrettyDuration = ({ + timeFrom, + timeTo, + quickRanges, + dateFormat, +}: PrettyDurationProps) => { + let prettyDuration: string = ''; + + /** + * If it's a quick range, use the quick range label + */ + const { commonDurationRanges } = useI18nTimeOptions(); + const matchingQuickRange = hasRangeMatch( + timeFrom, + timeTo, + quickRanges || commonDurationRanges + ); + if (matchingQuickRange && matchingQuickRange.label) { + prettyDuration = matchingQuickRange.label; + } + + /** + * Otherwise if it's a relative (possibly rounded) duration, figure out + * a pretty i18n'd duration to display + */ + let relativeDuration: number = 0; + let relativeParts = {} as RelativeParts; + + if (isRelativeToNow(timeFrom, timeTo)) { + if (getDateMode(timeTo) === DATE_MODES.NOW) { + relativeParts = parseRelativeParts(timeFrom); + } else { + relativeParts = parseRelativeParts(timeTo); + } + relativeDuration = relativeParts.count; + } + + const relativeDurationI18n = useRelativeDurationI18n(relativeDuration); + const relativeDurationString = relativeParts.unit + ? relativeDurationI18n[relativeParts.unit as TimeUnitAllId] + : ''; + + const roundedDurationI18n = useRelativeDurationRoundedI18n( + relativeDurationString + ); + const roundedDurationString = + relativeParts.round && relativeParts.roundUnit + ? roundedDurationI18n[relativeParts.roundUnit] + : ''; + + if (!prettyDuration) { + prettyDuration = roundedDurationString || relativeDurationString; + } + + /** + * If it's none of the above, display basic fallback copy + */ + const displayFrom = useFormatTimeString(timeFrom, dateFormat); + const displayTo = useFormatTimeString(timeTo, dateFormat, true); + const fallbackDuration = useEuiI18n( + 'euiPrettyDuration.fallbackDuration', + '{displayFrom} to {displayTo}', + { displayFrom, displayTo } + ); + if (!prettyDuration) { + prettyDuration = fallbackDuration; + } + + return prettyDuration; +}; + +export const PrettyDuration: React.FC = ({ + timeFrom, + timeTo, + quickRanges, + dateFormat, +}) => { + const prettyDuration = usePrettyDuration({ + timeFrom, + timeTo, + quickRanges, + dateFormat, + }); + return <>{prettyDuration}; +}; + +/** + * Non-hook utils + */ + +const hasRangeMatch = ( + timeFrom: ShortDate, + timeTo: ShortDate, + ranges: DurationRange[] +) => { + return ranges.find(({ start, end }) => timeFrom === start && timeTo === end); +}; + +const isRelativeToNow = (timeFrom: ShortDate, timeTo: ShortDate): boolean => { + const fromDateMode = getDateMode(timeFrom); + const toDateMode = getDateMode(timeTo); + const isLast = + fromDateMode === DATE_MODES.RELATIVE && toDateMode === DATE_MODES.NOW; + const isNext = + fromDateMode === DATE_MODES.NOW && toDateMode === DATE_MODES.RELATIVE; + return isLast || isNext; +}; + +export const showPrettyDuration = ( + timeFrom: ShortDate, + timeTo: ShortDate, + quickRanges: DurationRange[] +): boolean => { + if (hasRangeMatch(timeFrom, timeTo, quickRanges)) { + return true; + } + return isRelativeToNow(timeFrom, timeTo); +}; diff --git a/src/components/date_picker/super_date_picker/pretty_interval.test.ts b/src/components/date_picker/super_date_picker/pretty_interval.test.ts index e15004af616d..fb0d93399662 100644 --- a/src/components/date_picker/super_date_picker/pretty_interval.test.ts +++ b/src/components/date_picker/super_date_picker/pretty_interval.test.ts @@ -6,40 +6,77 @@ * Side Public License, v 1. */ -import { prettyInterval } from './pretty_interval'; +import { testCustomHook } from '../../../test/internal'; + +import { usePrettyInterval } from './pretty_interval'; const IS_NOT_PAUSED = false; const IS_PAUSED = true; const SHORT_HAND = true; -test('Off', () => { - expect(prettyInterval(IS_NOT_PAUSED, 0)).toBe('Off'); - expect(prettyInterval(IS_PAUSED, 1000)).toBe('Off'); -}); +describe('usePrettyInterval', () => { + test('off', () => { + expect( + testCustomHook(() => usePrettyInterval(IS_NOT_PAUSED, 0)).return + ).toBe('Off'); + expect( + testCustomHook(() => usePrettyInterval(IS_PAUSED, 1000)).return + ).toBe('Off'); + }); -test('seconds', () => { - expect(prettyInterval(IS_NOT_PAUSED, 1000)).toBe('1 second'); - expect(prettyInterval(IS_NOT_PAUSED, 15000)).toBe('15 seconds'); -}); + test('seconds', () => { + expect( + testCustomHook(() => usePrettyInterval(IS_NOT_PAUSED, 1000)).return + ).toBe('1 second'); + expect( + testCustomHook(() => usePrettyInterval(IS_NOT_PAUSED, 15000)).return + ).toBe('15 seconds'); + expect( + testCustomHook(() => usePrettyInterval(IS_NOT_PAUSED, 15000, SHORT_HAND)) + .return + ).toBe('15 s'); + }); -test('minutes', () => { - expect(prettyInterval(IS_NOT_PAUSED, 60000)).toBe('1 minute'); - expect(prettyInterval(IS_NOT_PAUSED, 1800000)).toBe('30 minutes'); -}); + test('minutes', () => { + expect( + testCustomHook(() => usePrettyInterval(IS_NOT_PAUSED, 60000)).return + ).toBe('1 minute'); + expect( + testCustomHook(() => usePrettyInterval(IS_NOT_PAUSED, 1800000)).return + ).toBe('30 minutes'); + expect( + testCustomHook(() => + usePrettyInterval(IS_NOT_PAUSED, 1800000, SHORT_HAND) + ).return + ).toBe('30 m'); + }); -test('hours', () => { - expect(prettyInterval(IS_NOT_PAUSED, 3600000)).toBe('1 hour'); - expect(prettyInterval(IS_NOT_PAUSED, 43200000)).toBe('12 hours'); -}); - -test('days', () => { - expect(prettyInterval(IS_NOT_PAUSED, 86400000)).toBe('1 day'); - expect(prettyInterval(IS_NOT_PAUSED, 86400000 * 2)).toBe('2 days'); -}); + test('hours', () => { + expect( + testCustomHook(() => usePrettyInterval(IS_NOT_PAUSED, 3600000)).return + ).toBe('1 hour'); + expect( + testCustomHook(() => usePrettyInterval(IS_NOT_PAUSED, 43200000)).return + ).toBe('12 hours'); + expect( + testCustomHook(() => + usePrettyInterval(IS_NOT_PAUSED, 43200000, SHORT_HAND) + ).return + ).toBe('12 h'); + }); -test('shortHand', () => { - expect(prettyInterval(IS_NOT_PAUSED, 86400000, SHORT_HAND)).toBe('1 d'); - expect(prettyInterval(IS_NOT_PAUSED, 43200000, SHORT_HAND)).toBe('12 h'); - expect(prettyInterval(IS_NOT_PAUSED, 1800000, SHORT_HAND)).toBe('30 m'); - expect(prettyInterval(IS_NOT_PAUSED, 15000, SHORT_HAND)).toBe('15 s'); + test('days', () => { + expect( + testCustomHook(() => usePrettyInterval(IS_NOT_PAUSED, 86400000)).return + ).toBe('1 day'); + expect( + testCustomHook(() => usePrettyInterval(IS_NOT_PAUSED, 86400000 * 2)) + .return + ).toBe('2 days'); + expect( + testCustomHook(() => + usePrettyInterval(IS_NOT_PAUSED, 86400000, SHORT_HAND) + ).return + ).toBe('1 d'); + }); }); diff --git a/src/components/date_picker/super_date_picker/pretty_interval.ts b/src/components/date_picker/super_date_picker/pretty_interval.ts index 60f6b2de9de7..cced154246e1 100644 --- a/src/components/date_picker/super_date_picker/pretty_interval.ts +++ b/src/components/date_picker/super_date_picker/pretty_interval.ts @@ -6,38 +6,89 @@ * Side Public License, v 1. */ +import { useEuiI18n } from '../../i18n'; + const MS_IN_SECOND = 1000; const MS_IN_MINUTE = 60 * MS_IN_SECOND; const MS_IN_HOUR = 60 * MS_IN_MINUTE; const MS_IN_DAY = 24 * MS_IN_HOUR; -export const prettyInterval = ( +type IntervalUnitId = 's' | 'm' | 'h' | 'd'; + +/** + * Pretty interval i18n strings + * + * Units should not be simply concatenated because different languages + * will have different grammar/positions for time than English + */ +const usePrettyIntervalI18n = (interval: number) => ({ + s: useEuiI18n( + 'euiPrettyInterval.seconds', + ({ interval }) => `${interval} second${interval > 1 ? 's' : ''}`, + { interval } + ), + m: useEuiI18n( + 'euiPrettyInterval.minutes', + ({ interval }) => `${interval} minute${interval > 1 ? 's' : ''}`, + { interval } + ), + h: useEuiI18n( + 'euiPrettyInterval.hours', + ({ interval }) => `${interval} hour${interval > 1 ? 's' : ''}`, + { interval } + ), + d: useEuiI18n( + 'euiPrettyInterval.days', + ({ interval }) => `${interval} day${interval > 1 ? 's' : ''}`, + { interval } + ), + shorthand: { + s: useEuiI18n('euiPrettyInterval.secondsShorthand', '{interval} s', { + interval, + }), + m: useEuiI18n('euiPrettyInterval.minutesShorthand', '{interval} m', { + interval, + }), + h: useEuiI18n('euiPrettyInterval.hoursShorthand', '{interval} h', { + interval, + }), + d: useEuiI18n('euiPrettyInterval.daysShorthand', '{interval} d', { + interval, + }), + }, +}); + +export const usePrettyInterval = ( isPaused: boolean, intervalInMs: number, shortHand: boolean = false ) => { - let units: string; - if (isPaused || intervalInMs === 0) { - return 'Off'; - } else if (intervalInMs < MS_IN_MINUTE) { - const intervalInSeconds = Math.round(intervalInMs / MS_IN_SECOND); - if (shortHand) return `${intervalInSeconds} s`; - units = intervalInSeconds > 1 ? 'seconds' : 'second'; - return `${intervalInSeconds} ${units}`; + let prettyInterval = ''; + let interval: number; + let unitId: IntervalUnitId; + + if (intervalInMs < MS_IN_MINUTE) { + interval = Math.round(intervalInMs / MS_IN_SECOND); + unitId = 's'; } else if (intervalInMs < MS_IN_HOUR) { - const intervalInMinutes = Math.round(intervalInMs / MS_IN_MINUTE); - if (shortHand) return `${intervalInMinutes} m`; - units = intervalInMinutes > 1 ? 'minutes' : 'minute'; - return `${intervalInMinutes} ${units}`; + interval = Math.round(intervalInMs / MS_IN_MINUTE); + unitId = 'm'; } else if (intervalInMs < MS_IN_DAY) { - const intervalInHours = Math.round(intervalInMs / MS_IN_HOUR); - if (shortHand) return `${intervalInHours} h`; - units = intervalInHours > 1 ? 'hours' : 'hour'; - return `${intervalInHours} ${units}`; + interval = Math.round(intervalInMs / MS_IN_HOUR); + unitId = 'h'; + } else { + interval = Math.round(intervalInMs / MS_IN_DAY); + unitId = 'd'; + } + const prettyIntervalI18n = usePrettyIntervalI18n(interval); + prettyInterval = shortHand + ? prettyIntervalI18n.shorthand[unitId] + : prettyIntervalI18n[unitId]; + + const off = useEuiI18n('euiPrettyInterval.off', 'Off'); + if (isPaused || intervalInMs === 0) { + prettyInterval = off; } - const intervalInDays = Math.round(intervalInMs / MS_IN_DAY); - if (shortHand) return `${intervalInDays} d`; - units = intervalInDays > 1 ? 'days' : 'day'; - return `${intervalInDays} ${units}`; + return prettyInterval; }; diff --git a/src/components/date_picker/super_date_picker/quick_select_popover/__snapshots__/quick_select.test.tsx.snap b/src/components/date_picker/super_date_picker/quick_select_popover/__snapshots__/quick_select.test.tsx.snap index ca349c98328e..01703e056e2a 100644 --- a/src/components/date_picker/super_date_picker/quick_select_popover/__snapshots__/quick_select.test.tsx.snap +++ b/src/components/date_picker/super_date_picker/quick_select_popover/__snapshots__/quick_select.test.tsx.snap @@ -113,7 +113,7 @@ exports[`EuiQuickSelect is rendered 1`] = ` values={ Object { "timeTense": "last", - "timeUnit": "minutes", + "timeUnit": "Minutes", "timeValue": 15, } } @@ -236,7 +236,7 @@ exports[`EuiQuickSelect prevQuickSelect 1`] = ` values={ Object { "timeTense": "Next", - "timeUnit": "months", + "timeUnit": "Months", "timeValue": 32, } } diff --git a/src/components/date_picker/super_date_picker/quick_select_popover/__snapshots__/quick_select_popover.test.tsx.snap b/src/components/date_picker/super_date_picker/quick_select_popover/__snapshots__/quick_select_popover.test.tsx.snap index 15edcb536b6f..5b5f295bcf7d 100644 --- a/src/components/date_picker/super_date_picker/quick_select_popover/__snapshots__/quick_select_popover.test.tsx.snap +++ b/src/components/date_picker/super_date_picker/quick_select_popover/__snapshots__/quick_select_popover.test.tsx.snap @@ -40,6 +40,173 @@ exports[`EuiQuickSelectPopover is rendered 1`] = ` applyTime={[Function]} end="now" start="now-15m" + timeOptions={ + Object { + "commonDurationRanges": Array [ + Object { + "end": "now/d", + "label": "Today", + "start": "now/d", + }, + Object { + "end": "now/w", + "label": "This week", + "start": "now/w", + }, + Object { + "end": "now/M", + "label": "This month", + "start": "now/M", + }, + Object { + "end": "now/y", + "label": "This year", + "start": "now/y", + }, + Object { + "end": "now-1d/d", + "label": "Yesterday", + "start": "now-1d/d", + }, + Object { + "end": "now", + "label": "Week to date", + "start": "now/w", + }, + Object { + "end": "now", + "label": "Month to date", + "start": "now/M", + }, + Object { + "end": "now", + "label": "Year to date", + "start": "now/y", + }, + ], + "refreshUnitsOptions": Array [ + Object { + "text": "Seconds", + "value": "s", + }, + Object { + "text": "Minutes", + "value": "m", + }, + Object { + "text": "Hours", + "value": "h", + }, + ], + "relativeOptions": Array [ + Object { + "text": "Seconds ago", + "value": "s", + }, + Object { + "text": "Minutes ago", + "value": "m", + }, + Object { + "text": "Hours ago", + "value": "h", + }, + Object { + "text": "Days ago", + "value": "d", + }, + Object { + "text": "Weeks ago", + "value": "w", + }, + Object { + "text": "Months ago", + "value": "M", + }, + Object { + "text": "Years ago", + "value": "y", + }, + Object { + "text": "Seconds from now", + "value": "s+", + }, + Object { + "text": "Minutes from now", + "value": "m+", + }, + Object { + "text": "Hours from now", + "value": "h+", + }, + Object { + "text": "Days from now", + "value": "d+", + }, + Object { + "text": "Weeks from now", + "value": "w+", + }, + Object { + "text": "Months from now", + "value": "M+", + }, + Object { + "text": "Years from now", + "value": "y+", + }, + ], + "relativeRoundingLabels": Object { + "M": "Round to the month", + "d": "Round to the day", + "h": "Round to the hour", + "m": "Round to the minute", + "s": "Round to the second", + "w": "Round to the week", + "y": "Round to the year", + }, + "timeTenseOptions": Array [ + Object { + "text": "Last", + "value": "last", + }, + Object { + "text": "Next", + "value": "next", + }, + ], + "timeUnitsOptions": Array [ + Object { + "text": "Seconds", + "value": "s", + }, + Object { + "text": "Minutes", + "value": "m", + }, + Object { + "text": "Hours", + "value": "h", + }, + Object { + "text": "Days", + "value": "d", + }, + Object { + "text": "Weeks", + "value": "w", + }, + Object { + "text": "Months", + "value": "M", + }, + Object { + "text": "Years", + "value": "y", + }, + ], + } + } /> {}; const defaultProps = { @@ -20,22 +21,33 @@ const defaultProps = { describe('EuiQuickSelect', () => { test('is rendered', () => { - const component = shallow(); + const component = shallow( + + {(timeOptions) => ( + + )} + + ).dive(); expect(component).toMatchSnapshot(); }); test('prevQuickSelect', () => { const component = shallow( - - ); + + {(timeOptions) => ( + + )} + + ).dive(); expect(component).toMatchSnapshot(); }); diff --git a/src/components/date_picker/super_date_picker/quick_select_popover/quick_select.tsx b/src/components/date_picker/super_date_picker/quick_select_popover/quick_select.tsx index f4eeabe65a83..c3711c88b7f7 100644 --- a/src/components/date_picker/super_date_picker/quick_select_popover/quick_select.tsx +++ b/src/components/date_picker/super_date_picker/quick_select_popover/quick_select.tsx @@ -20,23 +20,11 @@ import { EuiSpacer } from '../../../spacer'; import { EuiSelect, EuiFieldNumber } from '../../../form'; import { EuiToolTip } from '../../../tool_tip'; import { EuiI18n } from '../../../i18n'; -import { timeUnits } from '../time_units'; import { EuiScreenReaderOnly } from '../../../accessibility'; import { ApplyTime, QuickSelect, TimeUnitId } from '../../types'; -import { keysOf } from '../../../common'; +import { TimeOptions, NEXT } from '../time_options'; import { parseTimeParts } from './quick_select_utils'; -const LAST = 'last'; -const NEXT = 'next'; - -const timeTenseOptions = [ - { value: LAST, text: 'Last' }, - { value: NEXT, text: 'Next' }, -]; -const timeUnitsOptions = keysOf(timeUnits).map((key) => { - return { value: key, text: `${timeUnits[key]}s` }; -}); - type EuiQuickSelectState = QuickSelect; export interface EuiQuickSelectProps { @@ -44,6 +32,7 @@ export interface EuiQuickSelectProps { start: string; end: string; prevQuickSelect?: EuiQuickSelectState; + timeOptions: TimeOptions; } export class EuiQuickSelect extends Component< @@ -161,6 +150,8 @@ export class EuiQuickSelect extends Component< render() { const { timeTense, timeValue, timeUnits } = this.state; + const { timeTenseOptions, timeUnitsOptions } = this.props.timeOptions; + const matchedTimeUnit = timeUnitsOptions.find( ({ value }) => value === timeUnits ); diff --git a/src/components/date_picker/super_date_picker/quick_select_popover/quick_select_popover.test.tsx b/src/components/date_picker/super_date_picker/quick_select_popover/quick_select_popover.test.tsx index 1564c54fffc3..7389468739c8 100644 --- a/src/components/date_picker/super_date_picker/quick_select_popover/quick_select_popover.test.tsx +++ b/src/components/date_picker/super_date_picker/quick_select_popover/quick_select_popover.test.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { shallow } from 'enzyme'; +import { RenderI18nTimeOptions } from '../time_options'; import { EuiQuickSelectPopover, EuiQuickSelectPopoverProps, @@ -16,7 +17,7 @@ import { const noop = () => {}; -const defaultProps: EuiQuickSelectPopoverProps = { +const defaultProps: Omit = { applyTime: noop, applyRefreshInterval: noop, start: 'now-15m', @@ -31,8 +32,13 @@ const defaultProps: EuiQuickSelectPopoverProps = { describe('EuiQuickSelectPopover', () => { test('is rendered', () => { - const component = shallow(); - + const component = shallow( + + {(timeOptions) => ( + + )} + + ).dive(); expect(component).toMatchSnapshot(); }); }); diff --git a/src/components/date_picker/super_date_picker/quick_select_popover/quick_select_popover.tsx b/src/components/date_picker/super_date_picker/quick_select_popover/quick_select_popover.tsx index c581211d7c81..f333e9f8396b 100644 --- a/src/components/date_picker/super_date_picker/quick_select_popover/quick_select_popover.tsx +++ b/src/components/date_picker/super_date_picker/quick_select_popover/quick_select_popover.tsx @@ -20,6 +20,7 @@ import { EuiQuickSelect } from './quick_select'; import { EuiCommonlyUsedTimeRanges } from './commonly_used_time_ranges'; import { EuiRecentlyUsed } from './recently_used'; import { EuiRefreshInterval } from '../../auto_refresh/refresh_interval'; +import { TimeOptions } from '../time_options'; import { DurationRange, ApplyRefreshInterval, @@ -40,6 +41,7 @@ export interface EuiQuickSelectPopoverProps { recentlyUsedRanges: DurationRange[]; refreshInterval: number; start: string; + timeOptions: TimeOptions; } interface EuiQuickSelectPopoverState { @@ -90,6 +92,7 @@ export class EuiQuickSelectPopover extends Component< end, recentlyUsedRanges, start, + timeOptions, } = this.props; const { prevQuickSelect } = this.state; @@ -100,6 +103,7 @@ export class EuiQuickSelectPopover extends Component< start={start} end={end} prevQuickSelect={prevQuickSelect} + timeOptions={timeOptions} /> {commonlyUsedRanges.length > 0 && } value === DATE_MODES.NOW; diff --git a/src/components/date_picker/super_date_picker/quick_select_popover/recently_used.tsx b/src/components/date_picker/super_date_picker/quick_select_popover/recently_used.tsx index 6a12a7222f65..2855dd3b57da 100644 --- a/src/components/date_picker/super_date_picker/quick_select_popover/recently_used.tsx +++ b/src/components/date_picker/super_date_picker/quick_select_popover/recently_used.tsx @@ -7,7 +7,7 @@ */ import React, { FunctionComponent } from 'react'; -import { prettyDuration } from '../pretty_duration'; +import { PrettyDuration } from '../pretty_duration'; import { EuiI18n } from '../../../i18n'; import { useGeneratedHtmlId } from '../../../../services'; @@ -44,7 +44,12 @@ export const EuiRecentlyUsed: FunctionComponent = ({ key={`${start}-${end}`} > - {prettyDuration(start, end, commonlyUsedRanges, dateFormat)} + ); diff --git a/src/components/date_picker/super_date_picker/relative_options.test.ts b/src/components/date_picker/super_date_picker/relative_options.test.ts deleted file mode 100644 index 3af0f629e840..000000000000 --- a/src/components/date_picker/super_date_picker/relative_options.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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 { relativeUnitsFromLargestToSmallest } from './relative_options'; - -describe('relativeUnitsFromLargestToSmallest', () => { - test('relativeUnitsFromLargestToSmallest length', () => { - expect(relativeUnitsFromLargestToSmallest.length).toBe(7); - }); - test('relativeUnitsFromLargestToSmallest order', () => { - expect(relativeUnitsFromLargestToSmallest).toEqual([ - 'y', - 'M', - 'w', - 'd', - 'h', - 'm', - 's', - ]); - }); -}); diff --git a/src/components/date_picker/super_date_picker/relative_options.ts b/src/components/date_picker/super_date_picker/relative_options.ts deleted file mode 100644 index 43eea68886ce..000000000000 --- a/src/components/date_picker/super_date_picker/relative_options.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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 { RelativeOption, TimeUnitId } from '../types'; - -export const relativeOptions: RelativeOption[] = [ - { text: 'Seconds ago', value: 's' }, - { text: 'Minutes ago', value: 'm' }, - { text: 'Hours ago', value: 'h' }, - { text: 'Days ago', value: 'd' }, - { text: 'Weeks ago', value: 'w' }, - { text: 'Months ago', value: 'M' }, - { text: 'Years ago', value: 'y' }, - - { text: 'Seconds from now', value: 's+' }, - { text: 'Minutes from now', value: 'm+' }, - { text: 'Hours from now', value: 'h+' }, - { text: 'Days from now', value: 'd+' }, - { text: 'Weeks from now', value: 'w+' }, - { text: 'Months from now', value: 'M+' }, - { text: 'Years from now', value: 'y+' }, -]; - -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.ts b/src/components/date_picker/super_date_picker/relative_utils.ts index 2ff8bd33d7d0..9ad0b211f6c6 100644 --- a/src/components/date_picker/super_date_picker/relative_utils.ts +++ b/src/components/date_picker/super_date_picker/relative_utils.ts @@ -11,11 +11,20 @@ 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 const relativeUnitsFromLargestToSmallest: TimeUnitId[] = [ + 'y', + 'M', + 'w', + 'd', + 'h', + 'm', + 's', +]; + export function parseRelativeParts(value: string): RelativeParts { const matches = isString(value) && diff --git a/src/components/date_picker/super_date_picker/super_date_picker.test.tsx b/src/components/date_picker/super_date_picker/super_date_picker.test.tsx index 0c6fa0463a8a..a81705a63f80 100644 --- a/src/components/date_picker/super_date_picker/super_date_picker.test.tsx +++ b/src/components/date_picker/super_date_picker/super_date_picker.test.tsx @@ -7,29 +7,42 @@ */ import React from 'react'; -import { mount, shallow } from 'enzyme'; +import { mount, shallow, ReactWrapper } from 'enzyme'; import { EuiSuperDatePicker, EuiSuperDatePickerProps, + EuiSuperDatePickerInternal, } from './super_date_picker'; import { EuiButton } from '../../button'; const noop = () => {}; +// Test utils to handle diving into EuiSuperDatePickerInternal +const findInternalInstance = ( + wrapper: ReactWrapper +): [EuiSuperDatePickerInternal, ReactWrapper] => { + const component = wrapper.find('EuiSuperDatePickerInternal'); + const instance = component.instance() as EuiSuperDatePickerInternal; + return [instance, component]; +}; +const shallowAndDive = (component: React.ReactElement) => + shallow(component).dive().dive(); + describe('EuiSuperDatePicker', () => { test('is rendered', () => { - const component = shallow(); + const component = shallowAndDive( + + ); expect(component).toMatchSnapshot(); }); test('refresh is disabled by default', () => { // By default we expect `asyncInterval` to be not set. - const componentPaused = mount( - - ); - const instancePaused = componentPaused.instance(); + const component = mount(); + const [instancePaused, componentPaused] = findInternalInstance(component); + expect(instancePaused.asyncInterval).toBe(undefined); expect(componentPaused.prop('isPaused')).toBe(true); }); @@ -38,31 +51,38 @@ 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 component = mount( ); - const instanceRefresh = componentRefresh.instance(); + const [instanceRefresh, componentRefresh] = findInternalInstance(component); + expect(typeof instanceRefresh.asyncInterval).toBe('object'); 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(); + component.setProps({ isPaused: true }); + const [ + instanceUpdatedPaused, + componentUpdatedPaused, + ] = findInternalInstance(component); expect(typeof instanceUpdatedPaused.asyncInterval).toBe('object'); expect(instanceUpdatedPaused.asyncInterval!.isStopped).toBe(true); - expect(componentRefresh.prop('isPaused')).toBe(true); + expect(componentUpdatedPaused.prop('isPaused')).toBe(true); // Let's start refresh again for a final sanity check. - componentRefresh.setProps({ isPaused: false }); - const instanceUpdatedRefresh = componentRefresh.instance(); + component.setProps({ isPaused: false }); + const [ + instanceUpdatedRefresh, + componentUpdatedRefresh, + ] = findInternalInstance(component); expect(typeof instanceUpdatedRefresh.asyncInterval).toBe('object'); expect(instanceUpdatedRefresh.asyncInterval!.isStopped).toBe(false); - expect(componentRefresh.prop('isPaused')).toBe(false); + expect(componentUpdatedRefresh.prop('isPaused')).toBe(false); }); test('Listen for consecutive super date picker refreshes', async () => { @@ -70,7 +90,7 @@ describe('EuiSuperDatePicker', () => { const onRefresh = jest.fn(); - const componentRefresh = mount( + const component = mount( { refreshInterval={10} /> ); + const [instanceRefresh] = findInternalInstance(component); - const instanceRefresh = componentRefresh.instance(); expect(typeof instanceRefresh.asyncInterval).toBe('object'); jest.advanceTimersByTime(10); @@ -97,7 +117,7 @@ describe('EuiSuperDatePicker', () => { const onRefresh = jest.fn(); - const componentRefresh = mount( + const component = mount( { refreshInterval={10} /> ); - - const instanceRefresh = componentRefresh.instance(); + const [instanceRefresh] = findInternalInstance(component); jest.advanceTimersByTime(10); expect(typeof instanceRefresh.asyncInterval).toBe('object'); await instanceRefresh.asyncInterval!.__pendingFn; - componentRefresh.setProps({ isPaused: true, refreshInterval: 0 }); + component.setProps({ isPaused: true, refreshInterval: 0 }); jest.advanceTimersByTime(10); await instanceRefresh.asyncInterval!.__pendingFn; @@ -140,14 +159,14 @@ describe('EuiSuperDatePicker', () => { describe('showUpdateButton', () => { test('can be false', () => { - const component = shallow( + const component = shallowAndDive( ); expect(component).toMatchSnapshot(); }); test('can be iconOnly', () => { - const component = shallow( + const component = shallowAndDive( ); expect(component).toMatchSnapshot(); @@ -155,7 +174,7 @@ describe('EuiSuperDatePicker', () => { }); test('accepts data-test-subj and passes to EuiFormControlLayout', () => { - const component = shallow( + const component = shallowAndDive( { describe('width', () => { test('can be full', () => { - const component = shallow( + const component = shallowAndDive( ); expect(component).toMatchSnapshot(); }); test('can be auto', () => { - const component = shallow( + const component = shallowAndDive( ); expect(component).toMatchSnapshot(); @@ -181,7 +200,7 @@ describe('EuiSuperDatePicker', () => { describe('compressed', () => { test('is rendered', () => { - const component = shallow( + const component = shallowAndDive( ); expect(component).toMatchSnapshot(); @@ -190,7 +209,7 @@ describe('EuiSuperDatePicker', () => { describe('isQuickSelectOnly', () => { test('is rendered', () => { - const component = shallow( + const component = shallowAndDive( ); expect(component).toMatchSnapshot(); @@ -199,7 +218,7 @@ describe('EuiSuperDatePicker', () => { describe('isAutoRefreshOnly', () => { it('is rendered', () => { - const component = shallow( + const component = shallowAndDive( { }); it('passes required props', () => { - const component = shallow( + const component = shallowAndDive( void; - recentlyUsedRanges: DurationRange[]; + recentlyUsedRanges?: DurationRange[]; /** * Refresh interval in milliseconds */ - refreshInterval: Milliseconds; + refreshInterval?: Milliseconds; - start: ShortDate; - end: ShortDate; + start?: ShortDate; + end?: ShortDate; /** * Specifies the formatted used when displaying times */ - timeFormat: string; + timeFormat?: string; utcOffset?: number; /** * Set showUpdateButton to false to immediately invoke onTimeChange for all start and end changes. */ - showUpdateButton: boolean | 'iconOnly'; + showUpdateButton?: boolean | 'iconOnly'; /** * Hides the actual input reducing to just the quick select button. @@ -142,6 +137,20 @@ export type EuiSuperDatePickerProps = CommonProps & { updateButtonProps?: EuiSuperUpdateButtonProps; }; +type EuiSuperDatePickerInternalProps = EuiSuperDatePickerProps & { + timeOptions: TimeOptions; + // The below options are marked as required because they have default fallbacks + commonlyUsedRanges: DurationRange[]; + recentlyUsedRanges: DurationRange[]; + start: ShortDate; + end: ShortDate; + refreshInterval: Milliseconds; + dateFormat: string; + timeFormat: string; + isPaused: boolean; + isDisabled: boolean; +}; + interface EuiSuperDatePickerState { end: ShortDate; hasChanged: boolean; @@ -176,12 +185,11 @@ function isRangeInvalid(start: ShortDate, end: ShortDate) { return isInvalid; } -export class EuiSuperDatePicker extends Component< - EuiSuperDatePickerProps, +export class EuiSuperDatePickerInternal extends Component< + EuiSuperDatePickerInternalProps, EuiSuperDatePickerState > { static defaultProps = { - commonlyUsedRanges: commonDurationRanges, dateFormat: 'MMM D, YYYY @ HH:mm:ss.SSS', end: 'now', isAutoRefreshOnly: false, @@ -216,7 +224,7 @@ export class EuiSuperDatePicker extends Component< }; static getDerivedStateFromProps( - nextProps: EuiSuperDatePickerProps, + nextProps: EuiSuperDatePickerInternalProps, prevState: EuiSuperDatePickerState ) { if ( @@ -301,7 +309,11 @@ export class EuiSuperDatePicker extends Component< applyQuickTime: ApplyTime = ({ start, end }) => { this.setState({ - showPrettyDuration: showPrettyDuration(start, end, commonDurationRanges), + showPrettyDuration: showPrettyDuration( + start, + end, + this.props.commonlyUsedRanges + ), }); this.props.onTimeChange({ start, @@ -374,6 +386,7 @@ export class EuiSuperDatePicker extends Component< } = this.state; const { commonlyUsedRanges, + timeOptions, dateFormat, isDisabled, locale, @@ -403,7 +416,12 @@ export class EuiSuperDatePicker extends Component< disabled={isDisabled} onClick={this.hidePrettyDuration} > - {prettyDuration(start, end, commonlyUsedRanges, dateFormat)} + ); @@ -433,6 +451,7 @@ export class EuiSuperDatePicker extends Component< isOpen={this.state.isStartDatePopoverOpen} onPopoverToggle={this.onStartDatePopoverToggle} onPopoverClose={this.onStartDatePopoverClose} + timeOptions={timeOptions} /> } endDateControl={ @@ -452,6 +471,7 @@ export class EuiSuperDatePicker extends Component< isOpen={this.state.isEndDatePopoverOpen} onPopoverToggle={this.onEndDatePopoverToggle} onPopoverClose={this.onEndDatePopoverClose} + timeOptions={timeOptions} /> } /> @@ -503,6 +523,7 @@ export class EuiSuperDatePicker extends Component< render() { const { commonlyUsedRanges, + timeOptions, customQuickSelectPanels, dateFormat, end, @@ -549,6 +570,7 @@ export class EuiSuperDatePicker extends Component< recentlyUsedRanges={recentlyUsedRanges} refreshInterval={refreshInterval} start={start} + timeOptions={timeOptions} /> ); @@ -599,3 +621,23 @@ export class EuiSuperDatePicker extends Component< ); } } + +// Because EuiSuperDatePicker is a class component and not a functional component, +// we have to use a render prop here in order for us to pass i18n'd strings/objects/etc +// to all underlying usages of our timeOptions constants. If someday we convert +// EuiSuperDatePicker to an FC, we can likely get rid of this wrapper. +export const EuiSuperDatePicker: FunctionComponent = ( + props +) => ( + + {(timeOptions) => ( + + )} + +); diff --git a/src/components/date_picker/super_date_picker/time_options.test.tsx b/src/components/date_picker/super_date_picker/time_options.test.tsx new file mode 100644 index 000000000000..27c70bb3c172 --- /dev/null +++ b/src/components/date_picker/super_date_picker/time_options.test.tsx @@ -0,0 +1,203 @@ +/* + * 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 { shallow } from 'enzyme'; +import { testCustomHook } from '../../../test/internal'; + +import { useI18nTimeOptions, RenderI18nTimeOptions } from './time_options'; + +describe('useI18nTimeOptions', () => { + it('returns a series of time options constants/arrays/objects with i18n strings', () => { + const { return: timeOptions } = testCustomHook(useI18nTimeOptions); + + expect(timeOptions).toMatchInlineSnapshot(` + Object { + "commonDurationRanges": Array [ + Object { + "end": "now/d", + "label": "Today", + "start": "now/d", + }, + Object { + "end": "now/w", + "label": "This week", + "start": "now/w", + }, + Object { + "end": "now/M", + "label": "This month", + "start": "now/M", + }, + Object { + "end": "now/y", + "label": "This year", + "start": "now/y", + }, + Object { + "end": "now-1d/d", + "label": "Yesterday", + "start": "now-1d/d", + }, + Object { + "end": "now", + "label": "Week to date", + "start": "now/w", + }, + Object { + "end": "now", + "label": "Month to date", + "start": "now/M", + }, + Object { + "end": "now", + "label": "Year to date", + "start": "now/y", + }, + ], + "refreshUnitsOptions": Array [ + Object { + "text": "Seconds", + "value": "s", + }, + Object { + "text": "Minutes", + "value": "m", + }, + Object { + "text": "Hours", + "value": "h", + }, + ], + "relativeOptions": Array [ + Object { + "text": "Seconds ago", + "value": "s", + }, + Object { + "text": "Minutes ago", + "value": "m", + }, + Object { + "text": "Hours ago", + "value": "h", + }, + Object { + "text": "Days ago", + "value": "d", + }, + Object { + "text": "Weeks ago", + "value": "w", + }, + Object { + "text": "Months ago", + "value": "M", + }, + Object { + "text": "Years ago", + "value": "y", + }, + Object { + "text": "Seconds from now", + "value": "s+", + }, + Object { + "text": "Minutes from now", + "value": "m+", + }, + Object { + "text": "Hours from now", + "value": "h+", + }, + Object { + "text": "Days from now", + "value": "d+", + }, + Object { + "text": "Weeks from now", + "value": "w+", + }, + Object { + "text": "Months from now", + "value": "M+", + }, + Object { + "text": "Years from now", + "value": "y+", + }, + ], + "relativeRoundingLabels": Object { + "M": "Round to the month", + "d": "Round to the day", + "h": "Round to the hour", + "m": "Round to the minute", + "s": "Round to the second", + "w": "Round to the week", + "y": "Round to the year", + }, + "timeTenseOptions": Array [ + Object { + "text": "Last", + "value": "last", + }, + Object { + "text": "Next", + "value": "next", + }, + ], + "timeUnitsOptions": Array [ + Object { + "text": "Seconds", + "value": "s", + }, + Object { + "text": "Minutes", + "value": "m", + }, + Object { + "text": "Hours", + "value": "h", + }, + Object { + "text": "Days", + "value": "d", + }, + Object { + "text": "Weeks", + "value": "w", + }, + Object { + "text": "Months", + "value": "M", + }, + Object { + "text": "Years", + "value": "y", + }, + ], + } + `); + }); +}); + +describe('RenderI18nTimeOptions', () => { + it('is a render function that passes the underlying children timeOptions as an arg', () => { + const component = shallow( + + {({ timeUnitsOptions }) => <>{timeUnitsOptions[0].text}} + + ); + + expect(component).toMatchInlineSnapshot(` + + Seconds + + `); + }); +}); diff --git a/src/components/date_picker/super_date_picker/time_options.tsx b/src/components/date_picker/super_date_picker/time_options.tsx new file mode 100644 index 000000000000..eda29e7b9576 --- /dev/null +++ b/src/components/date_picker/super_date_picker/time_options.tsx @@ -0,0 +1,190 @@ +/* + * 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 { useEuiI18n } from '../../i18n'; +import { EuiSelectOption } from '../../form'; + +import { TimeUnitId, RelativeOption, DurationRange } from '../types'; + +export const LAST = 'last'; +export const NEXT = 'next'; + +export type TimeOptions = { + timeTenseOptions: EuiSelectOption[]; + timeUnitsOptions: EuiSelectOption[]; + relativeOptions: RelativeOption[]; + relativeRoundingLabels: { [id in TimeUnitId]: string }; + refreshUnitsOptions: EuiSelectOption[]; + commonDurationRanges: DurationRange[]; +}; + +/** + * i18n'd time options, mostly used in EuiSelects (except for a few cases) + * used in EuiSuperDatePicker child sub-components + */ +export const useI18nTimeOptions = (): TimeOptions => { + /** + * Quick select panel + */ + const timeTenseOptions = [ + { value: LAST, text: useEuiI18n('euiTimeOptions.last', 'Last') }, + { value: NEXT, text: useEuiI18n('euiTimeOptions.next', 'Next') }, + ]; + + const timeUnitsOptions = [ + { value: 's', text: useEuiI18n('euiTimeOptions.seconds', 'Seconds') }, + { value: 'm', text: useEuiI18n('euiTimeOptions.minutes', 'Minutes') }, + { value: 'h', text: useEuiI18n('euiTimeOptions.hours', 'Hours') }, + { value: 'd', text: useEuiI18n('euiTimeOptions.days', 'Days') }, + { value: 'w', text: useEuiI18n('euiTimeOptions.weeks', 'Weeks') }, + { value: 'M', text: useEuiI18n('euiTimeOptions.months', 'Months') }, + { value: 'y', text: useEuiI18n('euiTimeOptions.years', 'Years') }, + ]; + + /** + * Relative tab + */ + const relativeOptions: RelativeOption[] = [ + { + text: useEuiI18n('euiTimeOptions.secondsAgo', 'Seconds ago'), + value: 's', + }, + { + text: useEuiI18n('euiTimeOptions.minutesAgo', 'Minutes ago'), + value: 'm', + }, + { + text: useEuiI18n('euiTimeOptions.hoursAgo', 'Hours ago'), + value: 'h', + }, + { + text: useEuiI18n('euiTimeOptions.daysAgo', 'Days ago'), + value: 'd', + }, + { + text: useEuiI18n('euiTimeOptions.weeksAgo', 'Weeks ago'), + value: 'w', + }, + { + text: useEuiI18n('euiTimeOptions.monthsAgo', 'Months ago'), + value: 'M', + }, + { + text: useEuiI18n('euiTimeOptions.yearsAgo', 'Years ago'), + value: 'y', + }, + { + text: useEuiI18n('euiTimeOptions.secondsFromNow', 'Seconds from now'), + value: 's+', + }, + { + text: useEuiI18n('euiTimeOptions.minutesFromNow', 'Minutes from now'), + value: 'm+', + }, + { + text: useEuiI18n('euiTimeOptions.hoursFromNow', 'Hours from now'), + value: 'h+', + }, + { + text: useEuiI18n('euiTimeOptions.daysFromNow', 'Days from now'), + value: 'd+', + }, + { + text: useEuiI18n('euiTimeOptions.weeksFromNow', 'Weeks from now'), + value: 'w+', + }, + { + text: useEuiI18n('euiTimeOptions.monthsFromNow', 'Months from now'), + value: 'M+', + }, + { + text: useEuiI18n('euiTimeOptions.yearsFromNow', 'Years from now'), + value: 'y+', + }, + ]; + + const relativeRoundingLabels = { + s: useEuiI18n('euiTimeOptions.roundToSecond', 'Round to the second'), + m: useEuiI18n('euiTimeOptions.roundToMinute', 'Round to the minute'), + h: useEuiI18n('euiTimeOptions.roundToHour', 'Round to the hour'), + d: useEuiI18n('euiTimeOptions.roundToDay', 'Round to the day'), + w: useEuiI18n('euiTimeOptions.roundToWeek', 'Round to the week'), + M: useEuiI18n('euiTimeOptions.roundToMonth', 'Round to the month'), + y: useEuiI18n('euiTimeOptions.roundToYear', 'Round to the year'), + }; + + /** + * Auto Refresh + */ + const refreshUnitsOptions = timeUnitsOptions.filter( + ({ value }) => value === 'h' || value === 'm' || value === 's' + ); + + /** + * Used by both Quick Select ('Commonly used') and by PrettyDuration + */ + const commonDurationRanges = [ + { + start: 'now/d', + end: 'now/d', + label: useEuiI18n('euiTimeOptions.today', 'Today'), + }, + { + start: 'now/w', + end: 'now/w', + label: useEuiI18n('euiTimeOptions.thisWeek', 'This week'), + }, + { + start: 'now/M', + end: 'now/M', + label: useEuiI18n('euiTimeOptions.thisMonth', 'This month'), + }, + { + start: 'now/y', + end: 'now/y', + label: useEuiI18n('euiTimeOptions.thisYear', 'This year'), + }, + { + start: 'now-1d/d', + end: 'now-1d/d', + label: useEuiI18n('euiTimeOptions.yesterday', 'Yesterday'), + }, + { + start: 'now/w', + end: 'now', + label: useEuiI18n('euiTimeOptions.weekToDate', 'Week to date'), + }, + { + start: 'now/M', + end: 'now', + label: useEuiI18n('euiTimeOptions.monthToDate', 'Month to date'), + }, + { + start: 'now/y', + end: 'now', + label: useEuiI18n('euiTimeOptions.yearToDate', 'Year to date'), + }, + ]; + + return { + timeTenseOptions, + timeUnitsOptions, + relativeOptions, + relativeRoundingLabels, + refreshUnitsOptions, + commonDurationRanges, + }; +}; + +// Render function of the above, used by class components that can't use hooks +export const RenderI18nTimeOptions = (props: { + children: (args: TimeOptions) => any; +}) => { + const timeOptions = useI18nTimeOptions(); + return props.children(timeOptions); +}; diff --git a/src/components/date_picker/super_date_picker/time_units.ts b/src/components/date_picker/super_date_picker/time_units.ts deleted file mode 100644 index c6418cff96d8..000000000000 --- a/src/components/date_picker/super_date_picker/time_units.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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 { 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/types.ts b/src/components/date_picker/types.ts index a318d3d1f0fd..02a0918dad82 100644 --- a/src/components/date_picker/types.ts +++ b/src/components/date_picker/types.ts @@ -16,6 +16,7 @@ export interface DurationRange { export type TimeUnitId = 's' | 'm' | 'h' | 'd' | 'w' | 'M' | 'y'; export type TimeUnitFromNowId = 's+' | 'm+' | 'h+' | 'd+' | 'w+' | 'M+' | 'y+'; +export type TimeUnitAllId = TimeUnitId | TimeUnitFromNowId; export type TimeUnitLabel = | 'second' | 'minute' @@ -54,7 +55,7 @@ export interface RelativeParts { export interface RelativeOption { text: string; - value: TimeUnitId | TimeUnitFromNowId; + value: TimeUnitAllId; } export type OnRefreshChangeProps = { diff --git a/upcoming_changelogs/5743.md b/upcoming_changelogs/5743.md new file mode 100644 index 000000000000..6d5bedfa66d1 --- /dev/null +++ b/upcoming_changelogs/5743.md @@ -0,0 +1,6 @@ +- Added all remaining missing i18n tokens for `EuiSuperDatePicker` + +**Breaking changes** + +- Removed `prettyDuration` utility exported by `EuiSuperDatePicker` - this util was converted to a `PrettyDuration` component and `usePrettyDuration` hook which should be used instead +- Removed `commonDurationRanges` exported by `EuiSuperDatePicker`. The new pretty duration utils will fall back to `commonDurationRanges` by default if no `quickRanges` are passed