diff --git a/packages/grafana-data/src/datetime/rangeutil.ts b/packages/grafana-data/src/datetime/rangeutil.ts index cbdfb00e39d..c7cc0d84159 100644 --- a/packages/grafana-data/src/datetime/rangeutil.ts +++ b/packages/grafana-data/src/datetime/rangeutil.ts @@ -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 }); } diff --git a/packages/grafana-ui/src/components/TimePicker/TimeOfDayPicker.tsx b/packages/grafana-ui/src/components/TimePicker/TimeOfDayPicker.tsx index 6184be49039..20a0d92ad96 100644 --- a/packages/grafana-ui/src/components/TimePicker/TimeOfDayPicker.tsx +++ b/packages/grafana-ui/src/components/TimePicker/TimeOfDayPicker.tsx @@ -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 { diff --git a/packages/grafana-ui/src/components/TimePicker/TimeRangeInput.mdx b/packages/grafana-ui/src/components/TimePicker/TimeRangeInput.mdx new file mode 100644 index 00000000000..d5096ec9e67 --- /dev/null +++ b/packages/grafana-ui/src/components/TimePicker/TimeRangeInput.mdx @@ -0,0 +1,21 @@ +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'; + + console.log('range', range)} + onChangeTimeZone={tz => console.log('timezone', tz)} +/> +``` + +### Props + diff --git a/packages/grafana-ui/src/components/TimePicker/TimeRangeInput.story.tsx b/packages/grafana-ui/src/components/TimePicker/TimeRangeInput.story.tsx new file mode 100644 index 00000000000..f29ccde50b0 --- /dev/null +++ b/packages/grafana-ui/src/components/TimePicker/TimeRangeInput.story.tsx @@ -0,0 +1,44 @@ +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 ( + + {(value, updateValue) => { + return ( + action('onTimeZoneChange fired')(tz)} + timeZone="browser" + value={value} + onChange={timeRange => { + action('onChange fired')(timeRange); + updateValue(timeRange); + }} + /> + ); + }} + + ); +}; diff --git a/packages/grafana-ui/src/components/TimePicker/TimeRangeInput.tsx b/packages/grafana-ui/src/components/TimePicker/TimeRangeInput.tsx new file mode 100644 index 00000000000..7abb9165c3f --- /dev/null +++ b/packages/grafana-ui/src/components/TimePicker/TimeRangeInput.tsx @@ -0,0 +1,104 @@ +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 = ({ + value, + onChange, + onChangeTimeZone, + hideTimeZone = true, + timeZone = 'browser', +}) => { + const [isOpen, setIsOpen] = useState(false); + const styles = useStyles(getStyles); + + const onOpen = (event: FormEvent) => { + event.stopPropagation(); + event.preventDefault(); + setIsOpen(!isOpen); + }; + + const onClose = () => { + setIsOpen(false); + }; + + const onRangeChange = (timeRange: TimeRange) => { + onClose(); + onChange(timeRange); + }; + + return ( +
+
+ + + + +
+ {isOpen && ( + + + + )} +
+ ); +}; + +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}; + ` + ), + }; +}; diff --git a/packages/grafana-ui/src/components/TimePicker/TimeRangePicker.tsx b/packages/grafana-ui/src/components/TimePicker/TimeRangePicker.tsx index 371b55f5efe..becbd5f2e09 100644 --- a/packages/grafana-ui/src/components/TimePicker/TimeRangePicker.tsx +++ b/packages/grafana-ui/src/components/TimePicker/TimeRangePicker.tsx @@ -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 { onOpen = (event: FormEvent) => { const { isOpen } = this.state; event.stopPropagation(); + event.preventDefault(); this.setState({ isOpen: !isOpen }); }; @@ -178,6 +144,7 @@ export class UnthemedTimeRangePicker extends PureComponent { otherOptions={otherOptions} quickOptions={quickOptions} history={history} + showHistory onChangeTimeZone={onChangeTimeZone} /> @@ -225,7 +192,9 @@ const TimePickerTooltip = ({ timeRange, timeZone }: { timeRange: TimeRange; time ); }; -const TimePickerButtonLabel = memo(({ hideText, value, timeZone }) => { +type LabelProps = Pick; + +export const TimePickerButtonLabel = memo(({ hideText, value, timeZone }) => { const theme = useTheme(); const styles = getLabelStyles(theme); @@ -236,7 +205,7 @@ const TimePickerButtonLabel = memo(({ hideText, value, timeZone }) => { return ( {formattedRange(value, timeZone)} - {rangeUtil.describeTimeRangeAbbrevation(value, timeZone)} + {rangeUtil.describeTimeRangeAbbreviation(value, timeZone)} ); }); diff --git a/packages/grafana-ui/src/components/TimePicker/TimeRangePicker/TimePickerCalendar.tsx b/packages/grafana-ui/src/components/TimePicker/TimeRangePicker/TimePickerCalendar.tsx index 44e224693cd..6138bb16445 100644 --- a/packages/grafana-ui/src/components/TimePicker/TimeRangePicker/TimePickerCalendar.tsx +++ b/packages/grafana-ui/src/components/TimePicker/TimeRangePicker/TimePickerCalendar.tsx @@ -1,4 +1,4 @@ -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) => void; onChange: (from: DateTime, to: DateTime) => void; isFullscreen: boolean; timeZone?: TimeZone; diff --git a/packages/grafana-ui/src/components/TimePicker/TimeRangePicker/TimePickerContent.tsx b/packages/grafana-ui/src/components/TimePicker/TimeRangePicker/TimePickerContent.tsx index aa84f8fcb11..a640ff77c92 100644 --- a/packages/grafana-ui/src/components/TimePicker/TimeRangePicker/TimePickerContent.tsx +++ b/packages/grafana-ui/src/components/TimePicker/TimeRangePicker/TimePickerContent.tsx @@ -1,5 +1,5 @@ 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 = pr const { quickOptions = [], otherOptions = [], isFullscreen } = props; return ( -
+
@@ -176,7 +179,9 @@ export const TimePickerContentWithScreenSize: React.FC = pr />
- {isFullscreen && } + {!props.hideTimeZone && isFullscreen && ( + + )}
); }; @@ -218,14 +223,16 @@ const NarrowScreenForm: React.FC = props => { isFullscreen={false} />
- + {props.showHistory && ( + + )}
)} @@ -248,16 +255,18 @@ const FullScreenForm: React.FC = props => { -
- } - timeZone={props.timeZone} - /> -
+ {props.showHistory && ( +
+ } + timeZone={props.timeZone} + /> +
+ )} ); }; diff --git a/packages/grafana-ui/src/components/TimePicker/TimeRangePicker/TimeRangeForm.tsx b/packages/grafana-ui/src/components/TimePicker/TimeRangePicker/TimeRangeForm.tsx index 787ebb1c45c..b53f2fca263 100644 --- a/packages/grafana-ui/src/components/TimePicker/TimeRangePicker/TimeRangeForm.tsx +++ b/packages/grafana-ui/src/components/TimePicker/TimeRangePicker/TimeRangeForm.tsx @@ -1,4 +1,4 @@ -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 => { const [to, setTo] = useState(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) => { event.preventDefault(); @@ -55,16 +61,20 @@ export const TimeRangeForm: React.FC = props => { [isFullscreen, onOpen] ); - const onApply = useCallback(() => { - if (to.invalid || from.invalid) { - return; - } + const onApply = useCallback( + (e: FormEvent) => { + 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) => { diff --git a/packages/grafana-ui/src/components/TimePicker/rangeOptions.ts b/packages/grafana-ui/src/components/TimePicker/rangeOptions.ts new file mode 100644 index 00000000000..aea2b9ac03d --- /dev/null +++ b/packages/grafana-ui/src/components/TimePicker/rangeOptions.ts @@ -0,0 +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 }, +]; + +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 }, +]; diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index e71c8323323..5dfdff65e0b 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -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