Commit 718d6fb2 by Alex Khomenko Committed by GitHub

Grafana-UI: Add time range input (#26158)

* Grafana UI: Do not submit form on range change

* Grafana UI: Add TimeRangeInput

* Grafana UI: Style input

* Grafana UI: Customize content

* Grafana UI: Adjust caret style

* Grafana UI: Add mdx

* Grafana UI: Fix caret styles

* Grafana UI: Fix typo

* Grafana UI: Do not reload page on timerange change

* Grafana UI: Sync TimeRangeForm state with external value

* Grafana UI: Close overlay on apply

* Grafana UI: Remove unused props

* Grafana UI: Fix story

* Grafana-UI: Make time zone optional

* Grafana-UI: Update styles

* Grafana-UI: Extract button label props

* Grafana-UI: hideHistory => showHistory

* Grafana-UI: Fix caret styles
parent 8f78b0e7
......@@ -203,7 +203,7 @@ export const isValidTimeSpan = (value: string) => {
return info.invalid !== true;
};
export const describeTimeRangeAbbrevation = (range: TimeRange, timeZone?: TimeZone) => {
export const describeTimeRangeAbbreviation = (range: TimeRange, timeZone?: TimeZone) => {
if (isDateTime(range.from)) {
return timeZoneAbbrevation(range.from, { timeZone });
}
......
......@@ -30,7 +30,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
transform: translateY(-50%);
display: inline-block;
text-align: right;
z-index: 1071;
color: ${theme.colors.textWeak};
`,
picker: css`
.rc-time-picker-panel-select {
......
import { Story, Preview, Props } from '@storybook/addon-docs/blocks';
import { TimeRangeInput } from './TimeRangeInput';
# TimeRangeInput
A variant of `TimeRangePicker` for use in forms.
### Usage
```jsx
import { TimeRangeInput } from '@grafana/ui';
<TimeRangeInput
value={timeRange}
onChange={range => console.log('range', range)}
onChangeTimeZone={tz => console.log('timezone', tz)}
/>
```
### Props
<Props of={TimeRangeInput} />
import React from 'react';
import { action } from '@storybook/addon-actions';
import { dateTime, TimeFragment } from '@grafana/data';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { UseState } from '../../utils/storybook/UseState';
import { TimeRangeInput } from './TimeRangeInput';
import mdx from './TimeRangeInput.mdx';
export default {
title: 'Pickers and Editors/TimePickers/TimeRangeInput',
component: TimeRangeInput,
decorators: [withCenteredStory],
parameters: {
docs: {
page: mdx,
},
},
};
export const basic = () => {
return (
<UseState
initialState={{
from: dateTime(),
to: dateTime(),
raw: { from: 'now-6h' as TimeFragment, to: 'now' as TimeFragment },
}}
>
{(value, updateValue) => {
return (
<TimeRangeInput
onChangeTimeZone={tz => action('onTimeZoneChange fired')(tz)}
timeZone="browser"
value={value}
onChange={timeRange => {
action('onChange fired')(timeRange);
updateValue(timeRange);
}}
/>
);
}}
</UseState>
);
};
import React, { FC, FormEvent, useState } from 'react';
import { css, cx } from 'emotion';
import { GrafanaTheme, TimeRange, TimeZone } from '@grafana/data';
import { useStyles } from '../../themes/ThemeContext';
import { ClickOutsideWrapper } from '../ClickOutsideWrapper/ClickOutsideWrapper';
import { Icon } from '../Icon/Icon';
import { getInputStyles } from '../Input/Input';
import { getFocusStyle } from '../Forms/commonStyles';
import { TimePickerButtonLabel } from './TimeRangePicker';
import { TimePickerContent } from './TimeRangePicker/TimePickerContent';
import { otherOptions, quickOptions } from './rangeOptions';
export interface Props {
value: TimeRange;
timeZone?: TimeZone;
onChange: (timeRange: TimeRange) => void;
onChangeTimeZone?: (timeZone: TimeZone) => void;
hideTimeZone?: boolean;
}
const noop = () => {};
export const TimeRangeInput: FC<Props> = ({
value,
onChange,
onChangeTimeZone,
hideTimeZone = true,
timeZone = 'browser',
}) => {
const [isOpen, setIsOpen] = useState(false);
const styles = useStyles(getStyles);
const onOpen = (event: FormEvent<HTMLDivElement>) => {
event.stopPropagation();
event.preventDefault();
setIsOpen(!isOpen);
};
const onClose = () => {
setIsOpen(false);
};
const onRangeChange = (timeRange: TimeRange) => {
onClose();
onChange(timeRange);
};
return (
<div className={styles.container}>
<div tabIndex={0} className={styles.pickerInput} aria-label="TimePicker Open Button" onClick={onOpen}>
<TimePickerButtonLabel value={value} />
<span className={styles.caretIcon}>
<Icon name={isOpen ? 'angle-up' : 'angle-down'} size="lg" />
</span>
</div>
{isOpen && (
<ClickOutsideWrapper includeButtonPress={false} onClick={onClose}>
<TimePickerContent
timeZone={timeZone}
value={value}
onChange={onRangeChange}
otherOptions={otherOptions}
quickOptions={quickOptions}
onChangeTimeZone={onChangeTimeZone || noop}
className={styles.content}
hideTimeZone={hideTimeZone}
/>
</ClickOutsideWrapper>
)}
</div>
);
};
const getStyles = (theme: GrafanaTheme) => {
const inputStyles = getInputStyles({ theme, invalid: false });
return {
container: css`
display: flex;
position: relative;
`,
content: css`
margin-left: 0;
`,
pickerInput: cx(
inputStyles.input,
inputStyles.wrapper,
css`
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
padding-right: 0;
${getFocusStyle(theme)};
`
),
caretIcon: cx(
inputStyles.suffix,
css`
position: relative;
margin-left: ${theme.spacing.xs};
`
),
};
};
......@@ -14,44 +14,9 @@ import { withTheme, useTheme } from '../../themes/ThemeContext';
// Types
import { isDateTime, rangeUtil, GrafanaTheme, dateTimeFormat, timeZoneFormatUserFriendly } from '@grafana/data';
import { TimeRange, TimeOption, TimeZone, dateMath } from '@grafana/data';
import { TimeRange, TimeZone, dateMath } from '@grafana/data';
import { Themeable } from '../../types';
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 },
];
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 },
];
import { otherOptions, quickOptions } from './rangeOptions';
const getStyles = stylesFactory((theme: GrafanaTheme) => {
return {
......@@ -122,6 +87,7 @@ export class UnthemedTimeRangePicker extends PureComponent<Props, State> {
onOpen = (event: FormEvent<HTMLButtonElement>) => {
const { isOpen } = this.state;
event.stopPropagation();
event.preventDefault();
this.setState({ isOpen: !isOpen });
};
......@@ -178,6 +144,7 @@ export class UnthemedTimeRangePicker extends PureComponent<Props, State> {
otherOptions={otherOptions}
quickOptions={quickOptions}
history={history}
showHistory
onChangeTimeZone={onChangeTimeZone}
/>
</ClickOutsideWrapper>
......@@ -225,7 +192,9 @@ const TimePickerTooltip = ({ timeRange, timeZone }: { timeRange: TimeRange; time
);
};
const TimePickerButtonLabel = memo<Props>(({ hideText, value, timeZone }) => {
type LabelProps = Pick<Props, 'hideText' | 'value' | 'timeZone'>;
export const TimePickerButtonLabel = memo<LabelProps>(({ hideText, value, timeZone }) => {
const theme = useTheme();
const styles = getLabelStyles(theme);
......@@ -236,7 +205,7 @@ const TimePickerButtonLabel = memo<Props>(({ hideText, value, timeZone }) => {
return (
<span className={styles.container}>
<span>{formattedRange(value, timeZone)}</span>
<span className={styles.utc}>{rangeUtil.describeTimeRangeAbbrevation(value, timeZone)}</span>
<span className={styles.utc}>{rangeUtil.describeTimeRangeAbbreviation(value, timeZone)}</span>
</span>
);
});
......
import React, { memo, useState, useEffect, useCallback } from 'react';
import React, { memo, useState, useEffect, useCallback, FormEvent } from 'react';
import { css } from 'emotion';
import Calendar from 'react-calendar/dist/entry.nostyle';
import { GrafanaTheme, DateTime, TimeZone, dateTimeParse } from '@grafana/data';
......@@ -189,7 +189,7 @@ interface Props {
from: DateTime;
to: DateTime;
onClose: () => void;
onApply: () => void;
onApply: (e: FormEvent<HTMLButtonElement>) => void;
onChange: (from: DateTime, to: DateTime) => void;
isFullscreen: boolean;
timeZone?: TimeZone;
......
import { GrafanaTheme, isDateTime, TimeOption, TimeRange, TimeZone } from '@grafana/data';
import { css } from 'emotion';
import { css, cx } from 'emotion';
import React, { memo, useState } from 'react';
import { useMedia } from 'react-use';
import { stylesFactory, useTheme } from '../../../themes';
......@@ -134,6 +134,9 @@ interface Props {
quickOptions?: TimeOption[];
otherOptions?: TimeOption[];
history?: TimeRange[];
showHistory?: boolean;
className?: string;
hideTimeZone?: boolean;
}
interface PropsWithScreenSize extends Props {
......@@ -152,7 +155,7 @@ export const TimePickerContentWithScreenSize: React.FC<PropsWithScreenSize> = pr
const { quickOptions = [], otherOptions = [], isFullscreen } = props;
return (
<div className={styles.container}>
<div className={cx(styles.container, props.className)}>
<div className={styles.body}>
<div className={styles.leftSide}>
<FullScreenForm {...props} visible={isFullscreen} historyOptions={historyOptions} />
......@@ -176,7 +179,9 @@ export const TimePickerContentWithScreenSize: React.FC<PropsWithScreenSize> = pr
/>
</CustomScrollbar>
</div>
{isFullscreen && <TimePickerFooter timeZone={props.timeZone} onChangeTimeZone={props.onChangeTimeZone} />}
{!props.hideTimeZone && isFullscreen && (
<TimePickerFooter timeZone={props.timeZone} onChangeTimeZone={props.onChangeTimeZone} />
)}
</div>
);
};
......@@ -218,14 +223,16 @@ const NarrowScreenForm: React.FC<FormProps> = props => {
isFullscreen={false}
/>
</div>
<TimeRangeList
title="Recently used absolute ranges"
options={props.historyOptions || []}
onSelect={props.onChange}
value={props.value}
placeholderEmpty={null}
timeZone={props.timeZone}
/>
{props.showHistory && (
<TimeRangeList
title="Recently used absolute ranges"
options={props.historyOptions || []}
onSelect={props.onChange}
value={props.value}
placeholderEmpty={null}
timeZone={props.timeZone}
/>
)}
</div>
)}
</>
......@@ -248,16 +255,18 @@ const FullScreenForm: React.FC<FormProps> = props => {
</div>
<TimeRangeForm value={props.value} timeZone={props.timeZone} onApply={props.onChange} isFullscreen={true} />
</div>
<div className={styles.recent}>
<TimeRangeList
title="Recently used absolute ranges"
options={props.historyOptions || []}
onSelect={props.onChange}
value={props.value}
placeholderEmpty={<EmptyRecentList />}
timeZone={props.timeZone}
/>
</div>
{props.showHistory && (
<div className={styles.recent}>
<TimeRangeList
title="Recently used absolute ranges"
options={props.historyOptions || []}
onSelect={props.onChange}
value={props.value}
placeholderEmpty={<EmptyRecentList />}
timeZone={props.timeZone}
/>
</div>
)}
</>
);
};
......
import React, { FormEvent, useState, useCallback } from 'react';
import React, { FormEvent, useState, useCallback, useEffect } from 'react';
import {
TimeZone,
isDateTime,
......@@ -37,6 +37,12 @@ export const TimeRangeForm: React.FC<Props> = props => {
const [to, setTo] = useState<InputState>(valueToState(value.raw.to, true, timeZone));
const [isOpen, setOpen] = useState(false);
// Synchronize internal state with external value
useEffect(() => {
setFrom(valueToState(value.raw.from, false, timeZone));
setTo(valueToState(value.raw.to, true, timeZone));
}, [value.raw.from, value.raw.to, timeZone]);
const onOpen = useCallback(
(event: FormEvent<HTMLElement>) => {
event.preventDefault();
......@@ -55,16 +61,20 @@ export const TimeRangeForm: React.FC<Props> = props => {
[isFullscreen, onOpen]
);
const onApply = useCallback(() => {
if (to.invalid || from.invalid) {
return;
}
const onApply = useCallback(
(e: FormEvent<HTMLButtonElement>) => {
e.preventDefault();
if (to.invalid || from.invalid) {
return;
}
const raw: RawTimeRange = { from: from.value, to: to.value };
const timeRange = rangeUtil.convertRawToRange(raw, timeZone);
const raw: RawTimeRange = { from: from.value, to: to.value };
const timeRange = rangeUtil.convertRawToRange(raw, timeZone);
props.onApply(timeRange);
}, [from, to, roundup, timeZone]);
props.onApply(timeRange);
},
[from, to, roundup, timeZone]
);
const onChange = useCallback(
(from: DateTime, to: DateTime) => {
......
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 },
];
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 },
];
......@@ -159,6 +159,7 @@ export { Checkbox } from './Forms/Checkbox';
export { TextArea } from './TextArea/TextArea';
export { FileUpload } from './FileUpload/FileUpload';
export { TimeRangeInput } from './TimePicker/TimeRangeInput';
// Legacy forms
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment