mirror of https://github.com/grafana/grafana
Alerting: adding a time picker for selecting relative time. (#33689)
* adding placeholder for relative time range. * fixed story. * added basic structure to handle open/close of time range picker. * removed section from TimeOptions since it isn't used any where. * adding mapper and tests * move relativetimepicker to its own dir * added some simple tests. * changed test. * use relativetimerangeinput * redo state management * refactored the tests. * replace timerange with relativetimerange * wip * wip * did some refactoring. * refactored time option formatting. * added proper formatting and display of time range. * add relative time description, slight refactor of height * fixed incorrect import. * added validator and changed formatting. * removed unused dep. * reverted back to internal function. * fixed display of relative time range picker. * fixed failing tests. * fixed parsing issue. * fixed position of time range picker. * some more refactorings. * fixed validation of really big values. * added another test. Co-authored-by: Peter Holmberg <peter.hlmbrg@gmail.com>pull/34011/head^2
parent
2459a0ceb5
commit
07ef4060a3
@ -0,0 +1,37 @@ |
||||
import React from 'react'; |
||||
import { action } from '@storybook/addon-actions'; |
||||
import { UseState } from '../../../utils/storybook/UseState'; |
||||
import { withCenteredStory } from '../../../utils/storybook/withCenteredStory'; |
||||
import { RelativeTimeRangePicker } from './RelativeTimeRangePicker'; |
||||
|
||||
export default { |
||||
title: 'Pickers and Editors/TimePickers/RelativeTimeRangePicker', |
||||
component: RelativeTimeRangePicker, |
||||
decorators: [withCenteredStory], |
||||
parameters: { |
||||
docs: {}, |
||||
}, |
||||
}; |
||||
|
||||
export const basic = () => { |
||||
return ( |
||||
<UseState |
||||
initialState={{ |
||||
from: 900, |
||||
to: 0, |
||||
}} |
||||
> |
||||
{(value, updateValue) => { |
||||
return ( |
||||
<RelativeTimeRangePicker |
||||
onChange={(newValue) => { |
||||
action('on selected')(newValue); |
||||
updateValue(newValue); |
||||
}} |
||||
timeRange={value} |
||||
/> |
||||
); |
||||
}} |
||||
</UseState> |
||||
); |
||||
}; |
||||
@ -0,0 +1,47 @@ |
||||
import React, { useState } from 'react'; |
||||
import { render, fireEvent, RenderResult } from '@testing-library/react'; |
||||
import { RelativeTimeRangePicker } from './RelativeTimeRangePicker'; |
||||
import { RelativeTimeRange } from '@grafana/data'; |
||||
|
||||
function setup(initial: RelativeTimeRange = { from: 900, to: 0 }): RenderResult { |
||||
const StatefulPicker: React.FC<{}> = () => { |
||||
const [value, setValue] = useState<RelativeTimeRange>(initial); |
||||
return <RelativeTimeRangePicker timeRange={value} onChange={setValue} />; |
||||
}; |
||||
|
||||
return render(<StatefulPicker />); |
||||
} |
||||
|
||||
describe('RelativeTimePicker', () => { |
||||
it('should render the picker button with an user friendly text', () => { |
||||
const { getByText } = setup({ from: 900, to: 0 }); |
||||
expect(getByText('now-15m to now')).toBeInTheDocument(); |
||||
}); |
||||
|
||||
it('should open the picker when clicking the button', () => { |
||||
const { getByText } = setup({ from: 900, to: 0 }); |
||||
|
||||
fireEvent.click(getByText('now-15m to now')); |
||||
|
||||
expect(getByText('Specify time range')).toBeInTheDocument(); |
||||
expect(getByText('Example time ranges')).toBeInTheDocument(); |
||||
}); |
||||
|
||||
it('should not have open picker without clicking the button', () => { |
||||
const { queryByText } = setup({ from: 900, to: 0 }); |
||||
expect(queryByText('Specify time range')).toBeNull(); |
||||
expect(queryByText('Example time ranges')).toBeNull(); |
||||
}); |
||||
|
||||
it('should not be able to apply range via quick options', () => { |
||||
const { getByText, queryByText } = setup({ from: 900, to: 0 }); |
||||
|
||||
fireEvent.click(getByText('now-15m to now')); // open the picker
|
||||
fireEvent.click(getByText('Last 30 minutes')); // select the quick range, should close picker.
|
||||
|
||||
expect(queryByText('Specify time range')).toBeNull(); |
||||
expect(queryByText('Example time ranges')).toBeNull(); |
||||
|
||||
expect(getByText('now-30m to now')).toBeInTheDocument(); // new text on picker button
|
||||
}); |
||||
}); |
||||
@ -0,0 +1,191 @@ |
||||
import React, { FormEvent, ReactElement, useCallback, useState } from 'react'; |
||||
import { css } from '@emotion/css'; |
||||
import { RelativeTimeRange, GrafanaTheme2, TimeOption } from '@grafana/data'; |
||||
import { Tooltip } from '../../Tooltip/Tooltip'; |
||||
import { useStyles2 } from '../../../themes'; |
||||
import { Button, ButtonGroup, ToolbarButton } from '../../Button'; |
||||
import { ClickOutsideWrapper } from '../../ClickOutsideWrapper/ClickOutsideWrapper'; |
||||
import { TimeRangeList } from '../TimeRangePicker/TimeRangeList'; |
||||
import { quickOptions } from '../rangeOptions'; |
||||
import CustomScrollbar from '../../CustomScrollbar/CustomScrollbar'; |
||||
import { TimePickerTitle } from '../TimeRangePicker/TimePickerTitle'; |
||||
import { isRangeValid, isRelativeFormat, mapOptionToRelativeTimeRange, mapRelativeTimeRangeToOption } from './utils'; |
||||
import { Field } from '../../Forms/Field'; |
||||
import { Input } from '../../Input/Input'; |
||||
import { InputState } from '../TimeRangePicker/TimeRangeForm'; |
||||
import { Icon } from '../../Icon/Icon'; |
||||
|
||||
/** |
||||
* @internal |
||||
*/ |
||||
export interface RelativeTimeRangePickerProps { |
||||
timeRange: RelativeTimeRange; |
||||
onChange: (timeRange: RelativeTimeRange) => void; |
||||
} |
||||
|
||||
const errorMessage = 'Value not in relative time format.'; |
||||
const validOptions = quickOptions.filter((o) => isRelativeFormat(o.from)); |
||||
|
||||
/** |
||||
* @internal |
||||
*/ |
||||
export function RelativeTimeRangePicker(props: RelativeTimeRangePickerProps): ReactElement | null { |
||||
const { timeRange, onChange } = props; |
||||
const [isOpen, setIsOpen] = useState(false); |
||||
const onClose = useCallback(() => setIsOpen(false), []); |
||||
const timeOption = mapRelativeTimeRangeToOption(timeRange); |
||||
const [from, setFrom] = useState<InputState>({ value: timeOption.from, invalid: !isRangeValid(timeOption.from) }); |
||||
const [to, setTo] = useState<InputState>({ value: timeOption.to, invalid: !isRangeValid(timeOption.to) }); |
||||
|
||||
const styles = useStyles2(getStyles(from.invalid, to.invalid)); |
||||
|
||||
const onChangeTimeOption = (option: TimeOption) => { |
||||
const relativeTimeRange = mapOptionToRelativeTimeRange(option); |
||||
if (!relativeTimeRange) { |
||||
return; |
||||
} |
||||
onClose(); |
||||
setFrom({ ...from, value: option.from }); |
||||
setTo({ ...to, value: option.to }); |
||||
onChange(relativeTimeRange); |
||||
}; |
||||
|
||||
const onOpen = useCallback( |
||||
(event: FormEvent<HTMLButtonElement>) => { |
||||
event.stopPropagation(); |
||||
event.preventDefault(); |
||||
setIsOpen(!isOpen); |
||||
}, |
||||
[isOpen] |
||||
); |
||||
|
||||
const onApply = (event: FormEvent<HTMLButtonElement>) => { |
||||
event.preventDefault(); |
||||
|
||||
if (to.invalid || from.invalid) { |
||||
return; |
||||
} |
||||
|
||||
const timeRange = mapOptionToRelativeTimeRange({ |
||||
from: from.value, |
||||
to: to.value, |
||||
display: '', |
||||
}); |
||||
|
||||
if (!timeRange) { |
||||
return; |
||||
} |
||||
|
||||
onChange(timeRange); |
||||
setIsOpen(false); |
||||
}; |
||||
|
||||
return ( |
||||
<ButtonGroup className={styles.container}> |
||||
<Tooltip content="Choose time range" placement="bottom"> |
||||
<ToolbarButton aria-label="TimePicker Open Button" onClick={onOpen} icon="clock-nine" isOpen={isOpen}> |
||||
<span data-testid="picker-button-label" className={styles.container}> |
||||
{timeOption.from} to {timeOption.to} |
||||
</span> |
||||
</ToolbarButton> |
||||
</Tooltip> |
||||
{isOpen && ( |
||||
<ClickOutsideWrapper includeButtonPress={false} onClick={onClose}> |
||||
<div className={styles.content}> |
||||
<div className={styles.body}> |
||||
<CustomScrollbar className={styles.leftSide} hideHorizontalTrack> |
||||
<TimeRangeList |
||||
title="Example time ranges" |
||||
options={validOptions} |
||||
onChange={onChangeTimeOption} |
||||
value={timeOption} |
||||
/> |
||||
</CustomScrollbar> |
||||
<div className={styles.rightSide}> |
||||
<div className={styles.title}> |
||||
<TimePickerTitle>Specify time range</TimePickerTitle> |
||||
<div className={styles.description}> |
||||
Specify a relative time range, for more information see{' '} |
||||
<a href="https://grafana.com/docs/grafana/latest/dashboards/time-range-controls/"> |
||||
docs <Icon name="external-link-alt" /> |
||||
</a> |
||||
. |
||||
</div> |
||||
</div> |
||||
<Field label="From" invalid={from.invalid} error={errorMessage}> |
||||
<Input |
||||
onClick={(event) => event.stopPropagation()} |
||||
onBlur={() => setFrom({ ...from, invalid: !isRangeValid(from.value) })} |
||||
onChange={(event) => setFrom({ ...from, value: event.currentTarget.value })} |
||||
value={from.value} |
||||
/> |
||||
</Field> |
||||
<Field label="To" invalid={to.invalid} error={errorMessage}> |
||||
<Input |
||||
onClick={(event) => event.stopPropagation()} |
||||
onBlur={() => setTo({ ...to, invalid: !isRangeValid(to.value) })} |
||||
onChange={(event) => setTo({ ...to, value: event.currentTarget.value })} |
||||
value={to.value} |
||||
/> |
||||
</Field> |
||||
<Button aria-label="TimePicker submit button" onClick={onApply}> |
||||
Apply time range |
||||
</Button> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</ClickOutsideWrapper> |
||||
)} |
||||
</ButtonGroup> |
||||
); |
||||
} |
||||
|
||||
const getStyles = (fromInvalid: boolean, toInvalid: boolean) => (theme: GrafanaTheme2) => { |
||||
let bodyHeight = 250; |
||||
const errorHeight = theme.spacing.gridSize * 4; |
||||
|
||||
if (fromInvalid && toInvalid) { |
||||
bodyHeight += errorHeight * 2; |
||||
} else if (fromInvalid || toInvalid) { |
||||
bodyHeight += errorHeight; |
||||
} |
||||
|
||||
return { |
||||
container: css` |
||||
position: relative; |
||||
display: flex; |
||||
vertical-align: middle; |
||||
`,
|
||||
content: css` |
||||
background: ${theme.colors.background.primary}; |
||||
box-shadow: ${theme.shadows.z3}; |
||||
position: absolute; |
||||
z-index: ${theme.zIndex.dropdown}; |
||||
width: 500px; |
||||
top: 116%; |
||||
border-radius: 2px; |
||||
border: 1px solid ${theme.colors.border.weak}; |
||||
left: 0; |
||||
white-space: normal; |
||||
`,
|
||||
body: css` |
||||
display: flex; |
||||
height: ${bodyHeight}px; |
||||
`,
|
||||
description: css` |
||||
color: ${theme.colors.text.secondary}; |
||||
font-size: ${theme.typography.size.sm}; |
||||
`,
|
||||
leftSide: css` |
||||
width: 50% !important; |
||||
border-right: 1px solid ${theme.colors.border.medium}; |
||||
`,
|
||||
rightSide: css` |
||||
width: 50%; |
||||
padding: ${theme.spacing(1)}; |
||||
`,
|
||||
title: css` |
||||
margin-bottom: ${theme.spacing(1)}; |
||||
`,
|
||||
}; |
||||
}; |
||||
@ -0,0 +1,108 @@ |
||||
import { isRangeValid, isRelativeFormat, mapOptionToRelativeTimeRange, mapRelativeTimeRangeToOption } from './utils'; |
||||
|
||||
describe('utils', () => { |
||||
describe('mapRelativeTimeRangeToOption', () => { |
||||
it('should map relative time range from minutes to time option', () => { |
||||
const relativeTimeRange = { from: 600, to: 0 }; |
||||
const timeOption = mapRelativeTimeRangeToOption(relativeTimeRange); |
||||
|
||||
expect(timeOption).toEqual({ from: 'now-10m', to: 'now', display: 'now-10m to now' }); |
||||
}); |
||||
|
||||
it('should map relative time range from one hour to time option', () => { |
||||
const relativeTimeRange = { from: 3600, to: 0 }; |
||||
const timeOption = mapRelativeTimeRangeToOption(relativeTimeRange); |
||||
|
||||
expect(timeOption).toEqual({ from: 'now-1h', to: 'now', display: 'now-1h to now' }); |
||||
}); |
||||
|
||||
it('should map relative time range from hours to time option', () => { |
||||
const relativeTimeRange = { from: 7200, to: 0 }; |
||||
const timeOption = mapRelativeTimeRangeToOption(relativeTimeRange); |
||||
|
||||
expect(timeOption).toEqual({ from: 'now-2h', to: 'now', display: 'now-2h to now' }); |
||||
}); |
||||
|
||||
it('should handle two relative ranges', () => { |
||||
const relativeTimeRange = { from: 600, to: 300 }; |
||||
const timeOption = mapRelativeTimeRangeToOption(relativeTimeRange); |
||||
|
||||
expect(timeOption).toEqual({ from: 'now-10m', to: 'now-5m', display: 'now-10m to now-5m' }); |
||||
}); |
||||
|
||||
it('should handle two relative ranges with single/multiple units', () => { |
||||
const relativeTimeRange = { from: 6000, to: 300 }; |
||||
const timeOption = mapRelativeTimeRangeToOption(relativeTimeRange); |
||||
|
||||
expect(timeOption).toEqual({ |
||||
from: 'now-100m', |
||||
to: 'now-5m', |
||||
display: 'now-100m to now-5m', |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('mapOptionToRelativeTimeRange', () => { |
||||
it('should map simple case', () => { |
||||
const timeOption = { from: 'now-10m', to: 'now', display: 'asdfasdf' }; |
||||
const relativeTimeRange = mapOptionToRelativeTimeRange(timeOption); |
||||
|
||||
expect(relativeTimeRange).toEqual({ from: 600, to: 0 }); |
||||
}); |
||||
|
||||
it('should map advanced case', () => { |
||||
const timeOption = { from: 'now-1d', to: 'now-12h', display: 'asdfasdf' }; |
||||
const relativeTimeRange = mapOptionToRelativeTimeRange(timeOption); |
||||
|
||||
expect(relativeTimeRange).toEqual({ from: 86400, to: 43200 }); |
||||
}); |
||||
}); |
||||
|
||||
describe('isRelativeFormat', () => { |
||||
it('should consider now as a relative format', () => { |
||||
expect(isRelativeFormat('now')).toBe(true); |
||||
}); |
||||
|
||||
it('should consider now-10s as a relative format', () => { |
||||
expect(isRelativeFormat('now-10s')).toBe(true); |
||||
}); |
||||
|
||||
it('should consider now-2000m as a relative format', () => { |
||||
expect(isRelativeFormat('now-2000m')).toBe(true); |
||||
}); |
||||
|
||||
it('should consider now-112334h as a relative format', () => { |
||||
expect(isRelativeFormat('now-112334h')).toBe(true); |
||||
}); |
||||
|
||||
it('should consider now-12d as a relative format', () => { |
||||
expect(isRelativeFormat('now-12d')).toBe(true); |
||||
}); |
||||
|
||||
it('should consider now-53w as a relative format', () => { |
||||
expect(isRelativeFormat('now-53w')).toBe(true); |
||||
}); |
||||
|
||||
it('should consider 123123123 as a relative format', () => { |
||||
expect(isRelativeFormat('123123123')).toBe(false); |
||||
}); |
||||
}); |
||||
|
||||
describe('isRangeValid', () => { |
||||
it('should consider now as a valid relative format', () => { |
||||
expect(isRangeValid('now')).toBe(true); |
||||
}); |
||||
|
||||
it('should consider now-90d as a valid relative format', () => { |
||||
expect(isRangeValid('now-90d')).toBe(true); |
||||
}); |
||||
|
||||
it('should consider now-90000000d as an invalid relative format', () => { |
||||
expect(isRangeValid('now-90000000d')).toBe(false); |
||||
}); |
||||
|
||||
it('should consider now-11111111111s as an invalid relative format', () => { |
||||
expect(isRangeValid('now-11111111111s')).toBe(false); |
||||
}); |
||||
}); |
||||
}); |
||||
@ -0,0 +1,83 @@ |
||||
import { RelativeTimeRange, TimeOption } from '@grafana/data'; |
||||
|
||||
const regex = /^now$|^now\-(\d{1,10})([wdhms])$/; |
||||
|
||||
export const mapOptionToRelativeTimeRange = (option: TimeOption): RelativeTimeRange | undefined => { |
||||
return { |
||||
from: relativeToSeconds(option.from), |
||||
to: relativeToSeconds(option.to), |
||||
}; |
||||
}; |
||||
|
||||
export const mapRelativeTimeRangeToOption = (range: RelativeTimeRange): TimeOption => { |
||||
const from = secondsToRelativeFormat(range.from); |
||||
const to = secondsToRelativeFormat(range.to); |
||||
|
||||
return { from, to, display: `${from} to ${to}` }; |
||||
}; |
||||
|
||||
export const isRangeValid = (relative: string, now = Date.now()): boolean => { |
||||
if (!isRelativeFormat(relative)) { |
||||
return false; |
||||
} |
||||
|
||||
const seconds = relativeToSeconds(relative); |
||||
|
||||
if (seconds > Math.ceil(now / 1000)) { |
||||
return false; |
||||
} |
||||
return true; |
||||
}; |
||||
|
||||
export const isRelativeFormat = (format: string): boolean => { |
||||
return regex.test(format); |
||||
}; |
||||
|
||||
const relativeToSeconds = (relative: string): number => { |
||||
const match = regex.exec(relative); |
||||
|
||||
if (!match || match.length !== 3) { |
||||
return 0; |
||||
} |
||||
|
||||
const [, value, unit] = match; |
||||
const parsed = parseInt(value, 10); |
||||
|
||||
if (isNaN(parsed)) { |
||||
return 0; |
||||
} |
||||
|
||||
return parsed * units[unit]; |
||||
}; |
||||
|
||||
const units: Record<string, number> = { |
||||
w: 604800, |
||||
d: 86400, |
||||
h: 3600, |
||||
m: 60, |
||||
s: 1, |
||||
}; |
||||
|
||||
const secondsToRelativeFormat = (seconds: number): string => { |
||||
if (seconds <= 0) { |
||||
return 'now'; |
||||
} |
||||
|
||||
if (seconds >= units.w && seconds % units.w === 0) { |
||||
return `now-${seconds / units.w}w`; |
||||
} |
||||
|
||||
if (seconds >= units.d && seconds % units.d === 0) { |
||||
return `now-${seconds / units.d}d`; |
||||
} |
||||
|
||||
if (seconds >= units.h && seconds % units.h === 0) { |
||||
return `now-${seconds / units.h}h`; |
||||
} |
||||
|
||||
if (seconds >= units.m && seconds % units.m === 0) { |
||||
return `now-${seconds / units.m}m`; |
||||
} |
||||
|
||||
return `now-${seconds}s`; |
||||
}; |
||||
@ -1,37 +1,37 @@ |
||||
import { TimeOption } from '@grafana/data'; |
||||
|
||||
export const quickOptions: TimeOption[] = [ |
||||
{ from: 'now-5m', to: 'now', display: 'Last 5 minutes', section: 3 }, |
||||
{ from: 'now-15m', to: 'now', display: 'Last 15 minutes', section: 3 }, |
||||
{ from: 'now-30m', to: 'now', display: 'Last 30 minutes', section: 3 }, |
||||
{ from: 'now-1h', to: 'now', display: 'Last 1 hour', section: 3 }, |
||||
{ from: 'now-3h', to: 'now', display: 'Last 3 hours', section: 3 }, |
||||
{ from: 'now-6h', to: 'now', display: 'Last 6 hours', section: 3 }, |
||||
{ from: 'now-12h', to: 'now', display: 'Last 12 hours', section: 3 }, |
||||
{ from: 'now-24h', to: 'now', display: 'Last 24 hours', section: 3 }, |
||||
{ from: 'now-2d', to: 'now', display: 'Last 2 days', section: 3 }, |
||||
{ from: 'now-7d', to: 'now', display: 'Last 7 days', section: 3 }, |
||||
{ from: 'now-30d', to: 'now', display: 'Last 30 days', section: 3 }, |
||||
{ from: 'now-90d', to: 'now', display: 'Last 90 days', section: 3 }, |
||||
{ from: 'now-6M', to: 'now', display: 'Last 6 months', section: 3 }, |
||||
{ from: 'now-1y', to: 'now', display: 'Last 1 year', section: 3 }, |
||||
{ from: 'now-2y', to: 'now', display: 'Last 2 years', section: 3 }, |
||||
{ from: 'now-5y', to: 'now', display: 'Last 5 years', section: 3 }, |
||||
{ from: 'now-5m', to: 'now', display: 'Last 5 minutes' }, |
||||
{ from: 'now-15m', to: 'now', display: 'Last 15 minutes' }, |
||||
{ from: 'now-30m', to: 'now', display: 'Last 30 minutes' }, |
||||
{ from: 'now-1h', to: 'now', display: 'Last 1 hour' }, |
||||
{ from: 'now-3h', to: 'now', display: 'Last 3 hours' }, |
||||
{ from: 'now-6h', to: 'now', display: 'Last 6 hours' }, |
||||
{ from: 'now-12h', to: 'now', display: 'Last 12 hours' }, |
||||
{ from: 'now-24h', to: 'now', display: 'Last 24 hours' }, |
||||
{ from: 'now-2d', to: 'now', display: 'Last 2 days' }, |
||||
{ from: 'now-7d', to: 'now', display: 'Last 7 days' }, |
||||
{ from: 'now-30d', to: 'now', display: 'Last 30 days' }, |
||||
{ from: 'now-90d', to: 'now', display: 'Last 90 days' }, |
||||
{ from: 'now-6M', to: 'now', display: 'Last 6 months' }, |
||||
{ from: 'now-1y', to: 'now', display: 'Last 1 year' }, |
||||
{ from: 'now-2y', to: 'now', display: 'Last 2 years' }, |
||||
{ from: 'now-5y', to: 'now', display: 'Last 5 years' }, |
||||
]; |
||||
|
||||
export const otherOptions: TimeOption[] = [ |
||||
{ from: 'now-1d/d', to: 'now-1d/d', display: 'Yesterday', section: 3 }, |
||||
{ from: 'now-2d/d', to: 'now-2d/d', display: 'Day before yesterday', section: 3 }, |
||||
{ from: 'now-7d/d', to: 'now-7d/d', display: 'This day last week', section: 3 }, |
||||
{ from: 'now-1w/w', to: 'now-1w/w', display: 'Previous week', section: 3 }, |
||||
{ from: 'now-1M/M', to: 'now-1M/M', display: 'Previous month', section: 3 }, |
||||
{ from: 'now-1y/y', to: 'now-1y/y', display: 'Previous year', section: 3 }, |
||||
{ from: 'now/d', to: 'now/d', display: 'Today', section: 3 }, |
||||
{ from: 'now/d', to: 'now', display: 'Today so far', section: 3 }, |
||||
{ from: 'now/w', to: 'now/w', display: 'This week', section: 3 }, |
||||
{ from: 'now/w', to: 'now', display: 'This week so far', section: 3 }, |
||||
{ from: 'now/M', to: 'now/M', display: 'This month', section: 3 }, |
||||
{ from: 'now/M', to: 'now', display: 'This month so far', section: 3 }, |
||||
{ from: 'now/y', to: 'now/y', display: 'This year', section: 3 }, |
||||
{ from: 'now/y', to: 'now', display: 'This year so far', section: 3 }, |
||||
{ from: 'now-1d/d', to: 'now-1d/d', display: 'Yesterday' }, |
||||
{ from: 'now-2d/d', to: 'now-2d/d', display: 'Day before yesterday' }, |
||||
{ from: 'now-7d/d', to: 'now-7d/d', display: 'This day last week' }, |
||||
{ from: 'now-1w/w', to: 'now-1w/w', display: 'Previous week' }, |
||||
{ from: 'now-1M/M', to: 'now-1M/M', display: 'Previous month' }, |
||||
{ from: 'now-1y/y', to: 'now-1y/y', display: 'Previous year' }, |
||||
{ from: 'now/d', to: 'now/d', display: 'Today' }, |
||||
{ from: 'now/d', to: 'now', display: 'Today so far' }, |
||||
{ from: 'now/w', to: 'now/w', display: 'This week' }, |
||||
{ from: 'now/w', to: 'now', display: 'This week so far' }, |
||||
{ from: 'now/M', to: 'now/M', display: 'This month' }, |
||||
{ from: 'now/M', to: 'now', display: 'This month so far' }, |
||||
{ from: 'now/y', to: 'now/y', display: 'This year' }, |
||||
{ from: 'now/y', to: 'now', display: 'This year so far' }, |
||||
]; |
||||
|
||||
Loading…
Reference in new issue