mirror of https://github.com/grafana/grafana.git
				
				
				
			Dashboard: new updated time picker (#20931)
* Dashboard: started to implement new time picker. * TimePicker: working in full screen (except calendar). * TimePicker: first draft on narrow screen variant. * TimePicker: small adjustments to the narrow design. * TimePicker: enabled range selection and started to style calendar. * TimePicker: applied some more styling. * Calendar: added so the calendar range selection is styled properly. * Calendar: added styling for timepicker calendar in narrow screen. * TimePicker: made it possible to select range from calendar. * TimePicker: made the calendar have previous selected value. * TimePicker: moved calendar to be able to update form state. * TimePicker: calendar is now displayed onFocus or onClick. * TimePicker: calendar will be closed if click outside input. * Calendar: fixed the styling of the calendar in narrow screen. * Calendar: made it work properly with narrow screen. * TimePicker: connected recent to absolute time range. * TimePicker: changed the label on recent ranges. * TimePicker: cleaned up the range list and options. * TimePicker: some more cleaning up. * TimePicker: cleaned up the calendar a bit. * TimePicker: some more refactorings. * TimePicker: refactorings. * TimePicker: styled modal properly. * TimePicker: empty recent list. * TimePicker: width when calendar in full screen. * TimePicker: will validate input value. * TimePicker: removed unused code. * TimePicker: positioning with emotion instead of sass. * Calendar: Made sure we send the dates in the correct order to the calendar. * TimePicker: fixed theme. * TimePicker: fixed positioning of the content. * TimePicker: positioning of narrow. * TimePicker: added some simple tets. * TimePicker: fixed issue with invalid and added error message. * TimePicker: added history. * TimePicker: cleaned up snapshot data. * TimePicker: fixed so we keep the quick values in the input. * TimePicker: fixed the missing styling on UTC. * TimePicker: added missing caret icon. * TimePicker: fixed formatting on recent time ranges. * TimePicker: added missing -. * TimePicker: refactorings after feedback. * TimePicker: renamed reserved prop name. * TimePicker: added missing onChange call. * TimePicker: removed alternative return type. * TimePicker: fixed the sorting order on the recent list. * TimePicker: added useCallback for the onEvent functions. * TimePicker: moving away from default export. * TimePicker: used the isMathString instead of private function. * TimePicker: minor refactoring simplify the code. * TimePicker: Added empty container that will expand when less then 4 recent searches. * TimePicker: changed the top to be absolute relative to the container. * TimePicker: updated snapshots for failing tests. * Fixed shadow * Move it down a bit * added some more tests. * Fixed so we change the anchor point of the time picker in really small screens. * removed memo. * fixed snapshot. * Make sure that we always use the correct timeZone when formatting output. * Fixed form background. * Some minor fixes after demo. * Making sure that empty info box is centered. * updated snapshots for timepicker after css changes. * fixed so we don't overflow when input validation error. * adjusted final things on the time picker. Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
This commit is contained in:
		
							parent
							
								
									104c2e3636
								
							
						
					
					
						commit
						587e4009f3
					
				|  | @ -78,14 +78,14 @@ export const Field: React.FC<FieldProps> = ({ | |||
|       )} | ||||
|       <div> | ||||
|         {React.cloneElement(children, { invalid, disabled, loading })} | ||||
|         {error && !horizontal && ( | ||||
|         {invalid && error && !horizontal && ( | ||||
|           <div className={styles.fieldValidationWrapper}> | ||||
|             <FieldValidationMessage>{error}</FieldValidationMessage> | ||||
|           </div> | ||||
|         )} | ||||
|       </div> | ||||
| 
 | ||||
|       {error && horizontal && ( | ||||
|       {invalid && error && horizontal && ( | ||||
|         <div className={cx(styles.fieldValidationWrapper, styles.fieldValidationWrapperHorizontal)}> | ||||
|           <FieldValidationMessage>{error}</FieldValidationMessage> | ||||
|         </div> | ||||
|  |  | |||
|  | @ -115,7 +115,7 @@ export const getInputStyles = stylesFactory(({ theme, invalid = false }: StyleDe | |||
| 
 | ||||
|     input: cx( | ||||
|       getFocusStyle(theme), | ||||
|       sharedInputStyle(theme), | ||||
|       sharedInputStyle(theme, invalid), | ||||
|       css` | ||||
|         label: input-input; | ||||
|         position: relative; | ||||
|  | @ -211,8 +211,8 @@ export const Input: FC<Props> = props => { | |||
|    */ | ||||
|   const [prefixRect, prefixRef] = useClientRect<HTMLDivElement>(); | ||||
|   const [suffixRect, suffixRef] = useClientRect<HTMLDivElement>(); | ||||
|   const theme = useTheme(); | ||||
| 
 | ||||
|   const theme = useTheme(); | ||||
|   const styles = getInputStyles({ theme, invalid: !!invalid }); | ||||
| 
 | ||||
|   return ( | ||||
|  |  | |||
|  | @ -1,13 +1,17 @@ | |||
| import { getFormStyles } from './getFormStyles'; | ||||
| import { Label } from './Label'; | ||||
| import { Input } from './Input/Input'; | ||||
| import { Form } from './Form'; | ||||
| import { Field } from './Field'; | ||||
| import { Button } from './Button'; | ||||
| 
 | ||||
| const Forms = { | ||||
|   getFormStyles, | ||||
|   Label: Label, | ||||
|   Input: Input, | ||||
|   Button: Button, | ||||
|   Label, | ||||
|   Input, | ||||
|   Form, | ||||
|   Field, | ||||
|   Button, | ||||
| }; | ||||
| 
 | ||||
| export default Forms; | ||||
|  |  | |||
|  | @ -4,12 +4,12 @@ import { action } from '@storybook/addon-actions'; | |||
| 
 | ||||
| import { TimePicker } from './TimePicker'; | ||||
| import { UseState } from '../../utils/storybook/UseState'; | ||||
| import { withRightAlignedStory } from '../../utils/storybook/withRightAlignedStory'; | ||||
| import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; | ||||
| import { TimeFragment, dateTime } from '@grafana/data'; | ||||
| 
 | ||||
| const TimePickerStories = storiesOf('UI/TimePicker', module); | ||||
| 
 | ||||
| TimePickerStories.addDecorator(withRightAlignedStory); | ||||
| TimePickerStories.addDecorator(withCenteredStory); | ||||
| 
 | ||||
| TimePickerStories.add('default', () => { | ||||
|   return ( | ||||
|  | @ -25,16 +25,6 @@ TimePickerStories.add('default', () => { | |||
|           <TimePicker | ||||
|             timeZone="browser" | ||||
|             value={value} | ||||
|             selectOptions={[ | ||||
|               { 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 }, | ||||
|             ]} | ||||
|             onChange={timeRange => { | ||||
|               action('onChange fired')(timeRange); | ||||
|               updateValue(timeRange); | ||||
|  |  | |||
|  | @ -0,0 +1,46 @@ | |||
| import React from 'react'; | ||||
| import { shallow } from 'enzyme'; | ||||
| import { UnthemedTimePicker } from './TimePicker'; | ||||
| import { dateTime, TimeRange } from '@grafana/data'; | ||||
| import dark from './../../themes/dark'; | ||||
| 
 | ||||
| const from = '2019-12-17T07:48:27.433Z'; | ||||
| const to = '2019-12-18T07:48:27.433Z'; | ||||
| 
 | ||||
| const value: TimeRange = { | ||||
|   from: dateTime(from), | ||||
|   to: dateTime(to), | ||||
|   raw: { from: dateTime(from), to: dateTime(to) }, | ||||
| }; | ||||
| 
 | ||||
| describe('TimePicker', () => { | ||||
|   it('renders buttons correctly', () => { | ||||
|     const wrapper = shallow( | ||||
|       <UnthemedTimePicker | ||||
|         onChange={value => {}} | ||||
|         value={value} | ||||
|         onMoveBackward={() => {}} | ||||
|         onMoveForward={() => {}} | ||||
|         onZoom={() => {}} | ||||
|         theme={dark} | ||||
|       /> | ||||
|     ); | ||||
|     expect(wrapper).toMatchSnapshot(); | ||||
|   }); | ||||
| 
 | ||||
|   it('renders content correctly after beeing open', () => { | ||||
|     const wrapper = shallow( | ||||
|       <UnthemedTimePicker | ||||
|         onChange={value => {}} | ||||
|         value={value} | ||||
|         onMoveBackward={() => {}} | ||||
|         onMoveForward={() => {}} | ||||
|         onZoom={() => {}} | ||||
|         theme={dark} | ||||
|       /> | ||||
|     ); | ||||
| 
 | ||||
|     wrapper.find('button[aria-label="TimePicker Open Button"]').simulate('click', new Event('click')); | ||||
|     expect(wrapper).toMatchSnapshot(); | ||||
|   }); | ||||
| }); | ||||
|  | @ -1,60 +1,23 @@ | |||
| // Libraries
 | ||||
| import React, { PureComponent, createRef } from 'react'; | ||||
| import React, { PureComponent, memo, FormEvent } from 'react'; | ||||
| import { css } from 'emotion'; | ||||
| import classNames from 'classnames'; | ||||
| 
 | ||||
| // Components
 | ||||
| import { ButtonSelect } from '../Select/ButtonSelect'; | ||||
| import { Tooltip } from '../Tooltip/Tooltip'; | ||||
| import { TimePickerPopover } from './TimePickerPopover'; | ||||
| import { TimePickerContent } from './TimePickerContent/TimePickerContent'; | ||||
| import { ClickOutsideWrapper } from '../ClickOutsideWrapper/ClickOutsideWrapper'; | ||||
| 
 | ||||
| // Utils & Services
 | ||||
| import { isDateTime, DateTime, rangeUtil } from '@grafana/data'; | ||||
| import { rawToTimeRange } from './time'; | ||||
| import { stylesFactory } from '../../themes/stylesFactory'; | ||||
| import { withTheme } from '../../themes/ThemeContext'; | ||||
| import { withTheme, useTheme } from '../../themes/ThemeContext'; | ||||
| 
 | ||||
| // Types
 | ||||
| import { TimeRange, TimeOption, TimeZone, TIME_FORMAT, SelectableValue, dateMath, GrafanaTheme } from '@grafana/data'; | ||||
| import { isDateTime, DateTime, rangeUtil, GrafanaTheme, TIME_FORMAT } from '@grafana/data'; | ||||
| import { TimeRange, TimeOption, TimeZone, dateMath } from '@grafana/data'; | ||||
| import { Themeable } from '../../types'; | ||||
| 
 | ||||
| const getStyles = stylesFactory((theme: GrafanaTheme) => { | ||||
|   return { | ||||
|     timePickerSynced: css` | ||||
|       label: timePickerSynced; | ||||
|       border-color: ${theme.colors.orangeDark}; | ||||
|       background-image: none; | ||||
|       background-color: transparent; | ||||
|       color: ${theme.colors.orangeDark}; | ||||
|       &:focus, | ||||
|       :hover { | ||||
|         color: ${theme.colors.orangeDark}; | ||||
|         background-image: none; | ||||
|         background-color: transparent; | ||||
|       } | ||||
|     `,
 | ||||
|     noRightBorderStyle: css` | ||||
|       label: noRightBorderStyle; | ||||
|       border-right: 0; | ||||
|     `,
 | ||||
|   }; | ||||
| }); | ||||
| 
 | ||||
| export interface Props extends Themeable { | ||||
|   hideText?: boolean; | ||||
|   value: TimeRange; | ||||
|   selectOptions: TimeOption[]; | ||||
|   timeZone?: TimeZone; | ||||
|   timeSyncButton?: JSX.Element; | ||||
|   isSynced?: boolean; | ||||
|   onChange: (timeRange: TimeRange) => void; | ||||
|   onMoveBackward: () => void; | ||||
|   onMoveForward: () => void; | ||||
|   onZoom: () => void; | ||||
| } | ||||
| 
 | ||||
| export const defaultSelectOptions: TimeOption[] = [ | ||||
| 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 }, | ||||
|  | @ -71,6 +34,9 @@ export const defaultSelectOptions: TimeOption[] = [ | |||
|   { 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 }, | ||||
|  | @ -87,69 +53,79 @@ export const defaultSelectOptions: TimeOption[] = [ | |||
|   { from: 'now/y', to: 'now', display: 'This year so far', section: 3 }, | ||||
| ]; | ||||
| 
 | ||||
| const defaultZoomOutTooltip = () => { | ||||
|   return ( | ||||
|     <> | ||||
|       Time range zoom out <br /> CTRL+Z | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
| const getStyles = stylesFactory((theme: GrafanaTheme) => { | ||||
|   return { | ||||
|     container: css` | ||||
|       position: relative; | ||||
|       display: flex; | ||||
|       flex-flow: column nowrap; | ||||
|     `,
 | ||||
|     buttons: css` | ||||
|       display: flex; | ||||
|     `,
 | ||||
|     caretIcon: css` | ||||
|       margin-left: 3px; | ||||
| 
 | ||||
|       i { | ||||
|         font-size: ${theme.typography.size.md}; | ||||
|       } | ||||
|     `,
 | ||||
|   }; | ||||
| }); | ||||
| 
 | ||||
| const getLabelStyles = stylesFactory((theme: GrafanaTheme) => { | ||||
|   return { | ||||
|     container: css` | ||||
|       display: inline-block; | ||||
|     `,
 | ||||
|     utc: css` | ||||
|       color: ${theme.colors.orange}; | ||||
|       font-size: 75%; | ||||
|       padding: 3px; | ||||
|       font-weight: ${theme.typography.weight.semibold}; | ||||
|     `,
 | ||||
|   }; | ||||
| }); | ||||
| 
 | ||||
| export interface Props extends Themeable { | ||||
|   hideText?: boolean; | ||||
|   value: TimeRange; | ||||
|   timeZone?: TimeZone; | ||||
|   timeSyncButton?: JSX.Element; | ||||
|   isSynced?: boolean; | ||||
|   onChange: (timeRange: TimeRange) => void; | ||||
|   onMoveBackward: () => void; | ||||
|   onMoveForward: () => void; | ||||
|   onZoom: () => void; | ||||
|   history?: TimeRange[]; | ||||
| } | ||||
| 
 | ||||
| export interface State { | ||||
|   isCustomOpen: boolean; | ||||
|   isOpen: boolean; | ||||
| } | ||||
| class UnThemedTimePicker extends PureComponent<Props, State> { | ||||
|   pickerTriggerRef = createRef<HTMLDivElement>(); | ||||
| 
 | ||||
| export class UnthemedTimePicker extends PureComponent<Props, State> { | ||||
|   state: State = { | ||||
|     isCustomOpen: false, | ||||
|     isOpen: false, | ||||
|   }; | ||||
| 
 | ||||
|   mapTimeOptionsToSelectableValues = (selectOptions: TimeOption[]) => { | ||||
|     const options = selectOptions.map(timeOption => { | ||||
|       return { | ||||
|         label: timeOption.display, | ||||
|         value: timeOption, | ||||
|       }; | ||||
|     }); | ||||
| 
 | ||||
|     options.unshift({ | ||||
|       label: 'Custom time range', | ||||
|       value: { from: 'custom', to: 'custom', display: 'Custom', section: 1 }, | ||||
|     }); | ||||
| 
 | ||||
|     return options; | ||||
|   onChange = (timeRange: TimeRange) => { | ||||
|     this.props.onChange(timeRange); | ||||
|     this.setState({ isOpen: false }); | ||||
|   }; | ||||
| 
 | ||||
|   onSelectChanged = (item: SelectableValue<TimeOption>) => { | ||||
|     const { onChange, timeZone } = this.props; | ||||
| 
 | ||||
|     if (item.value && item.value.from === 'custom') { | ||||
|       // this is to prevent the ClickOutsideWrapper from directly closing the popover
 | ||||
|       setTimeout(() => { | ||||
|         this.setState({ isCustomOpen: true }); | ||||
|       }, 1); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     if (item.value) { | ||||
|       onChange(rawToTimeRange({ from: item.value.from, to: item.value.to }, timeZone)); | ||||
|     } | ||||
|   onOpen = (event: FormEvent<HTMLButtonElement>) => { | ||||
|     const { isOpen } = this.state; | ||||
|     event.stopPropagation(); | ||||
|     this.setState({ isOpen: !isOpen }); | ||||
|   }; | ||||
| 
 | ||||
|   onCustomChange = (timeRange: TimeRange) => { | ||||
|     const { onChange } = this.props; | ||||
|     onChange(timeRange); | ||||
|     this.setState({ isCustomOpen: false }); | ||||
|   }; | ||||
| 
 | ||||
|   onCloseCustom = () => { | ||||
|     this.setState({ isCustomOpen: false }); | ||||
|   onClose = () => { | ||||
|     this.setState({ isOpen: false }); | ||||
|   }; | ||||
| 
 | ||||
|   render() { | ||||
|     const { | ||||
|       selectOptions: selectTimeOptions, | ||||
|       value, | ||||
|       onMoveBackward, | ||||
|       onMoveForward, | ||||
|  | @ -158,56 +134,48 @@ class UnThemedTimePicker extends PureComponent<Props, State> { | |||
|       timeSyncButton, | ||||
|       isSynced, | ||||
|       theme, | ||||
|       hideText, | ||||
|       history, | ||||
|     } = this.props; | ||||
| 
 | ||||
|     const { isOpen } = this.state; | ||||
|     const styles = getStyles(theme); | ||||
|     const { isCustomOpen } = this.state; | ||||
|     const options = this.mapTimeOptionsToSelectableValues(selectTimeOptions); | ||||
|     const currentOption = options.find(item => isTimeOptionEqualToTimeRange(item.value, value)); | ||||
| 
 | ||||
|     const isUTC = timeZone === 'utc'; | ||||
| 
 | ||||
|     const adjustedTime = (time: DateTime) => (isUTC ? time.utc() : time.local()) || null; | ||||
|     const adjustedTimeRange = { | ||||
|       to: dateMath.isMathString(value.raw.to) ? value.raw.to : adjustedTime(value.to), | ||||
|       from: dateMath.isMathString(value.raw.from) ? value.raw.from : adjustedTime(value.from), | ||||
|     }; | ||||
|     const rangeString = rangeUtil.describeTimeRange(adjustedTimeRange); | ||||
| 
 | ||||
|     const label = !hideText ? ( | ||||
|       <> | ||||
|         {isCustomOpen && <span>Custom time range</span>} | ||||
|         {!isCustomOpen && <span>{rangeString}</span>} | ||||
|         {isUTC && <span className="time-picker-utc">UTC</span>} | ||||
|       </> | ||||
|     ) : ( | ||||
|       '' | ||||
|     ); | ||||
|     const hasAbsolute = isDateTime(value.raw.from) || isDateTime(value.raw.to); | ||||
| 
 | ||||
|     return ( | ||||
|       <div className="time-picker" ref={this.pickerTriggerRef}> | ||||
|         <div className="time-picker-buttons"> | ||||
|       <div className={styles.container}> | ||||
|         <div className={styles.buttons}> | ||||
|           {hasAbsolute && ( | ||||
|             <button className="btn navbar-button navbar-button--tight" onClick={onMoveBackward}> | ||||
|               <i className="fa fa-chevron-left" /> | ||||
|             </button> | ||||
|           )} | ||||
|           <ButtonSelect | ||||
|             className={classNames('time-picker-button-select', { | ||||
|               ['explore-active-button-glow']: timeSyncButton && isSynced, | ||||
|               [`btn--radius-right-0 ${styles.noRightBorderStyle}`]: timeSyncButton, | ||||
|               [styles.timePickerSynced]: timeSyncButton ? isSynced : null, | ||||
|             })} | ||||
|             value={currentOption} | ||||
|             label={label} | ||||
|             options={options} | ||||
|             maxMenuHeight={600} | ||||
|             onChange={this.onSelectChanged} | ||||
|             iconClass={classNames('fa fa-clock-o fa-fw', isSynced && timeSyncButton && 'icon-brand-gradient')} | ||||
|             tooltipContent={<TimePickerTooltipContent timeRange={value} />} | ||||
|           /> | ||||
|           <div> | ||||
|             <Tooltip content={<TimePickerTooltip timeRange={value} />} placement="bottom"> | ||||
|               <button | ||||
|                 aria-label="TimePicker Open Button" | ||||
|                 className="btn navbar-button navbar-button--zoom" | ||||
|                 onClick={this.onOpen} | ||||
|               > | ||||
|                 <i className={classNames('fa fa-clock-o fa-fw', isSynced && timeSyncButton && 'icon-brand-gradient')} /> | ||||
|                 <TimePickerButtonLabel {...this.props} /> | ||||
|                 <span className={styles.caretIcon}> | ||||
|                   {isOpen ? <i className="fa fa-caret-up fa-fw" /> : <i className="fa fa-caret-down fa-fw" />} | ||||
|                 </span> | ||||
|               </button> | ||||
|             </Tooltip> | ||||
|             {isOpen && ( | ||||
|               <ClickOutsideWrapper onClick={this.onClose}> | ||||
|                 <TimePickerContent | ||||
|                   timeZone={timeZone} | ||||
|                   value={value} | ||||
|                   onChange={this.onChange} | ||||
|                   otherOptions={otherOptions} | ||||
|                   quickOptions={quickOptions} | ||||
|                   history={history} | ||||
|                 /> | ||||
|               </ClickOutsideWrapper> | ||||
|             )} | ||||
|           </div> | ||||
| 
 | ||||
|           {timeSyncButton} | ||||
| 
 | ||||
|  | @ -217,24 +185,24 @@ class UnThemedTimePicker extends PureComponent<Props, State> { | |||
|             </button> | ||||
|           )} | ||||
| 
 | ||||
|           <Tooltip content={defaultZoomOutTooltip} placement="bottom"> | ||||
|           <Tooltip content={ZoomOutTooltip} placement="bottom"> | ||||
|             <button className="btn navbar-button navbar-button--zoom" onClick={onZoom}> | ||||
|               <i className="fa fa-search-minus" /> | ||||
|             </button> | ||||
|           </Tooltip> | ||||
| 
 | ||||
|           {isCustomOpen && ( | ||||
|             <ClickOutsideWrapper onClick={this.onCloseCustom}> | ||||
|               <TimePickerPopover value={value} timeZone={timeZone} onChange={this.onCustomChange} /> | ||||
|             </ClickOutsideWrapper> | ||||
|           )} | ||||
|         </div> | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| const TimePickerTooltipContent = ({ timeRange }: { timeRange: TimeRange }) => ( | ||||
| const ZoomOutTooltip = () => ( | ||||
|   <> | ||||
|     Time range zoom out <br /> CTRL+Z | ||||
|   </> | ||||
| ); | ||||
| 
 | ||||
| const TimePickerTooltip = ({ timeRange }: { timeRange: TimeRange }) => ( | ||||
|   <> | ||||
|     {timeRange.from.format(TIME_FORMAT)} | ||||
|     <div className="text-center">to</div> | ||||
|  | @ -242,8 +210,36 @@ const TimePickerTooltipContent = ({ timeRange }: { timeRange: TimeRange }) => ( | |||
|   </> | ||||
| ); | ||||
| 
 | ||||
| function isTimeOptionEqualToTimeRange(option: TimeOption, range: TimeRange): boolean { | ||||
|   return range.raw.from === option.from && range.raw.to === option.to; | ||||
| } | ||||
| const TimePickerButtonLabel = memo<Props>(props => { | ||||
|   const theme = useTheme(); | ||||
|   const styles = getLabelStyles(theme); | ||||
|   const isUTC = props.timeZone === 'utc'; | ||||
| 
 | ||||
| export const TimePicker = withTheme(UnThemedTimePicker); | ||||
|   if (props.hideText) { | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   return ( | ||||
|     <span className={styles.container}> | ||||
|       <span>{formattedRange(props.value, isUTC)}</span> | ||||
|       {isUTC && <span className={styles.utc}>UTC</span>} | ||||
|     </span> | ||||
|   ); | ||||
| }); | ||||
| 
 | ||||
| const formattedRange = (value: TimeRange, isUTC: boolean) => { | ||||
|   const adjustedTimeRange = { | ||||
|     to: dateMath.isMathString(value.raw.to) ? value.raw.to : adjustedTime(value.to, isUTC), | ||||
|     from: dateMath.isMathString(value.raw.from) ? value.raw.from : adjustedTime(value.from, isUTC), | ||||
|   }; | ||||
|   return rangeUtil.describeTimeRange(adjustedTimeRange); | ||||
| }; | ||||
| 
 | ||||
| const adjustedTime = (time: DateTime, isUTC: boolean) => { | ||||
|   if (isUTC) { | ||||
|     return time.utc() || null; | ||||
|   } | ||||
|   return time.local() || null; | ||||
| }; | ||||
| 
 | ||||
| export const TimePicker = withTheme(UnthemedTimePicker); | ||||
|  |  | |||
|  | @ -1,29 +0,0 @@ | |||
| import React from 'react'; | ||||
| import { storiesOf } from '@storybook/react'; | ||||
| import { action } from '@storybook/addon-actions'; | ||||
| 
 | ||||
| import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; | ||||
| import { TimePickerCalendar } from './TimePickerCalendar'; | ||||
| import { UseState } from '../../utils/storybook/UseState'; | ||||
| import { TimeFragment } from '@grafana/data'; | ||||
| 
 | ||||
| const TimePickerCalendarStories = storiesOf('UI/TimePicker/TimePickerCalendar', module); | ||||
| 
 | ||||
| TimePickerCalendarStories.addDecorator(withCenteredStory); | ||||
| 
 | ||||
| TimePickerCalendarStories.add('default', () => ( | ||||
|   <UseState initialState={'now-6h' as TimeFragment}> | ||||
|     {(value, updateValue) => { | ||||
|       return ( | ||||
|         <TimePickerCalendar | ||||
|           timeZone="browser" | ||||
|           value={value} | ||||
|           onChange={timeRange => { | ||||
|             action('onChange fired')(timeRange); | ||||
|             updateValue(timeRange); | ||||
|           }} | ||||
|         /> | ||||
|       ); | ||||
|     }} | ||||
|   </UseState> | ||||
| )); | ||||
|  | @ -1,61 +0,0 @@ | |||
| import React, { PureComponent } from 'react'; | ||||
| import Calendar from 'react-calendar/dist/entry.nostyle'; | ||||
| import { TimeFragment, TimeZone, TIME_FORMAT } from '@grafana/data'; | ||||
| import { DateTime, dateTime, toUtc } from '@grafana/data'; | ||||
| import { stringToDateTimeType } from './time'; | ||||
| 
 | ||||
| export interface Props { | ||||
|   value: TimeFragment; | ||||
|   roundup?: boolean; | ||||
|   timeZone?: TimeZone; | ||||
|   onChange: (value: DateTime) => void; | ||||
| } | ||||
| 
 | ||||
| export class TimePickerCalendar extends PureComponent<Props> { | ||||
|   onCalendarChange = (date: Date | Date[]) => { | ||||
|     const { onChange, timeZone } = this.props; | ||||
| 
 | ||||
|     if (Array.isArray(date)) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     let newDate = dateTime(date); | ||||
| 
 | ||||
|     if (timeZone === 'utc') { | ||||
|       newDate = toUtc(newDate.format(TIME_FORMAT)); | ||||
|     } | ||||
| 
 | ||||
|     onChange(newDate); | ||||
|   }; | ||||
| 
 | ||||
|   onDrilldown = (props: any) => { | ||||
|     // this is to prevent clickout side wrapper from triggering when drilling down
 | ||||
|     if (window.event) { | ||||
|       // @ts-ignore
 | ||||
|       window.event.stopPropagation(); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   render() { | ||||
|     const { value, roundup, timeZone } = this.props; | ||||
|     let date = stringToDateTimeType(value, roundup, timeZone); | ||||
| 
 | ||||
|     if (!date.isValid()) { | ||||
|       date = dateTime(); | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|       <Calendar | ||||
|         value={date.toDate()} | ||||
|         next2Label={null} | ||||
|         prev2Label={null} | ||||
|         className="time-picker-calendar" | ||||
|         tileClassName="time-picker-calendar-tile" | ||||
|         onChange={this.onCalendarChange} | ||||
|         onDrillDown={this.onDrilldown} | ||||
|         nextLabel={<span className="fa fa-angle-right" />} | ||||
|         prevLabel={<span className="fa fa-angle-left" />} | ||||
|       /> | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | @ -0,0 +1,305 @@ | |||
| import React, { memo } from 'react'; | ||||
| import { css, cx } from 'emotion'; | ||||
| import Calendar from 'react-calendar/dist/entry.nostyle'; | ||||
| import { GrafanaTheme, dateTime, TIME_FORMAT } from '@grafana/data'; | ||||
| import { stringToDateTimeType } from '../time'; | ||||
| import { useTheme, stylesFactory } from '../../../themes'; | ||||
| import { TimePickerTitle } from './TimePickerTitle'; | ||||
| import Forms from '../../Forms'; | ||||
| import { Portal } from '../../Portal/Portal'; | ||||
| import { getThemeColors } from './colors'; | ||||
| import { ClickOutsideWrapper } from '../../ClickOutsideWrapper/ClickOutsideWrapper'; | ||||
| 
 | ||||
| const getStyles = stylesFactory((theme: GrafanaTheme) => { | ||||
|   const colors = getThemeColors(theme); | ||||
| 
 | ||||
|   return { | ||||
|     container: css` | ||||
|       top: 0; | ||||
|       position: absolute; | ||||
|       right: 546px; | ||||
|       box-shadow: 0px 0px 20px ${colors.shadow}; | ||||
|       background-color: ${colors.background}; | ||||
|       z-index: -1; | ||||
| 
 | ||||
|       &:after { | ||||
|         display: block; | ||||
|         background-color: ${colors.background}; | ||||
|         width: 19px; | ||||
|         height: 381px; | ||||
|         content: ' '; | ||||
|         position: absolute; | ||||
|         top: 0; | ||||
|         right: -19px; | ||||
|         border-left: 1px solid ${colors.border}; | ||||
|       } | ||||
|     `,
 | ||||
|     modal: css` | ||||
|       position: fixed; | ||||
|       top: 20%; | ||||
|       width: 100%; | ||||
|       z-index: ${theme.zIndex.modal}; | ||||
|     `,
 | ||||
|     content: css` | ||||
|       margin: 0 auto; | ||||
|       width: 268px; | ||||
|     `,
 | ||||
|     backdrop: css` | ||||
|       position: fixed; | ||||
|       top: 0; | ||||
|       right: 0; | ||||
|       bottom: 0; | ||||
|       left: 0; | ||||
|       background: #202226; | ||||
|       opacity: 0.7; | ||||
|       z-index: ${theme.zIndex.modalBackdrop}; | ||||
|       text-align: center; | ||||
|     `,
 | ||||
|   }; | ||||
| }); | ||||
| 
 | ||||
| const getFooterStyles = stylesFactory((theme: GrafanaTheme) => { | ||||
|   const colors = getThemeColors(theme); | ||||
| 
 | ||||
|   return { | ||||
|     container: css` | ||||
|       background-color: ${colors.background}; | ||||
|       display: flex; | ||||
|       justify-content: center; | ||||
|       padding: 10px; | ||||
|       align-items: stretch; | ||||
|     `,
 | ||||
|     apply: css` | ||||
|       margin-right: 4px; | ||||
|       width: 100%; | ||||
|       justify-content: center; | ||||
|     `,
 | ||||
|   }; | ||||
| }); | ||||
| 
 | ||||
| const getBodyStyles = stylesFactory((theme: GrafanaTheme) => { | ||||
|   const colors = getThemeColors(theme); | ||||
| 
 | ||||
|   return { | ||||
|     title: css` | ||||
|       color: ${theme.colors.text} | ||||
|       background-color: ${colors.background}; | ||||
|       line-height: 21px; | ||||
|       font-size: ${theme.typography.size.md}; | ||||
|       border: 1px solid transparent; | ||||
| 
 | ||||
|       &:hover { | ||||
|         position: relative; | ||||
|       } | ||||
|     `,
 | ||||
|     body: css` | ||||
|       z-index: ${theme.zIndex.modal}; | ||||
|       background-color: ${colors.background}; | ||||
|       width: 268px; | ||||
| 
 | ||||
|       .react-calendar__navigation__label, | ||||
|       .react-calendar__navigation__arrow, | ||||
|       .react-calendar__navigation { | ||||
|         padding-top: 4px; | ||||
|         background-color: inherit; | ||||
|         color: ${theme.colors.text}; | ||||
|         border: 0; | ||||
|         font-weight: ${theme.typography.weight.semibold}; | ||||
|       } | ||||
| 
 | ||||
|       .react-calendar__month-view__weekdays { | ||||
|         background-color: inherit; | ||||
|         text-align: center; | ||||
|         color: ${theme.colors.blueShade}; | ||||
| 
 | ||||
|         abbr { | ||||
|           border: 0; | ||||
|           text-decoration: none; | ||||
|           cursor: default; | ||||
|           display: block; | ||||
|           padding: 4px 0 4px 0; | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       .react-calendar__month-view__days { | ||||
|         background-color: inherit; | ||||
|       } | ||||
| 
 | ||||
|       .react-calendar__tile, | ||||
|       .react-calendar__tile--now { | ||||
|         margin-bottom: 4px; | ||||
|         background-color: inherit; | ||||
|       } | ||||
| 
 | ||||
|       .react-calendar__navigation__label, | ||||
|       .react-calendar__navigation > button:focus, | ||||
|       .time-picker-calendar-tile:focus { | ||||
|         outline: 0; | ||||
|       } | ||||
| 
 | ||||
|       .react-calendar__tile--active, | ||||
|       .react-calendar__tile--active:hover { | ||||
|         color: ${theme.colors.white}; | ||||
|         font-weight: ${theme.typography.weight.semibold}; | ||||
|         background: ${theme.colors.blue95}; | ||||
|         box-shadow: none; | ||||
|         border: 0px; | ||||
|       } | ||||
| 
 | ||||
|       .react-calendar__tile--rangeEnd, | ||||
|       .react-calendar__tile--rangeStart { | ||||
|         padding: 0; | ||||
|         border: 0px; | ||||
|         color: ${theme.colors.white}; | ||||
|         font-weight: ${theme.typography.weight.semibold}; | ||||
|         background: ${theme.colors.blue95}; | ||||
| 
 | ||||
|         abbr { | ||||
|           background-color: ${theme.colors.blue77}; | ||||
|           border-radius: 100px; | ||||
|           display: block; | ||||
|           padding: 2px 7px 3px; | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       .react-calendar__tile--rangeStart { | ||||
|         border-top-left-radius: 20px; | ||||
|         border-bottom-left-radius: 20px; | ||||
|       } | ||||
| 
 | ||||
|       .react-calendar__tile--rangeEnd { | ||||
|         border-top-right-radius: 20px; | ||||
|         border-bottom-right-radius: 20px; | ||||
|       } | ||||
|     `,
 | ||||
|   }; | ||||
| }); | ||||
| 
 | ||||
| const getHeaderStyles = stylesFactory((theme: GrafanaTheme) => { | ||||
|   const colors = getThemeColors(theme); | ||||
| 
 | ||||
|   return { | ||||
|     container: css` | ||||
|       background-color: ${colors.background}; | ||||
|       display: flex; | ||||
|       justify-content: space-between; | ||||
|       padding: 7px; | ||||
|     `,
 | ||||
|     close: css` | ||||
|       cursor: pointer; | ||||
|       font-size: ${theme.typography.size.lg}; | ||||
|     `,
 | ||||
|   }; | ||||
| }); | ||||
| 
 | ||||
| interface Props { | ||||
|   isOpen: boolean; | ||||
|   from: string; | ||||
|   to: string; | ||||
|   onClose: () => void; | ||||
|   onApply: () => void; | ||||
|   onChange: (from: string, to: string) => void; | ||||
|   isFullscreen: boolean; | ||||
| } | ||||
| 
 | ||||
| export const TimePickerCalendar = memo<Props>(props => { | ||||
|   const theme = useTheme(); | ||||
|   const styles = getStyles(theme); | ||||
|   const { isOpen, isFullscreen } = props; | ||||
| 
 | ||||
|   if (!isOpen) { | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   if (isFullscreen) { | ||||
|     return ( | ||||
|       <ClickOutsideWrapper onClick={props.onClose}> | ||||
|         <div className={styles.container}> | ||||
|           <Body {...props} /> | ||||
|         </div> | ||||
|       </ClickOutsideWrapper> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   return ( | ||||
|     <Portal> | ||||
|       <div className={styles.modal} onClick={event => event.stopPropagation()}> | ||||
|         <div className={styles.content}> | ||||
|           <Header {...props} /> | ||||
|           <Body {...props} /> | ||||
|           <Footer {...props} /> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div className={styles.backdrop} onClick={event => event.stopPropagation()} /> | ||||
|     </Portal> | ||||
|   ); | ||||
| }); | ||||
| 
 | ||||
| const Header = memo<Props>(({ onClose }) => { | ||||
|   const theme = useTheme(); | ||||
|   const styles = getHeaderStyles(theme); | ||||
| 
 | ||||
|   return ( | ||||
|     <div className={styles.container}> | ||||
|       <TimePickerTitle>Select a time range</TimePickerTitle> | ||||
|       <i className={cx(styles.close, 'fa', 'fa-times')} onClick={onClose} /> | ||||
|     </div> | ||||
|   ); | ||||
| }); | ||||
| 
 | ||||
| const Body = memo<Props>(props => { | ||||
|   const theme = useTheme(); | ||||
|   const styles = getBodyStyles(theme); | ||||
|   const { from, to, onChange } = props; | ||||
| 
 | ||||
|   return ( | ||||
|     <Calendar | ||||
|       selectRange={true} | ||||
|       next2Label={null} | ||||
|       prev2Label={null} | ||||
|       className={styles.body} | ||||
|       tileClassName={styles.title} | ||||
|       value={inputToValue(from, to)} | ||||
|       nextLabel={<span className="fa fa-angle-right" />} | ||||
|       prevLabel={<span className="fa fa-angle-left" />} | ||||
|       onChange={value => valueToInput(value, onChange)} | ||||
|       locale="en" | ||||
|     /> | ||||
|   ); | ||||
| }); | ||||
| 
 | ||||
| const Footer = memo<Props>(({ onClose, onApply }) => { | ||||
|   const theme = useTheme(); | ||||
|   const styles = getFooterStyles(theme); | ||||
| 
 | ||||
|   return ( | ||||
|     <div className={styles.container}> | ||||
|       <Forms.Button className={styles.apply} onClick={onApply}> | ||||
|         Apply time range | ||||
|       </Forms.Button> | ||||
|       <Forms.Button variant="secondary" onClick={onClose}> | ||||
|         Cancel | ||||
|       </Forms.Button> | ||||
|     </div> | ||||
|   ); | ||||
| }); | ||||
| 
 | ||||
| function inputToValue(from: string, to: string): Date[] { | ||||
|   const fromAsDateTime = stringToDateTimeType(from); | ||||
|   const toAsDateTime = stringToDateTimeType(to); | ||||
|   const fromAsDate = fromAsDateTime.isValid() ? fromAsDateTime.toDate() : new Date(); | ||||
|   const toAsDate = toAsDateTime.isValid() ? toAsDateTime.toDate() : new Date(); | ||||
| 
 | ||||
|   if (fromAsDate > toAsDate) { | ||||
|     return [toAsDate, fromAsDate]; | ||||
|   } | ||||
|   return [fromAsDate, toAsDate]; | ||||
| } | ||||
| 
 | ||||
| function valueToInput(value: Date | Date[], onChange: (from: string, to: string) => void): void { | ||||
|   const [from, to] = value; | ||||
|   const fromAsString = dateTime(from).format(TIME_FORMAT); | ||||
|   const toAsString = dateTime(to).format(TIME_FORMAT); | ||||
| 
 | ||||
|   return onChange(fromAsString, toAsString); | ||||
| } | ||||
|  | @ -0,0 +1,49 @@ | |||
| import React from 'react'; | ||||
| import { shallow } from 'enzyme'; | ||||
| import { TimePickerContentWithScreenSize } from './TimePickerContent'; | ||||
| import { dateTime, TimeRange } from '@grafana/data'; | ||||
| 
 | ||||
| describe('TimePickerContent', () => { | ||||
|   it('renders correctly in full screen', () => { | ||||
|     const value = createTimeRange('2019-12-17T07:48:27.433Z', '2019-12-18T07:48:27.433Z'); | ||||
|     const wrapper = shallow( | ||||
|       <TimePickerContentWithScreenSize onChange={value => {}} timeZone="utc" value={value} isFullscreen={true} /> | ||||
|     ); | ||||
|     expect(wrapper).toMatchSnapshot(); | ||||
|   }); | ||||
| 
 | ||||
|   it('renders correctly in narrow screen', () => { | ||||
|     const value = createTimeRange('2019-12-17T07:48:27.433Z', '2019-12-18T07:48:27.433Z'); | ||||
|     const wrapper = shallow( | ||||
|       <TimePickerContentWithScreenSize onChange={value => {}} timeZone="utc" value={value} isFullscreen={false} /> | ||||
|     ); | ||||
|     expect(wrapper).toMatchSnapshot(); | ||||
|   }); | ||||
| 
 | ||||
|   it('renders recent absolute ranges correctly', () => { | ||||
|     const value = createTimeRange('2019-12-17T07:48:27.433Z', '2019-12-18T07:48:27.433Z'); | ||||
|     const history = [ | ||||
|       createTimeRange('2019-12-17T07:48:27.433Z', '2019-12-18T07:48:27.433Z'), | ||||
|       createTimeRange('2019-10-17T07:48:27.433Z', '2019-10-18T07:48:27.433Z'), | ||||
|     ]; | ||||
| 
 | ||||
|     const wrapper = shallow( | ||||
|       <TimePickerContentWithScreenSize | ||||
|         onChange={value => {}} | ||||
|         timeZone="utc" | ||||
|         value={value} | ||||
|         isFullscreen={true} | ||||
|         history={history} | ||||
|       /> | ||||
|     ); | ||||
|     expect(wrapper).toMatchSnapshot(); | ||||
|   }); | ||||
| }); | ||||
| 
 | ||||
| function createTimeRange(from: string, to: string): TimeRange { | ||||
|   return { | ||||
|     from: dateTime(from), | ||||
|     to: dateTime(to), | ||||
|     raw: { from: dateTime(from), to: dateTime(to) }, | ||||
|   }; | ||||
| } | ||||
|  | @ -0,0 +1,279 @@ | |||
| import React, { useState, memo } from 'react'; | ||||
| import { useMedia } from 'react-use'; | ||||
| import { css } from 'emotion'; | ||||
| import { useTheme, stylesFactory } from '../../../themes'; | ||||
| import { GrafanaTheme, TimeOption, TimeRange, TimeZone, isDateTime } from '@grafana/data'; | ||||
| import { TimePickerTitle } from './TimePickerTitle'; | ||||
| import { TimeRangeForm } from './TimeRangeForm'; | ||||
| import { CustomScrollbar } from '../../CustomScrollbar/CustomScrollbar'; | ||||
| import { TimeRangeList } from './TimeRangeList'; | ||||
| import { mapRangeToTimeOption } from './mapper'; | ||||
| import { getThemeColors } from './colors'; | ||||
| 
 | ||||
| const getStyles = stylesFactory((theme: GrafanaTheme) => { | ||||
|   const colors = getThemeColors(theme); | ||||
| 
 | ||||
|   return { | ||||
|     container: css` | ||||
|       display: flex; | ||||
|       background: ${colors.background}; | ||||
|       box-shadow: 0px 0px 20px ${colors.shadow}; | ||||
|       position: absolute; | ||||
|       z-index: ${theme.zIndex.modal}; | ||||
|       width: 546px; | ||||
|       height: 381px; | ||||
|       top: 116%; | ||||
|       margin-left: -322px; | ||||
| 
 | ||||
|       @media only screen and (max-width: ${theme.breakpoints.lg}) { | ||||
|         width: 218px; | ||||
|         margin-left: 6px; | ||||
|       } | ||||
| 
 | ||||
|       @media only screen and (max-width: ${theme.breakpoints.sm}) { | ||||
|         width: 264px; | ||||
|         margin-left: -100px; | ||||
|       } | ||||
|     `,
 | ||||
|     leftSide: css` | ||||
|       display: flex; | ||||
|       flex-direction: column; | ||||
|       border-right: 1px solid ${colors.border}; | ||||
|       width: 60%; | ||||
|       overflow: hidden; | ||||
| 
 | ||||
|       @media only screen and (max-width: ${theme.breakpoints.lg}) { | ||||
|         display: none; | ||||
|       } | ||||
|     `,
 | ||||
|     rightSide: css` | ||||
|       width: 40% !important; | ||||
| 
 | ||||
|       @media only screen and (max-width: ${theme.breakpoints.lg}) { | ||||
|         width: 100% !important; | ||||
|       } | ||||
|     `,
 | ||||
|     spacing: css` | ||||
|       margin-top: 16px; | ||||
|     `,
 | ||||
|   }; | ||||
| }); | ||||
| 
 | ||||
| const getNarrowScreenStyles = stylesFactory((theme: GrafanaTheme) => { | ||||
|   const colors = getThemeColors(theme); | ||||
| 
 | ||||
|   return { | ||||
|     header: css` | ||||
|       display: flex; | ||||
|       flex-direction: row; | ||||
|       justify-content: space-between; | ||||
|       align-items: center; | ||||
|       border-bottom: 1px solid ${colors.border}; | ||||
|       padding: 7px 9px 7px 9px; | ||||
|     `,
 | ||||
|     body: css` | ||||
|       border-bottom: 1px solid ${colors.border}; | ||||
|       background: ${colors.formBackground}; | ||||
|       box-shadow: inset 0px 2px 2px ${colors.shadow}; | ||||
|     `,
 | ||||
|     form: css` | ||||
|       padding: 7px 9px 7px 9px; | ||||
|     `,
 | ||||
|   }; | ||||
| }); | ||||
| 
 | ||||
| const getFullScreenStyles = stylesFactory((theme: GrafanaTheme) => { | ||||
|   return { | ||||
|     container: css` | ||||
|       padding-top: 9px; | ||||
|       padding-left: 11px; | ||||
|       padding-right: 20%; | ||||
|     `,
 | ||||
|     title: css` | ||||
|       margin-bottom: 11px; | ||||
|     `,
 | ||||
|     recent: css` | ||||
|       flex-grow: 1; | ||||
|       display: flex; | ||||
|       flex-direction: column; | ||||
|       justify-content: flex-end; | ||||
|     `,
 | ||||
|   }; | ||||
| }); | ||||
| 
 | ||||
| const getEmptyListStyles = stylesFactory((theme: GrafanaTheme) => { | ||||
|   const colors = getThemeColors(theme); | ||||
| 
 | ||||
|   return { | ||||
|     container: css` | ||||
|       background-color: ${colors.formBackground}; | ||||
|       padding: 12px; | ||||
|       margin: 12px; | ||||
| 
 | ||||
|       a, | ||||
|       span { | ||||
|         font-size: 13px; | ||||
|       } | ||||
|     `,
 | ||||
|     link: css` | ||||
|       color: ${theme.colors.linkExternal}; | ||||
|     `,
 | ||||
|   }; | ||||
| }); | ||||
| 
 | ||||
| interface Props { | ||||
|   value: TimeRange; | ||||
|   onChange: (timeRange: TimeRange) => void; | ||||
|   timeZone?: TimeZone; | ||||
|   quickOptions?: TimeOption[]; | ||||
|   otherOptions?: TimeOption[]; | ||||
|   history?: TimeRange[]; | ||||
| } | ||||
| 
 | ||||
| interface PropsWithScreenSize extends Props { | ||||
|   isFullscreen: boolean; | ||||
| } | ||||
| 
 | ||||
| interface FormProps extends Omit<Props, 'history'> { | ||||
|   visible: boolean; | ||||
|   historyOptions?: TimeOption[]; | ||||
| } | ||||
| 
 | ||||
| export const TimePickerContentWithScreenSize: React.FC<PropsWithScreenSize> = props => { | ||||
|   const theme = useTheme(); | ||||
|   const styles = getStyles(theme); | ||||
|   const historyOptions = mapToHistoryOptions(props.history, props.timeZone); | ||||
|   const { quickOptions = [], otherOptions = [], isFullscreen } = props; | ||||
| 
 | ||||
|   return ( | ||||
|     <div className={styles.container}> | ||||
|       <div className={styles.leftSide}> | ||||
|         <FullScreenForm {...props} visible={isFullscreen} historyOptions={historyOptions} /> | ||||
|       </div> | ||||
|       <CustomScrollbar className={styles.rightSide}> | ||||
|         <NarrowScreenForm {...props} visible={!isFullscreen} historyOptions={historyOptions} /> | ||||
|         <TimeRangeList | ||||
|           title="Relative time ranges" | ||||
|           options={quickOptions} | ||||
|           onSelect={props.onChange} | ||||
|           value={props.value} | ||||
|           timeZone={props.timeZone} | ||||
|         /> | ||||
|         <div className={styles.spacing} /> | ||||
|         <TimeRangeList | ||||
|           title="Other quick ranges" | ||||
|           options={otherOptions} | ||||
|           onSelect={props.onChange} | ||||
|           value={props.value} | ||||
|           timeZone={props.timeZone} | ||||
|         /> | ||||
|       </CustomScrollbar> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export const TimePickerContent: React.FC<Props> = props => { | ||||
|   const theme = useTheme(); | ||||
|   const isFullscreen = useMedia(`(min-width: ${theme.breakpoints.lg})`); | ||||
| 
 | ||||
|   return <TimePickerContentWithScreenSize {...props} isFullscreen={isFullscreen} />; | ||||
| }; | ||||
| 
 | ||||
| const NarrowScreenForm: React.FC<FormProps> = props => { | ||||
|   if (!props.visible) { | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   const theme = useTheme(); | ||||
|   const styles = getNarrowScreenStyles(theme); | ||||
|   const isAbsolute = isDateTime(props.value.raw.from) || isDateTime(props.value.raw.to); | ||||
|   const [collapsed, setCollapsed] = useState(isAbsolute); | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|       <div className={styles.header} onClick={() => setCollapsed(!collapsed)}> | ||||
|         <TimePickerTitle>Absolute time range</TimePickerTitle> | ||||
|         {collapsed ? <i className="fa fa-caret-up" /> : <i className="fa fa-caret-down" />} | ||||
|       </div> | ||||
|       {collapsed && ( | ||||
|         <div className={styles.body}> | ||||
|           <div className={styles.form}> | ||||
|             <TimeRangeForm | ||||
|               value={props.value} | ||||
|               onApply={props.onChange} | ||||
|               timeZone={props.timeZone} | ||||
|               isFullscreen={false} | ||||
|             /> | ||||
|           </div> | ||||
|           <TimeRangeList | ||||
|             title="Recently used absolute ranges" | ||||
|             options={props.historyOptions || []} | ||||
|             onSelect={props.onChange} | ||||
|             value={props.value} | ||||
|             placeholderEmpty={null} | ||||
|             timeZone={props.timeZone} | ||||
|           /> | ||||
|         </div> | ||||
|       )} | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| const FullScreenForm: React.FC<FormProps> = props => { | ||||
|   if (!props.visible) { | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   const theme = useTheme(); | ||||
|   const styles = getFullScreenStyles(theme); | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|       <div className={styles.container}> | ||||
|         <div className={styles.title}> | ||||
|           <TimePickerTitle>Absolute time range</TimePickerTitle> | ||||
|         </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> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| const EmptyRecentList = memo(() => { | ||||
|   const theme = useTheme(); | ||||
|   const styles = getEmptyListStyles(theme); | ||||
| 
 | ||||
|   return ( | ||||
|     <div className={styles.container}> | ||||
|       <div> | ||||
|         <span> | ||||
|           It looks like you haven't used this timer picker before. As soon as you enter some time intervals, recently | ||||
|           used intervals will appear here. | ||||
|         </span> | ||||
|       </div> | ||||
|       <div> | ||||
|         <a className={styles.link} href="https://grafana.com/docs/grafana/latest/reference/timerange/" target="_new"> | ||||
|           Read the documentation | ||||
|         </a> | ||||
|         <span> to find out more about how to enter custom time ranges.</span> | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| }); | ||||
| 
 | ||||
| function mapToHistoryOptions(ranges?: TimeRange[], timeZone?: TimeZone): TimeOption[] { | ||||
|   if (!Array.isArray(ranges) || ranges.length === 0) { | ||||
|     return []; | ||||
|   } | ||||
|   return ranges.slice(ranges.length - 4).map(range => mapRangeToTimeOption(range, timeZone)); | ||||
| } | ||||
|  | @ -0,0 +1,21 @@ | |||
| import React, { memo, PropsWithChildren } from 'react'; | ||||
| import { css } from 'emotion'; | ||||
| import { GrafanaTheme } from '@grafana/data'; | ||||
| import { useTheme, stylesFactory } from '../../../themes'; | ||||
| 
 | ||||
| const getStyle = stylesFactory((theme: GrafanaTheme) => { | ||||
|   return { | ||||
|     text: css` | ||||
|       font-size: ${theme.typography.size.md}; | ||||
|       font-weight: ${theme.typography.weight.semibold}; | ||||
|       color: ${theme.colors.formLabel}; | ||||
|     `,
 | ||||
|   }; | ||||
| }); | ||||
| 
 | ||||
| export const TimePickerTitle = memo<PropsWithChildren<{}>>(({ children }) => { | ||||
|   const theme = useTheme(); | ||||
|   const styles = getStyle(theme); | ||||
| 
 | ||||
|   return <span className={styles.text}>{children}</span>; | ||||
| }); | ||||
|  | @ -0,0 +1,125 @@ | |||
| import React, { FormEvent, useState, useCallback } from 'react'; | ||||
| import { TIME_FORMAT, TimeZone, isDateTime, TimeRange, DateTime } from '@grafana/data'; | ||||
| import { stringToDateTimeType, isValidTimeString } from '../time'; | ||||
| import { mapStringsToTimeRange } from './mapper'; | ||||
| import { TimePickerCalendar } from './TimePickerCalendar'; | ||||
| import Forms from '../../Forms'; | ||||
| import { isMathString } from '@grafana/data/src/datetime/datemath'; | ||||
| 
 | ||||
| interface Props { | ||||
|   isFullscreen: boolean; | ||||
|   value: TimeRange; | ||||
|   onApply: (range: TimeRange) => void; | ||||
|   timeZone?: TimeZone; | ||||
|   roundup?: boolean; | ||||
| } | ||||
| 
 | ||||
| interface InputState { | ||||
|   value: string; | ||||
|   invalid: boolean; | ||||
| } | ||||
| 
 | ||||
| const errorMessage = 'Please enter a past date or "now"'; | ||||
| 
 | ||||
| export const TimeRangeForm: React.FC<Props> = props => { | ||||
|   const { value, isFullscreen = false, timeZone, roundup } = props; | ||||
| 
 | ||||
|   const [from, setFrom] = useState<InputState>(valueToState(value.raw.from, false, timeZone)); | ||||
|   const [to, setTo] = useState<InputState>(valueToState(value.raw.to, true, timeZone)); | ||||
|   const [isOpen, setOpen] = useState(false); | ||||
| 
 | ||||
|   const onOpen = useCallback( | ||||
|     (event: FormEvent<HTMLElement>) => { | ||||
|       event.preventDefault(); | ||||
|       setOpen(true); | ||||
|     }, | ||||
|     [setOpen] | ||||
|   ); | ||||
| 
 | ||||
|   const onFocus = useCallback( | ||||
|     (event: FormEvent<HTMLElement>) => { | ||||
|       if (!isFullscreen) { | ||||
|         return; | ||||
|       } | ||||
|       onOpen(event); | ||||
|     }, | ||||
|     [isFullscreen, onOpen] | ||||
|   ); | ||||
| 
 | ||||
|   const onApply = useCallback(() => { | ||||
|     if (to.invalid || from.invalid) { | ||||
|       return; | ||||
|     } | ||||
|     props.onApply(mapStringsToTimeRange(from.value, to.value, roundup, timeZone)); | ||||
|   }, [from, to, roundup, timeZone]); | ||||
| 
 | ||||
|   const onChange = useCallback( | ||||
|     (from: string, to: string) => { | ||||
|       setFrom(valueToState(from, false, timeZone)); | ||||
|       setTo(valueToState(to, true, timeZone)); | ||||
|     }, | ||||
|     [timeZone] | ||||
|   ); | ||||
| 
 | ||||
|   const icon = isFullscreen ? null : <Forms.Button icon="fa fa-calendar" variant="secondary" onClick={onOpen} />; | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|       <Forms.Field label="From" invalid={from.invalid} error={errorMessage}> | ||||
|         <Forms.Input | ||||
|           onClick={event => event.stopPropagation()} | ||||
|           onFocus={onFocus} | ||||
|           onChange={event => setFrom(eventToState(event, false, timeZone))} | ||||
|           addonAfter={icon} | ||||
|           value={from.value} | ||||
|         /> | ||||
|       </Forms.Field> | ||||
|       <Forms.Field label="To" invalid={to.invalid} error={errorMessage}> | ||||
|         <Forms.Input | ||||
|           onClick={event => event.stopPropagation()} | ||||
|           onFocus={onFocus} | ||||
|           onChange={event => setTo(eventToState(event, true, timeZone))} | ||||
|           addonAfter={icon} | ||||
|           value={to.value} | ||||
|         /> | ||||
|       </Forms.Field> | ||||
|       <Forms.Button onClick={onApply}>Apply time range</Forms.Button> | ||||
| 
 | ||||
|       <TimePickerCalendar | ||||
|         isFullscreen={isFullscreen} | ||||
|         isOpen={isOpen} | ||||
|         from={from.value} | ||||
|         to={to.value} | ||||
|         onApply={onApply} | ||||
|         onClose={() => setOpen(false)} | ||||
|         onChange={onChange} | ||||
|       /> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| function eventToState(event: FormEvent<HTMLInputElement>, roundup?: boolean, timeZone?: TimeZone): InputState { | ||||
|   return valueToState(event.currentTarget.value, roundup, timeZone); | ||||
| } | ||||
| 
 | ||||
| function valueToState(raw: DateTime | string, roundup?: boolean, timeZone?: TimeZone): InputState { | ||||
|   const value = valueAsString(raw); | ||||
|   const invalid = !isValid(value, roundup, timeZone); | ||||
|   return { value, invalid }; | ||||
| } | ||||
| 
 | ||||
| function valueAsString(value: DateTime | string): string { | ||||
|   if (isDateTime(value)) { | ||||
|     return value.format(TIME_FORMAT); | ||||
|   } | ||||
|   return value; | ||||
| } | ||||
| 
 | ||||
| function isValid(value: string, roundup?: boolean, timeZone?: TimeZone): boolean { | ||||
|   if (isMathString(value)) { | ||||
|     return isValidTimeString(value); | ||||
|   } | ||||
| 
 | ||||
|   const parsed = stringToDateTimeType(value, roundup, timeZone); | ||||
|   return parsed.isValid(); | ||||
| } | ||||
|  | @ -0,0 +1,90 @@ | |||
| import React, { ReactNode } from 'react'; | ||||
| import { css } from 'emotion'; | ||||
| import { TimeOption, TimeZone } from '@grafana/data'; | ||||
| import { TimeRange } from '@grafana/data'; | ||||
| import { TimePickerTitle } from './TimePickerTitle'; | ||||
| import { TimeRangeOption } from './TimeRangeOption'; | ||||
| import { mapOptionToTimeRange } from './mapper'; | ||||
| import { stylesFactory } from '../../../themes'; | ||||
| 
 | ||||
| const getStyles = stylesFactory(() => { | ||||
|   return { | ||||
|     title: css` | ||||
|       display: flex; | ||||
|       align-items: center; | ||||
|       justify-content: space-between; | ||||
|       padding: 8px 16px 5px 9px; | ||||
|     `,
 | ||||
|   }; | ||||
| }); | ||||
| 
 | ||||
| const getOptionsStyles = stylesFactory(() => { | ||||
|   return { | ||||
|     grow: css` | ||||
|       flex-grow: 1; | ||||
|       align-items: flex-start; | ||||
|     `,
 | ||||
|   }; | ||||
| }); | ||||
| 
 | ||||
| interface Props { | ||||
|   title?: string; | ||||
|   options: TimeOption[]; | ||||
|   value?: TimeRange; | ||||
|   onSelect: (option: TimeRange) => void; | ||||
|   placeholderEmpty?: ReactNode; | ||||
|   timeZone?: TimeZone; | ||||
| } | ||||
| 
 | ||||
| export const TimeRangeList: React.FC<Props> = props => { | ||||
|   const styles = getStyles(); | ||||
|   const { title, options, placeholderEmpty } = props; | ||||
| 
 | ||||
|   if (typeof placeholderEmpty !== 'undefined' && options.length <= 0) { | ||||
|     return <>{placeholderEmpty}</>; | ||||
|   } | ||||
| 
 | ||||
|   if (!title) { | ||||
|     return <Options {...props} />; | ||||
|   } | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|       <div className={styles.title}> | ||||
|         <TimePickerTitle>{title}</TimePickerTitle> | ||||
|       </div> | ||||
|       <Options {...props} /> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| const Options: React.FC<Props> = ({ options, value, onSelect, timeZone }) => { | ||||
|   const styles = getOptionsStyles(); | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|       <div> | ||||
|         {options.map((option, index) => ( | ||||
|           <TimeRangeOption | ||||
|             key={keyForOption(option, index)} | ||||
|             value={option} | ||||
|             selected={isEqual(option, value)} | ||||
|             onSelect={option => onSelect(mapOptionToTimeRange(option, timeZone))} | ||||
|           /> | ||||
|         ))} | ||||
|       </div> | ||||
|       <div className={styles.grow}></div> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| function keyForOption(option: TimeOption, index: number): string { | ||||
|   return `${option.from}-${option.to}-${index}`; | ||||
| } | ||||
| 
 | ||||
| function isEqual(x: TimeOption, y?: TimeRange): boolean { | ||||
|   if (!y || !x) { | ||||
|     return false; | ||||
|   } | ||||
|   return y.raw.from === x.from && y.raw.to === x.to; | ||||
| } | ||||
|  | @ -0,0 +1,54 @@ | |||
| import React, { memo } from 'react'; | ||||
| import { css } from 'emotion'; | ||||
| import { GrafanaTheme, TimeOption } from '@grafana/data'; | ||||
| import { useTheme, stylesFactory, selectThemeVariant } from '../../../themes'; | ||||
| 
 | ||||
| const getStyles = stylesFactory((theme: GrafanaTheme) => { | ||||
|   const background = selectThemeVariant( | ||||
|     { | ||||
|       light: theme.colors.gray7, | ||||
|       dark: theme.colors.dark3, | ||||
|     }, | ||||
|     theme.type | ||||
|   ); | ||||
| 
 | ||||
|   return { | ||||
|     container: css` | ||||
|       display: flex; | ||||
|       align-items: center; | ||||
|       justify-content: space-between; | ||||
|       padding: 7px 9px 7px 9px; | ||||
|       border-left: 2px solid rgba(255, 255, 255, 0); | ||||
| 
 | ||||
|       &:hover { | ||||
|         background: ${background}; | ||||
|         border-image: linear-gradient(#f05a28 30%, #fbca0a 99%); | ||||
|         border-image-slice: 1; | ||||
|         border-style: solid; | ||||
|         border-top: 0; | ||||
|         border-right: 0; | ||||
|         border-bottom: 0; | ||||
|         border-left-width: 2px; | ||||
|         cursor: pointer; | ||||
|       } | ||||
|     `,
 | ||||
|   }; | ||||
| }); | ||||
| 
 | ||||
| interface Props { | ||||
|   value: TimeOption; | ||||
|   selected?: boolean; | ||||
|   onSelect: (option: TimeOption) => void; | ||||
| } | ||||
| 
 | ||||
| export const TimeRangeOption = memo<Props>(({ value, onSelect, selected = false }) => { | ||||
|   const theme = useTheme(); | ||||
|   const styles = getStyles(theme); | ||||
| 
 | ||||
|   return ( | ||||
|     <div className={styles.container} onClick={() => onSelect(value)} tabIndex={-1}> | ||||
|       <span>{value.display}</span> | ||||
|       {selected ? <i className="fa fa-check" /> : null} | ||||
|     </div> | ||||
|   ); | ||||
| }); | ||||
|  | @ -0,0 +1,344 @@ | |||
| // Jest Snapshot v1, https://goo.gl/fbAQLP | ||||
| 
 | ||||
| exports[`TimePickerContent renders correctly in full screen 1`] = ` | ||||
| <div | ||||
|   className="css-1fbt695" | ||||
| > | ||||
|   <div | ||||
|     className="css-13dsoi7" | ||||
|   > | ||||
|     <FullScreenForm | ||||
|       historyOptions={Array []} | ||||
|       isFullscreen={true} | ||||
|       onChange={[Function]} | ||||
|       timeZone="utc" | ||||
|       value={ | ||||
|         Object { | ||||
|           "from": "2019-12-17T07:48:27.433Z", | ||||
|           "raw": Object { | ||||
|             "from": "2019-12-17T07:48:27.433Z", | ||||
|             "to": "2019-12-18T07:48:27.433Z", | ||||
|           }, | ||||
|           "to": "2019-12-18T07:48:27.433Z", | ||||
|         } | ||||
|       } | ||||
|       visible={true} | ||||
|     /> | ||||
|   </div> | ||||
|   <CustomScrollbar | ||||
|     autoHeightMax="100%" | ||||
|     autoHeightMin="0" | ||||
|     autoHide={false} | ||||
|     autoHideDuration={200} | ||||
|     autoHideTimeout={200} | ||||
|     className="css-1o1b8dr" | ||||
|     hideTracksWhenNotNeeded={false} | ||||
|     setScrollTop={[Function]} | ||||
|   > | ||||
|     <NarrowScreenForm | ||||
|       historyOptions={Array []} | ||||
|       isFullscreen={true} | ||||
|       onChange={[Function]} | ||||
|       timeZone="utc" | ||||
|       value={ | ||||
|         Object { | ||||
|           "from": "2019-12-17T07:48:27.433Z", | ||||
|           "raw": Object { | ||||
|             "from": "2019-12-17T07:48:27.433Z", | ||||
|             "to": "2019-12-18T07:48:27.433Z", | ||||
|           }, | ||||
|           "to": "2019-12-18T07:48:27.433Z", | ||||
|         } | ||||
|       } | ||||
|       visible={false} | ||||
|     /> | ||||
|     <Component | ||||
|       onSelect={[Function]} | ||||
|       options={Array []} | ||||
|       timeZone="utc" | ||||
|       title="Relative time ranges" | ||||
|       value={ | ||||
|         Object { | ||||
|           "from": "2019-12-17T07:48:27.433Z", | ||||
|           "raw": Object { | ||||
|             "from": "2019-12-17T07:48:27.433Z", | ||||
|             "to": "2019-12-18T07:48:27.433Z", | ||||
|           }, | ||||
|           "to": "2019-12-18T07:48:27.433Z", | ||||
|         } | ||||
|       } | ||||
|     /> | ||||
|     <div | ||||
|       className="css-1ogeuxc" | ||||
|     /> | ||||
|     <Component | ||||
|       onSelect={[Function]} | ||||
|       options={Array []} | ||||
|       timeZone="utc" | ||||
|       title="Other quick ranges" | ||||
|       value={ | ||||
|         Object { | ||||
|           "from": "2019-12-17T07:48:27.433Z", | ||||
|           "raw": Object { | ||||
|             "from": "2019-12-17T07:48:27.433Z", | ||||
|             "to": "2019-12-18T07:48:27.433Z", | ||||
|           }, | ||||
|           "to": "2019-12-18T07:48:27.433Z", | ||||
|         } | ||||
|       } | ||||
|     /> | ||||
|   </CustomScrollbar> | ||||
| </div> | ||||
| `; | ||||
| 
 | ||||
| exports[`TimePickerContent renders correctly in narrow screen 1`] = ` | ||||
| <div | ||||
|   className="css-1fbt695" | ||||
| > | ||||
|   <div | ||||
|     className="css-13dsoi7" | ||||
|   > | ||||
|     <FullScreenForm | ||||
|       historyOptions={Array []} | ||||
|       isFullscreen={false} | ||||
|       onChange={[Function]} | ||||
|       timeZone="utc" | ||||
|       value={ | ||||
|         Object { | ||||
|           "from": "2019-12-17T07:48:27.433Z", | ||||
|           "raw": Object { | ||||
|             "from": "2019-12-17T07:48:27.433Z", | ||||
|             "to": "2019-12-18T07:48:27.433Z", | ||||
|           }, | ||||
|           "to": "2019-12-18T07:48:27.433Z", | ||||
|         } | ||||
|       } | ||||
|       visible={false} | ||||
|     /> | ||||
|   </div> | ||||
|   <CustomScrollbar | ||||
|     autoHeightMax="100%" | ||||
|     autoHeightMin="0" | ||||
|     autoHide={false} | ||||
|     autoHideDuration={200} | ||||
|     autoHideTimeout={200} | ||||
|     className="css-1o1b8dr" | ||||
|     hideTracksWhenNotNeeded={false} | ||||
|     setScrollTop={[Function]} | ||||
|   > | ||||
|     <NarrowScreenForm | ||||
|       historyOptions={Array []} | ||||
|       isFullscreen={false} | ||||
|       onChange={[Function]} | ||||
|       timeZone="utc" | ||||
|       value={ | ||||
|         Object { | ||||
|           "from": "2019-12-17T07:48:27.433Z", | ||||
|           "raw": Object { | ||||
|             "from": "2019-12-17T07:48:27.433Z", | ||||
|             "to": "2019-12-18T07:48:27.433Z", | ||||
|           }, | ||||
|           "to": "2019-12-18T07:48:27.433Z", | ||||
|         } | ||||
|       } | ||||
|       visible={true} | ||||
|     /> | ||||
|     <Component | ||||
|       onSelect={[Function]} | ||||
|       options={Array []} | ||||
|       timeZone="utc" | ||||
|       title="Relative time ranges" | ||||
|       value={ | ||||
|         Object { | ||||
|           "from": "2019-12-17T07:48:27.433Z", | ||||
|           "raw": Object { | ||||
|             "from": "2019-12-17T07:48:27.433Z", | ||||
|             "to": "2019-12-18T07:48:27.433Z", | ||||
|           }, | ||||
|           "to": "2019-12-18T07:48:27.433Z", | ||||
|         } | ||||
|       } | ||||
|     /> | ||||
|     <div | ||||
|       className="css-1ogeuxc" | ||||
|     /> | ||||
|     <Component | ||||
|       onSelect={[Function]} | ||||
|       options={Array []} | ||||
|       timeZone="utc" | ||||
|       title="Other quick ranges" | ||||
|       value={ | ||||
|         Object { | ||||
|           "from": "2019-12-17T07:48:27.433Z", | ||||
|           "raw": Object { | ||||
|             "from": "2019-12-17T07:48:27.433Z", | ||||
|             "to": "2019-12-18T07:48:27.433Z", | ||||
|           }, | ||||
|           "to": "2019-12-18T07:48:27.433Z", | ||||
|         } | ||||
|       } | ||||
|     /> | ||||
|   </CustomScrollbar> | ||||
| </div> | ||||
| `; | ||||
| 
 | ||||
| exports[`TimePickerContent renders recent absolute ranges correctly 1`] = ` | ||||
| <div | ||||
|   className="css-1fbt695" | ||||
| > | ||||
|   <div | ||||
|     className="css-13dsoi7" | ||||
|   > | ||||
|     <FullScreenForm | ||||
|       history={ | ||||
|         Array [ | ||||
|           Object { | ||||
|             "from": "2019-12-17T07:48:27.433Z", | ||||
|             "raw": Object { | ||||
|               "from": "2019-12-17T07:48:27.433Z", | ||||
|               "to": "2019-12-18T07:48:27.433Z", | ||||
|             }, | ||||
|             "to": "2019-12-18T07:48:27.433Z", | ||||
|           }, | ||||
|           Object { | ||||
|             "from": "2019-10-17T07:48:27.433Z", | ||||
|             "raw": Object { | ||||
|               "from": "2019-10-17T07:48:27.433Z", | ||||
|               "to": "2019-10-18T07:48:27.433Z", | ||||
|             }, | ||||
|             "to": "2019-10-18T07:48:27.433Z", | ||||
|           }, | ||||
|         ] | ||||
|       } | ||||
|       historyOptions={ | ||||
|         Array [ | ||||
|           Object { | ||||
|             "display": "2019-12-17 07:48:27 to 2019-12-18 07:48:27", | ||||
|             "from": "2019-12-17 07:48:27", | ||||
|             "section": 3, | ||||
|             "to": "2019-12-18 07:48:27", | ||||
|           }, | ||||
|           Object { | ||||
|             "display": "2019-10-17 07:48:27 to 2019-10-18 07:48:27", | ||||
|             "from": "2019-10-17 07:48:27", | ||||
|             "section": 3, | ||||
|             "to": "2019-10-18 07:48:27", | ||||
|           }, | ||||
|         ] | ||||
|       } | ||||
|       isFullscreen={true} | ||||
|       onChange={[Function]} | ||||
|       timeZone="utc" | ||||
|       value={ | ||||
|         Object { | ||||
|           "from": "2019-12-17T07:48:27.433Z", | ||||
|           "raw": Object { | ||||
|             "from": "2019-12-17T07:48:27.433Z", | ||||
|             "to": "2019-12-18T07:48:27.433Z", | ||||
|           }, | ||||
|           "to": "2019-12-18T07:48:27.433Z", | ||||
|         } | ||||
|       } | ||||
|       visible={true} | ||||
|     /> | ||||
|   </div> | ||||
|   <CustomScrollbar | ||||
|     autoHeightMax="100%" | ||||
|     autoHeightMin="0" | ||||
|     autoHide={false} | ||||
|     autoHideDuration={200} | ||||
|     autoHideTimeout={200} | ||||
|     className="css-1o1b8dr" | ||||
|     hideTracksWhenNotNeeded={false} | ||||
|     setScrollTop={[Function]} | ||||
|   > | ||||
|     <NarrowScreenForm | ||||
|       history={ | ||||
|         Array [ | ||||
|           Object { | ||||
|             "from": "2019-12-17T07:48:27.433Z", | ||||
|             "raw": Object { | ||||
|               "from": "2019-12-17T07:48:27.433Z", | ||||
|               "to": "2019-12-18T07:48:27.433Z", | ||||
|             }, | ||||
|             "to": "2019-12-18T07:48:27.433Z", | ||||
|           }, | ||||
|           Object { | ||||
|             "from": "2019-10-17T07:48:27.433Z", | ||||
|             "raw": Object { | ||||
|               "from": "2019-10-17T07:48:27.433Z", | ||||
|               "to": "2019-10-18T07:48:27.433Z", | ||||
|             }, | ||||
|             "to": "2019-10-18T07:48:27.433Z", | ||||
|           }, | ||||
|         ] | ||||
|       } | ||||
|       historyOptions={ | ||||
|         Array [ | ||||
|           Object { | ||||
|             "display": "2019-12-17 07:48:27 to 2019-12-18 07:48:27", | ||||
|             "from": "2019-12-17 07:48:27", | ||||
|             "section": 3, | ||||
|             "to": "2019-12-18 07:48:27", | ||||
|           }, | ||||
|           Object { | ||||
|             "display": "2019-10-17 07:48:27 to 2019-10-18 07:48:27", | ||||
|             "from": "2019-10-17 07:48:27", | ||||
|             "section": 3, | ||||
|             "to": "2019-10-18 07:48:27", | ||||
|           }, | ||||
|         ] | ||||
|       } | ||||
|       isFullscreen={true} | ||||
|       onChange={[Function]} | ||||
|       timeZone="utc" | ||||
|       value={ | ||||
|         Object { | ||||
|           "from": "2019-12-17T07:48:27.433Z", | ||||
|           "raw": Object { | ||||
|             "from": "2019-12-17T07:48:27.433Z", | ||||
|             "to": "2019-12-18T07:48:27.433Z", | ||||
|           }, | ||||
|           "to": "2019-12-18T07:48:27.433Z", | ||||
|         } | ||||
|       } | ||||
|       visible={false} | ||||
|     /> | ||||
|     <Component | ||||
|       onSelect={[Function]} | ||||
|       options={Array []} | ||||
|       timeZone="utc" | ||||
|       title="Relative time ranges" | ||||
|       value={ | ||||
|         Object { | ||||
|           "from": "2019-12-17T07:48:27.433Z", | ||||
|           "raw": Object { | ||||
|             "from": "2019-12-17T07:48:27.433Z", | ||||
|             "to": "2019-12-18T07:48:27.433Z", | ||||
|           }, | ||||
|           "to": "2019-12-18T07:48:27.433Z", | ||||
|         } | ||||
|       } | ||||
|     /> | ||||
|     <div | ||||
|       className="css-1ogeuxc" | ||||
|     /> | ||||
|     <Component | ||||
|       onSelect={[Function]} | ||||
|       options={Array []} | ||||
|       timeZone="utc" | ||||
|       title="Other quick ranges" | ||||
|       value={ | ||||
|         Object { | ||||
|           "from": "2019-12-17T07:48:27.433Z", | ||||
|           "raw": Object { | ||||
|             "from": "2019-12-17T07:48:27.433Z", | ||||
|             "to": "2019-12-18T07:48:27.433Z", | ||||
|           }, | ||||
|           "to": "2019-12-18T07:48:27.433Z", | ||||
|         } | ||||
|       } | ||||
|     /> | ||||
|   </CustomScrollbar> | ||||
| </div> | ||||
| `; | ||||
|  | @ -0,0 +1,35 @@ | |||
| import { GrafanaTheme } from '@grafana/data'; | ||||
| import { selectThemeVariant } from '../../../themes/selectThemeVariant'; | ||||
| 
 | ||||
| export const getThemeColors = (theme: GrafanaTheme) => { | ||||
|   return { | ||||
|     border: selectThemeVariant( | ||||
|       { | ||||
|         light: theme.colors.gray4, | ||||
|         dark: theme.colors.gray25, | ||||
|       }, | ||||
|       theme.type | ||||
|     ), | ||||
|     background: selectThemeVariant( | ||||
|       { | ||||
|         dark: theme.colors.dark2, | ||||
|         light: theme.background.dropdown, | ||||
|       }, | ||||
|       theme.type | ||||
|     ), | ||||
|     shadow: selectThemeVariant( | ||||
|       { | ||||
|         light: theme.colors.gray85, | ||||
|         dark: theme.colors.black, | ||||
|       }, | ||||
|       theme.type | ||||
|     ), | ||||
|     formBackground: selectThemeVariant( | ||||
|       { | ||||
|         dark: theme.colors.gray15, | ||||
|         light: theme.colors.gray98, | ||||
|       }, | ||||
|       theme.type | ||||
|     ), | ||||
|   }; | ||||
| }; | ||||
|  | @ -0,0 +1,95 @@ | |||
| import { | ||||
|   TimeOption, | ||||
|   TimeRange, | ||||
|   isDateTime, | ||||
|   DateTime, | ||||
|   TimeZone, | ||||
|   dateMath, | ||||
|   dateTime, | ||||
|   dateTimeForTimeZone, | ||||
|   TIME_FORMAT, | ||||
| } from '@grafana/data'; | ||||
| import { stringToDateTimeType } from '../time'; | ||||
| import { isMathString } from '@grafana/data/src/datetime/datemath'; | ||||
| 
 | ||||
| export const mapOptionToTimeRange = (option: TimeOption, timeZone?: TimeZone): TimeRange => { | ||||
|   return { | ||||
|     from: stringToDateTime(option.from, false, timeZone), | ||||
|     to: stringToDateTime(option.to, true, timeZone), | ||||
|     raw: { | ||||
|       from: option.from, | ||||
|       to: option.to, | ||||
|     }, | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export const mapRangeToTimeOption = (range: TimeRange, timeZone?: TimeZone): TimeOption => { | ||||
|   const formattedFrom = stringToDateTime(range.from, false, timeZone).format(TIME_FORMAT); | ||||
|   const formattedTo = stringToDateTime(range.to, true, timeZone).format(TIME_FORMAT); | ||||
|   const from = dateTimeToString(range.from, timeZone === 'utc'); | ||||
|   const to = dateTimeToString(range.to, timeZone === 'utc'); | ||||
| 
 | ||||
|   return { | ||||
|     from, | ||||
|     to, | ||||
|     section: 3, | ||||
|     display: `${formattedFrom} to ${formattedTo}`, | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export const mapStringsToTimeRange = (from: string, to: string, roundup?: boolean, timeZone?: TimeZone): TimeRange => { | ||||
|   const fromDate = stringToDateTimeType(from, roundup, timeZone); | ||||
|   const toDate = stringToDateTimeType(to, roundup, timeZone); | ||||
| 
 | ||||
|   if (isMathString(from) || isMathString(to)) { | ||||
|     return { | ||||
|       from: fromDate, | ||||
|       to: toDate, | ||||
|       raw: { | ||||
|         from, | ||||
|         to, | ||||
|       }, | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   return { | ||||
|     from: fromDate, | ||||
|     to: toDate, | ||||
|     raw: { | ||||
|       from: fromDate, | ||||
|       to: toDate, | ||||
|     }, | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| const stringToDateTime = (value: string | DateTime, roundUp?: boolean, timeZone?: TimeZone): DateTime => { | ||||
|   if (isDateTime(value)) { | ||||
|     if (timeZone === 'utc') { | ||||
|       return value.utc(); | ||||
|     } | ||||
|     return value; | ||||
|   } | ||||
| 
 | ||||
|   if (value.indexOf('now') !== -1) { | ||||
|     if (!dateMath.isValid(value)) { | ||||
|       return dateTime(); | ||||
|     } | ||||
| 
 | ||||
|     const parsed = dateMath.parse(value, roundUp, timeZone); | ||||
|     return parsed || dateTime(); | ||||
|   } | ||||
| 
 | ||||
|   return dateTimeForTimeZone(timeZone, value, TIME_FORMAT); | ||||
| }; | ||||
| 
 | ||||
| const dateTimeToString = (value: DateTime, isUtc: boolean): string => { | ||||
|   if (!isDateTime(value)) { | ||||
|     return value; | ||||
|   } | ||||
| 
 | ||||
|   if (isUtc) { | ||||
|     return value.utc().format(TIME_FORMAT); | ||||
|   } | ||||
| 
 | ||||
|   return value.format(TIME_FORMAT); | ||||
| }; | ||||
|  | @ -1,60 +0,0 @@ | |||
| import React, { PureComponent, ChangeEvent } from 'react'; | ||||
| import { TimeFragment, TIME_FORMAT, TimeZone, isDateTime } from '@grafana/data'; | ||||
| import { Input } from '../Input/Input'; | ||||
| import { stringToDateTimeType, isValidTimeString } from './time'; | ||||
| 
 | ||||
| export interface Props { | ||||
|   value: TimeFragment; | ||||
|   roundup?: boolean; | ||||
|   timeZone?: TimeZone; | ||||
|   onChange: (value: string, isValid: boolean) => void; | ||||
|   tabIndex?: number; | ||||
| } | ||||
| 
 | ||||
| export class TimePickerInput extends PureComponent<Props> { | ||||
|   isValid = (value: string) => { | ||||
|     const { timeZone, roundup } = this.props; | ||||
| 
 | ||||
|     if (value.indexOf('now') !== -1) { | ||||
|       const isValid = isValidTimeString(value); | ||||
|       return isValid; | ||||
|     } | ||||
| 
 | ||||
|     const parsed = stringToDateTimeType(value, roundup, timeZone); | ||||
|     const isValid = parsed.isValid(); | ||||
|     return isValid; | ||||
|   }; | ||||
| 
 | ||||
|   onChange = (event: ChangeEvent<HTMLInputElement>) => { | ||||
|     const { onChange } = this.props; | ||||
|     const value = event.target.value; | ||||
| 
 | ||||
|     onChange(value, this.isValid(value)); | ||||
|   }; | ||||
| 
 | ||||
|   valueToString = (value: TimeFragment) => { | ||||
|     if (isDateTime(value)) { | ||||
|       return value.format(TIME_FORMAT); | ||||
|     } else { | ||||
|       return value; | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   render() { | ||||
|     const { value, tabIndex } = this.props; | ||||
|     const valueString = this.valueToString(value); | ||||
|     const error = !this.isValid(valueString); | ||||
| 
 | ||||
|     return ( | ||||
|       <Input | ||||
|         type="text" | ||||
|         onChange={this.onChange} | ||||
|         onBlur={this.onChange} | ||||
|         hideErrorMessage={true} | ||||
|         value={valueString} | ||||
|         className={`time-picker-input${error ? '-error' : ''}`} | ||||
|         tabIndex={tabIndex} | ||||
|       /> | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | @ -1,35 +0,0 @@ | |||
| import React from 'react'; | ||||
| import { action } from '@storybook/addon-actions'; | ||||
| 
 | ||||
| import { storiesOf } from '@storybook/react'; | ||||
| import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; | ||||
| import { TimePickerPopover } from './TimePickerPopover'; | ||||
| import { UseState } from '../../utils/storybook/UseState'; | ||||
| import { dateTime, DateTime } from '@grafana/data'; | ||||
| 
 | ||||
| const TimePickerPopoverStories = storiesOf('UI/TimePicker/TimePickerPopover', module); | ||||
| 
 | ||||
| TimePickerPopoverStories.addDecorator(withCenteredStory); | ||||
| 
 | ||||
| TimePickerPopoverStories.add('default', () => ( | ||||
|   <UseState | ||||
|     initialState={{ | ||||
|       from: dateTime(), | ||||
|       to: dateTime(), | ||||
|       raw: { from: 'now-6h' as string | DateTime, to: 'now' as string | DateTime }, | ||||
|     }} | ||||
|   > | ||||
|     {(value, updateValue) => { | ||||
|       return ( | ||||
|         <TimePickerPopover | ||||
|           value={value} | ||||
|           timeZone="browser" | ||||
|           onChange={timeRange => { | ||||
|             action('onChange fired')(timeRange); | ||||
|             updateValue(timeRange); | ||||
|           }} | ||||
|         /> | ||||
|       ); | ||||
|     }} | ||||
|   </UseState> | ||||
| )); | ||||
|  | @ -1,122 +0,0 @@ | |||
| // Libraries
 | ||||
| import React, { Component } from 'react'; | ||||
| 
 | ||||
| // Components
 | ||||
| import { TimePickerCalendar } from './TimePickerCalendar'; | ||||
| import { TimePickerInput } from './TimePickerInput'; | ||||
| import { rawToTimeRange } from './time'; | ||||
| 
 | ||||
| // Types
 | ||||
| import { DateTime, TimeRange, TimeZone } from '@grafana/data'; | ||||
| 
 | ||||
| export interface Props { | ||||
|   value: TimeRange; | ||||
|   timeZone?: TimeZone; | ||||
|   onChange: (timeRange: TimeRange) => void; | ||||
| } | ||||
| 
 | ||||
| export interface State { | ||||
|   from: DateTime | string; | ||||
|   to: DateTime | string; | ||||
|   isFromInputValid: boolean; | ||||
|   isToInputValid: boolean; | ||||
| } | ||||
| 
 | ||||
| export class TimePickerPopover extends Component<Props, State> { | ||||
|   static popoverClassName = 'time-picker-popover'; | ||||
| 
 | ||||
|   constructor(props: Props) { | ||||
|     super(props); | ||||
| 
 | ||||
|     this.state = { | ||||
|       from: props.value.raw.from, | ||||
|       to: props.value.raw.to, | ||||
|       isFromInputValid: true, | ||||
|       isToInputValid: true, | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   onFromInputChanged = (value: string, valid: boolean) => { | ||||
|     this.setState({ from: value, isFromInputValid: valid }); | ||||
|   }; | ||||
| 
 | ||||
|   onToInputChanged = (value: string, valid: boolean) => { | ||||
|     this.setState({ to: value, isToInputValid: valid }); | ||||
|   }; | ||||
| 
 | ||||
|   onFromCalendarChanged = (value: DateTime) => { | ||||
|     this.setState({ from: value }); | ||||
|   }; | ||||
| 
 | ||||
|   onToCalendarChanged = (value: DateTime) => { | ||||
|     value.set('h', 23); | ||||
|     value.set('m', 59); | ||||
|     value.set('s', 59); | ||||
|     this.setState({ to: value }); | ||||
|   }; | ||||
| 
 | ||||
|   onApplyClick = () => { | ||||
|     const { onChange, timeZone } = this.props; | ||||
|     const { from, to } = this.state; | ||||
| 
 | ||||
|     onChange(rawToTimeRange({ from, to }, timeZone)); | ||||
|   }; | ||||
| 
 | ||||
|   render() { | ||||
|     const { timeZone } = this.props; | ||||
|     const { isFromInputValid, isToInputValid, from, to } = this.state; | ||||
| 
 | ||||
|     const isValid = isFromInputValid && isToInputValid; | ||||
| 
 | ||||
|     return ( | ||||
|       <div className={TimePickerPopover.popoverClassName}> | ||||
|         <div className="time-picker-popover-body"> | ||||
|           <div className="time-picker-popover-body-custom-ranges"> | ||||
|             <div className="time-picker-popover-body-custom-ranges-input"> | ||||
|               <div className="gf-form"> | ||||
|                 <label className="gf-form-label">From</label> | ||||
|                 <TimePickerInput | ||||
|                   roundup={false} | ||||
|                   timeZone={timeZone} | ||||
|                   value={from} | ||||
|                   onChange={this.onFromInputChanged} | ||||
|                   tabIndex={1} | ||||
|                 /> | ||||
|               </div> | ||||
|             </div> | ||||
|             <div className="time-picker-popover-body-custom-ranges-calendar"> | ||||
|               <TimePickerCalendar | ||||
|                 timeZone={timeZone} | ||||
|                 roundup={false} | ||||
|                 value={from} | ||||
|                 onChange={this.onFromCalendarChanged} | ||||
|               /> | ||||
|             </div> | ||||
|           </div> | ||||
|           <div className="time-picker-popover-body-custom-ranges"> | ||||
|             <div className="time-picker-popover-body-custom-ranges-input"> | ||||
|               <div className="gf-form"> | ||||
|                 <label className="gf-form-label">To</label> | ||||
|                 <TimePickerInput | ||||
|                   roundup={true} | ||||
|                   timeZone={timeZone} | ||||
|                   value={to} | ||||
|                   onChange={this.onToInputChanged} | ||||
|                   tabIndex={2} | ||||
|                 /> | ||||
|               </div> | ||||
|             </div> | ||||
|             <div className="time-picker-popover-body-custom-ranges-calendar"> | ||||
|               <TimePickerCalendar roundup={true} timeZone={timeZone} value={to} onChange={this.onToCalendarChanged} /> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|         <div className="time-picker-popover-footer"> | ||||
|           <button type="submit" className="btn btn-success" disabled={!isValid} onClick={this.onApplyClick}> | ||||
|             Apply | ||||
|           </button> | ||||
|         </div> | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | @ -1,266 +0,0 @@ | |||
| .time-picker { | ||||
|   display: flex; | ||||
|   flex-flow: column nowrap; | ||||
| 
 | ||||
|   .time-picker-buttons { | ||||
|     display: flex; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .time-picker-utc { | ||||
|   color: $orange; | ||||
|   font-size: 75%; | ||||
|   padding: 3px; | ||||
|   font-weight: $font-weight-semi-bold; | ||||
|   margin-left: 4px; | ||||
|   position: relative; | ||||
| } | ||||
| 
 | ||||
| .time-picker-popover { | ||||
|   display: flex; | ||||
|   justify-content: space-around; | ||||
|   border: 1px solid $popover-border-color; | ||||
|   border-radius: $border-radius; | ||||
|   background: $popover-bg; | ||||
|   color: $popover-color; | ||||
|   box-shadow: $popover-shadow; | ||||
|   position: absolute; | ||||
|   z-index: $zindex-dropdown; | ||||
|   flex-direction: column; | ||||
|   max-width: 600px; | ||||
|   top: 41px; | ||||
|   right: 0px; | ||||
| } | ||||
| 
 | ||||
| .time-picker-popover-body { | ||||
|   display: flex; | ||||
|   flex-flow: row nowrap; | ||||
|   justify-content: space-around; | ||||
|   padding: $space-md; | ||||
|   padding-bottom: 0; | ||||
| } | ||||
| 
 | ||||
| .time-picker-popover-title { | ||||
|   font-size: $font-size-md; | ||||
|   font-weight: $font-weight-semi-bold; | ||||
| } | ||||
| 
 | ||||
| .time-picker-popover-body-custom-ranges:first-child { | ||||
|   margin-right: $space-md; | ||||
| } | ||||
| 
 | ||||
| .time-picker-popover-body-custom-ranges-input { | ||||
|   display: flex; | ||||
|   flex-flow: row nowrap; | ||||
|   align-items: center; | ||||
|   margin-bottom: $space-sm; | ||||
| 
 | ||||
|   .time-picker-input-error { | ||||
|     box-shadow: inset 0 0px 5px $red; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .time-picker-popover-footer { | ||||
|   display: flex; | ||||
|   flex-flow: row nowrap; | ||||
|   justify-content: center; | ||||
|   padding: $space-md; | ||||
| } | ||||
| 
 | ||||
| .time-picker-popover-header { | ||||
|   background: $popover-header-bg; | ||||
|   padding: $space-sm; | ||||
| } | ||||
| 
 | ||||
| .time-picker-input { | ||||
|   max-width: 170px; | ||||
| } | ||||
| 
 | ||||
| .react-calendar__navigation__label { | ||||
|   line-height: 31px; | ||||
|   padding-bottom: 0; | ||||
| } | ||||
| 
 | ||||
| .react-calendar__navigation__arrow { | ||||
|   font-size: $font-size-lg; | ||||
| } | ||||
| 
 | ||||
| $arrowPaddingToBorder: 7px; | ||||
| $arrowPadding: $arrowPaddingToBorder * 3; | ||||
| 
 | ||||
| .react-calendar__navigation__next-button { | ||||
|   padding-left: $arrowPadding; | ||||
|   padding-right: $arrowPaddingToBorder; | ||||
| } | ||||
| 
 | ||||
| .react-calendar__navigation__prev-button { | ||||
|   padding-left: $arrowPaddingToBorder; | ||||
|   padding-right: $arrowPadding; | ||||
| } | ||||
| 
 | ||||
| .react-calendar__month-view__days__day--neighboringMonth abbr { | ||||
|   opacity: 0.35; | ||||
| } | ||||
| 
 | ||||
| .react-calendar__month-view__days { | ||||
|   padding: 4px; | ||||
| } | ||||
| 
 | ||||
| .time-picker-calendar { | ||||
|   border: 1px solid $input-border-color; | ||||
|   color: $black; | ||||
|   width: 260px; | ||||
| 
 | ||||
|   .react-calendar__navigation__label, | ||||
|   .react-calendar__navigation__arrow, | ||||
|   .react-calendar__navigation { | ||||
|     color: $input-color; | ||||
|     background-color: $input-label-bg; | ||||
|     border: 0; | ||||
|   } | ||||
| 
 | ||||
|   .react-calendar__month-view__weekdays { | ||||
|     background-color: $input-bg; | ||||
|     text-align: center; | ||||
| 
 | ||||
|     abbr { | ||||
|       border: 0; | ||||
|       text-decoration: none; | ||||
|       cursor: default; | ||||
|       color: $orange; | ||||
|       font-weight: $font-weight-semi-bold; | ||||
|       display: block; | ||||
|       padding: 4px 0 0 0; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   .time-picker-calendar-tile { | ||||
|     color: $text-color; | ||||
|     background-color: inherit; | ||||
|     line-height: 26px; | ||||
|     font-size: $font-size-md; | ||||
|     border: 1px solid transparent; | ||||
|     border-radius: $border-radius; | ||||
| 
 | ||||
|     &:hover { | ||||
|       box-shadow: $panel-editor-viz-item-shadow-hover; | ||||
|       background: $panel-editor-viz-item-bg-hover; | ||||
|       border: $panel-editor-viz-item-border-hover; | ||||
|       color: $text-color-strong; | ||||
|       position: relative; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   .react-calendar__month-view__days { | ||||
|     background-color: $calendar-bg-days; | ||||
|   } | ||||
| 
 | ||||
|   .react-calendar__tile--now { | ||||
|     background-color: $calendar-bg-now; | ||||
|   } | ||||
| 
 | ||||
|   .react-calendar__navigation__label, | ||||
|   .react-calendar__navigation > button:focus, | ||||
|   .time-picker-calendar-tile:focus { | ||||
|     outline: 0; | ||||
|   } | ||||
| 
 | ||||
|   .react-calendar__tile--now { | ||||
|     border-radius: $border-radius; | ||||
|   } | ||||
| 
 | ||||
|   .react-calendar__tile--active, | ||||
|   .react-calendar__tile--active:hover { | ||||
|     color: $white; | ||||
|     font-weight: $font-weight-semi-bold; | ||||
|     background: linear-gradient(0deg, $blue-base, $blue-shade); | ||||
|     box-shadow: none; | ||||
|     border: 1px solid transparent; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .time-picker-popover-custom-range-label { | ||||
|   padding-right: $space-xs; | ||||
| } | ||||
| 
 | ||||
| @include media-breakpoint-down(md) { | ||||
|   .time-picker-popover { | ||||
|     .time-picker-popover-title { | ||||
|       font-size: $font-size-md; | ||||
|     } | ||||
| 
 | ||||
|     .time-picker-popover-body { | ||||
|       padding: $space-sm; | ||||
|       display: flex; | ||||
|       flex-flow: column nowrap; | ||||
|     } | ||||
| 
 | ||||
|     .time-picker-popover-body-custom-ranges:first-child { | ||||
|       margin-right: 0; | ||||
|       margin-bottom: $space-sm; | ||||
|     } | ||||
| 
 | ||||
|     .time-picker-popover-footer { | ||||
|       display: flex; | ||||
|       flex-flow: row nowrap; | ||||
|       justify-content: flex-end; | ||||
|       margin-top: $spacer; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   .time-picker-calendar { | ||||
|     width: 320px; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .time-picker-button-select { | ||||
|   .select-button-value { | ||||
|     display: none; | ||||
| 
 | ||||
|     @include media-breakpoint-up(sm) { | ||||
|       display: inline-block; | ||||
|       max-width: 100px; | ||||
|       overflow: hidden; | ||||
|     } | ||||
| 
 | ||||
|     @include media-breakpoint-up(md) { | ||||
|       display: inline-block; | ||||
|       max-width: 150px; | ||||
|       overflow: hidden; | ||||
|     } | ||||
| 
 | ||||
|     @include media-breakpoint-up(lg) { | ||||
|       max-width: 300px; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // special rules for when within explore toolbar in split | ||||
| .explore-toolbar.splitted { | ||||
|   .time-picker-button-select { | ||||
|     .select-button-value { | ||||
|       display: none; | ||||
| 
 | ||||
|       @include media-breakpoint-up(xl) { | ||||
|         display: inline-block; | ||||
|         max-width: 100px; | ||||
|       } | ||||
| 
 | ||||
|       @media only screen and (max-width: 1545px) { | ||||
|         max-width: 300px; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .dashboard-timepicker-wrapper { | ||||
|   position: relative; | ||||
|   display: flex; | ||||
|   // this is to align popover position with explore  ( .explore-toolbar-content-item class) | ||||
|   padding: 2px 2px; | ||||
| 
 | ||||
|   .gf-form-select-box__menu { | ||||
|     right: 0; | ||||
|     left: unset; | ||||
|   } | ||||
| } | ||||
|  | @ -0,0 +1,818 @@ | |||
| // Jest Snapshot v1, https://goo.gl/fbAQLP | ||||
| 
 | ||||
| exports[`TimePicker renders buttons correctly 1`] = ` | ||||
| <div | ||||
|   className="css-16ba5ut" | ||||
| > | ||||
|   <div | ||||
|     className="css-vyoujf" | ||||
|   > | ||||
|     <button | ||||
|       className="btn navbar-button navbar-button--tight" | ||||
|       onClick={[Function]} | ||||
|     > | ||||
|       <i | ||||
|         className="fa fa-chevron-left" | ||||
|       /> | ||||
|     </button> | ||||
|     <div> | ||||
|       <Component | ||||
|         content={ | ||||
|           <TimePickerTooltip | ||||
|             timeRange={ | ||||
|               Object { | ||||
|                 "from": "2019-12-17T07:48:27.433Z", | ||||
|                 "raw": Object { | ||||
|                   "from": "2019-12-17T07:48:27.433Z", | ||||
|                   "to": "2019-12-18T07:48:27.433Z", | ||||
|                 }, | ||||
|                 "to": "2019-12-18T07:48:27.433Z", | ||||
|               } | ||||
|             } | ||||
|           /> | ||||
|         } | ||||
|         placement="bottom" | ||||
|       > | ||||
|         <button | ||||
|           aria-label="TimePicker Open Button" | ||||
|           className="btn navbar-button navbar-button--zoom" | ||||
|           onClick={[Function]} | ||||
|         > | ||||
|           <i | ||||
|             className="fa fa-clock-o fa-fw" | ||||
|           /> | ||||
|           <Component | ||||
|             onChange={[Function]} | ||||
|             onMoveBackward={[Function]} | ||||
|             onMoveForward={[Function]} | ||||
|             onZoom={[Function]} | ||||
|             theme={ | ||||
|               Object { | ||||
|                 "background": Object { | ||||
|                   "dropdown": "#1f1f20", | ||||
|                   "pageHeader": "linear-gradient(90deg, #292a2d, #000000)", | ||||
|                   "scrollbar": "#343436", | ||||
|                   "scrollbar2": "#343436", | ||||
|                 }, | ||||
|                 "border": Object { | ||||
|                   "radius": Object { | ||||
|                     "lg": "5px", | ||||
|                     "md": "3px", | ||||
|                     "sm": "2px", | ||||
|                   }, | ||||
|                   "width": Object { | ||||
|                     "sm": "1px", | ||||
|                   }, | ||||
|                 }, | ||||
|                 "breakpoints": Object { | ||||
|                   "lg": "992px", | ||||
|                   "md": "769px", | ||||
|                   "sm": "544px", | ||||
|                   "xl": "1200px", | ||||
|                   "xs": "0", | ||||
|                 }, | ||||
|                 "colors": Object { | ||||
|                   "black": "#000000", | ||||
|                   "blue": "#33b5e5", | ||||
|                   "blue77": "#1f60c4", | ||||
|                   "blue85": "#3274d9", | ||||
|                   "blue95": "#5794f2", | ||||
|                   "blueBase": "#3274d9", | ||||
|                   "blueFaint": "#041126", | ||||
|                   "blueLight": "#5794f2", | ||||
|                   "blueShade": "#1f60c4", | ||||
|                   "body": "#d8d9da", | ||||
|                   "bodyBg": "#161719", | ||||
|                   "brandDanger": "#e02f44", | ||||
|                   "brandPrimary": "#eb7b18", | ||||
|                   "brandSuccess": "#299c46", | ||||
|                   "brandWarning": "#eb7b18", | ||||
|                   "critical": "#e02f44", | ||||
|                   "dark1": "#141414", | ||||
|                   "dark10": "#424345", | ||||
|                   "dark2": "#161719", | ||||
|                   "dark3": "#1f1f20", | ||||
|                   "dark4": "#212124", | ||||
|                   "dark5": "#222426", | ||||
|                   "dark6": "#262628", | ||||
|                   "dark7": "#292a2d", | ||||
|                   "dark8": "#2f2f32", | ||||
|                   "dark9": "#343436", | ||||
|                   "formCheckboxBg": "#222426", | ||||
|                   "formCheckboxBgChecked": "#5794f2", | ||||
|                   "formCheckboxBgCheckedHover": "#3274d9", | ||||
|                   "formCheckboxCheckmark": "#343b40", | ||||
|                   "formDescription": "#9fa7b3", | ||||
|                   "formFocusOutline": "#1f60c4", | ||||
|                   "formInputBg": "#202226", | ||||
|                   "formInputBgDisabled": "#141619", | ||||
|                   "formInputBorder": "#343b40", | ||||
|                   "formInputBorderActive": "#5794f2", | ||||
|                   "formInputBorderHover": "#464c54", | ||||
|                   "formInputBorderInvalid": "#e02f44", | ||||
|                   "formInputDisabledText": "#9fa7b3", | ||||
|                   "formInputText": "#c7d0d9", | ||||
|                   "formInputTextStrong": "#c7d0d9", | ||||
|                   "formInputTextWhite": "#ffffff", | ||||
|                   "formLabel": "#9fa7b3", | ||||
|                   "formLegend": "#c7d0d9", | ||||
|                   "formSwitchBg": "#343b40", | ||||
|                   "formSwitchBgActive": "#5794f2", | ||||
|                   "formSwitchBgActiveHover": "#3274d9", | ||||
|                   "formSwitchBgDisabled": "#343b40", | ||||
|                   "formSwitchBgHover": "#464c54", | ||||
|                   "formSwitchDot": "#202226", | ||||
|                   "formValidationMessageBg": "#e02f44", | ||||
|                   "formValidationMessageText": "#ffffff", | ||||
|                   "gray05": "#0b0c0e", | ||||
|                   "gray1": "#555555", | ||||
|                   "gray10": "#141619", | ||||
|                   "gray15": "#202226", | ||||
|                   "gray2": "#8e8e8e", | ||||
|                   "gray25": "#343b40", | ||||
|                   "gray3": "#b3b3b3", | ||||
|                   "gray33": "#464c54", | ||||
|                   "gray4": "#d8d9da", | ||||
|                   "gray5": "#ececec", | ||||
|                   "gray6": "#f4f5f8", | ||||
|                   "gray7": "#fbfbfb", | ||||
|                   "gray70": "#9fa7b3", | ||||
|                   "gray85": "#c7d0d9", | ||||
|                   "gray95": "#e9edf2", | ||||
|                   "gray98": "#f7f8fa", | ||||
|                   "grayBlue": "#212327", | ||||
|                   "greenBase": "#299c46", | ||||
|                   "greenShade": "#23843b", | ||||
|                   "headingColor": "#d8d9da", | ||||
|                   "inputBlack": "#09090b", | ||||
|                   "link": "#d8d9da", | ||||
|                   "linkDisabled": "#8e8e8e", | ||||
|                   "linkExternal": "#33b5e5", | ||||
|                   "linkHover": "#ffffff", | ||||
|                   "online": "#299c46", | ||||
|                   "orange": "#eb7b18", | ||||
|                   "orangeDark": "#ff780a", | ||||
|                   "pageBg": "#161719", | ||||
|                   "pageHeaderBorder": "#343436", | ||||
|                   "purple": "#9933cc", | ||||
|                   "queryGreen": "#74e680", | ||||
|                   "queryKeyword": "#66d9ef", | ||||
|                   "queryOrange": "#eb7b18", | ||||
|                   "queryPurple": "#fe85fc", | ||||
|                   "queryRed": "#e02f44", | ||||
|                   "red": "#d44a3a", | ||||
|                   "red88": "#e02f44", | ||||
|                   "redBase": "#e02f44", | ||||
|                   "redShade": "#c4162a", | ||||
|                   "text": "#d8d9da", | ||||
|                   "textEmphasis": "#ececec", | ||||
|                   "textFaint": "#222426", | ||||
|                   "textStrong": "#ffffff", | ||||
|                   "textWeak": "#8e8e8e", | ||||
|                   "variable": "#32d1df", | ||||
|                   "warn": "#f79520", | ||||
|                   "white": "#ffffff", | ||||
|                   "yellow": "#ecbb13", | ||||
|                 }, | ||||
|                 "height": Object { | ||||
|                   "lg": "48px", | ||||
|                   "md": "32px", | ||||
|                   "sm": "24px", | ||||
|                 }, | ||||
|                 "isDark": true, | ||||
|                 "isLight": false, | ||||
|                 "name": "Grafana Dark", | ||||
|                 "panelHeaderHeight": 28, | ||||
|                 "panelPadding": 8, | ||||
|                 "shadow": Object { | ||||
|                   "pageHeader": "inset 0px -4px 14px #1f1f20", | ||||
|                 }, | ||||
|                 "spacing": Object { | ||||
|                   "d": "14px", | ||||
|                   "formButtonHeight": 32, | ||||
|                   "formFieldsetMargin": "16px", | ||||
|                   "formInputAffixPaddingHorizontal": "4px", | ||||
|                   "formInputHeight": "32px", | ||||
|                   "formInputMargin": "16px", | ||||
|                   "formInputPaddingHorizontal": "8px", | ||||
|                   "formLabelMargin": "0 0 4px 0", | ||||
|                   "formLabelPadding": "0 0 0 2px", | ||||
|                   "formLegendMargin": "0 0 16px 0", | ||||
|                   "formMargin": "32px", | ||||
|                   "formSpacingBase": 8, | ||||
|                   "formValidationMessageMargin": "4px 0 0 0", | ||||
|                   "formValidationMessagePadding": "4px 8px", | ||||
|                   "gutter": "30px", | ||||
|                   "insetSquishMd": "4px 8px", | ||||
|                   "lg": "24px", | ||||
|                   "md": "16px", | ||||
|                   "sm": "8px", | ||||
|                   "xl": "32px", | ||||
|                   "xs": "4px", | ||||
|                   "xxs": "2px", | ||||
|                 }, | ||||
|                 "type": "dark", | ||||
|                 "typography": Object { | ||||
|                   "fontFamily": Object { | ||||
|                     "monospace": "Menlo, Monaco, Consolas, 'Courier New', monospace", | ||||
|                     "sansSerif": "'Roboto', 'Helvetica Neue', Arial, sans-serif", | ||||
|                   }, | ||||
|                   "heading": Object { | ||||
|                     "h1": "28px", | ||||
|                     "h2": "24px", | ||||
|                     "h3": "21px", | ||||
|                     "h4": "18px", | ||||
|                     "h5": "16px", | ||||
|                     "h6": "14px", | ||||
|                   }, | ||||
|                   "lineHeight": Object { | ||||
|                     "lg": 1.5, | ||||
|                     "md": 1.3333333333333333, | ||||
|                     "sm": 1.1, | ||||
|                     "xs": 1, | ||||
|                   }, | ||||
|                   "link": Object { | ||||
|                     "decoration": "none", | ||||
|                     "hoverDecoration": "none", | ||||
|                   }, | ||||
|                   "size": Object { | ||||
|                     "base": "14px", | ||||
|                     "lg": "18px", | ||||
|                     "md": "14px", | ||||
|                     "sm": "12px", | ||||
|                     "xs": "10px", | ||||
|                   }, | ||||
|                   "weight": Object { | ||||
|                     "bold": 600, | ||||
|                     "light": 300, | ||||
|                     "regular": 400, | ||||
|                     "semibold": 500, | ||||
|                   }, | ||||
|                 }, | ||||
|                 "zIndex": Object { | ||||
|                   "dropdown": "1000", | ||||
|                   "modal": "1050", | ||||
|                   "modalBackdrop": "1040", | ||||
|                   "navbarFixed": "1020", | ||||
|                   "sidemenu": "1025", | ||||
|                   "tooltip": "1030", | ||||
|                   "typeahead": "1060", | ||||
|                 }, | ||||
|               } | ||||
|             } | ||||
|             value={ | ||||
|               Object { | ||||
|                 "from": "2019-12-17T07:48:27.433Z", | ||||
|                 "raw": Object { | ||||
|                   "from": "2019-12-17T07:48:27.433Z", | ||||
|                   "to": "2019-12-18T07:48:27.433Z", | ||||
|                 }, | ||||
|                 "to": "2019-12-18T07:48:27.433Z", | ||||
|               } | ||||
|             } | ||||
|           /> | ||||
|           <span | ||||
|             className="css-6x26ye" | ||||
|           > | ||||
|             <i | ||||
|               className="fa fa-caret-down fa-fw" | ||||
|             /> | ||||
|           </span> | ||||
|         </button> | ||||
|       </Component> | ||||
|     </div> | ||||
|     <button | ||||
|       className="btn navbar-button navbar-button--tight" | ||||
|       onClick={[Function]} | ||||
|     > | ||||
|       <i | ||||
|         className="fa fa-chevron-right" | ||||
|       /> | ||||
|     </button> | ||||
|     <Component | ||||
|       content={[Function]} | ||||
|       placement="bottom" | ||||
|     > | ||||
|       <button | ||||
|         className="btn navbar-button navbar-button--zoom" | ||||
|         onClick={[Function]} | ||||
|       > | ||||
|         <i | ||||
|           className="fa fa-search-minus" | ||||
|         /> | ||||
|       </button> | ||||
|     </Component> | ||||
|   </div> | ||||
| </div> | ||||
| `; | ||||
| 
 | ||||
| exports[`TimePicker renders content correctly after beeing open 1`] = ` | ||||
| <div | ||||
|   className="css-16ba5ut" | ||||
| > | ||||
|   <div | ||||
|     className="css-vyoujf" | ||||
|   > | ||||
|     <button | ||||
|       className="btn navbar-button navbar-button--tight" | ||||
|       onClick={[Function]} | ||||
|     > | ||||
|       <i | ||||
|         className="fa fa-chevron-left" | ||||
|       /> | ||||
|     </button> | ||||
|     <div> | ||||
|       <Component | ||||
|         content={ | ||||
|           <TimePickerTooltip | ||||
|             timeRange={ | ||||
|               Object { | ||||
|                 "from": "2019-12-17T07:48:27.433Z", | ||||
|                 "raw": Object { | ||||
|                   "from": "2019-12-17T07:48:27.433Z", | ||||
|                   "to": "2019-12-18T07:48:27.433Z", | ||||
|                 }, | ||||
|                 "to": "2019-12-18T07:48:27.433Z", | ||||
|               } | ||||
|             } | ||||
|           /> | ||||
|         } | ||||
|         placement="bottom" | ||||
|       > | ||||
|         <button | ||||
|           aria-label="TimePicker Open Button" | ||||
|           className="btn navbar-button navbar-button--zoom" | ||||
|           onClick={[Function]} | ||||
|         > | ||||
|           <i | ||||
|             className="fa fa-clock-o fa-fw" | ||||
|           /> | ||||
|           <Component | ||||
|             onChange={[Function]} | ||||
|             onMoveBackward={[Function]} | ||||
|             onMoveForward={[Function]} | ||||
|             onZoom={[Function]} | ||||
|             theme={ | ||||
|               Object { | ||||
|                 "background": Object { | ||||
|                   "dropdown": "#1f1f20", | ||||
|                   "pageHeader": "linear-gradient(90deg, #292a2d, #000000)", | ||||
|                   "scrollbar": "#343436", | ||||
|                   "scrollbar2": "#343436", | ||||
|                 }, | ||||
|                 "border": Object { | ||||
|                   "radius": Object { | ||||
|                     "lg": "5px", | ||||
|                     "md": "3px", | ||||
|                     "sm": "2px", | ||||
|                   }, | ||||
|                   "width": Object { | ||||
|                     "sm": "1px", | ||||
|                   }, | ||||
|                 }, | ||||
|                 "breakpoints": Object { | ||||
|                   "lg": "992px", | ||||
|                   "md": "769px", | ||||
|                   "sm": "544px", | ||||
|                   "xl": "1200px", | ||||
|                   "xs": "0", | ||||
|                 }, | ||||
|                 "colors": Object { | ||||
|                   "black": "#000000", | ||||
|                   "blue": "#33b5e5", | ||||
|                   "blue77": "#1f60c4", | ||||
|                   "blue85": "#3274d9", | ||||
|                   "blue95": "#5794f2", | ||||
|                   "blueBase": "#3274d9", | ||||
|                   "blueFaint": "#041126", | ||||
|                   "blueLight": "#5794f2", | ||||
|                   "blueShade": "#1f60c4", | ||||
|                   "body": "#d8d9da", | ||||
|                   "bodyBg": "#161719", | ||||
|                   "brandDanger": "#e02f44", | ||||
|                   "brandPrimary": "#eb7b18", | ||||
|                   "brandSuccess": "#299c46", | ||||
|                   "brandWarning": "#eb7b18", | ||||
|                   "critical": "#e02f44", | ||||
|                   "dark1": "#141414", | ||||
|                   "dark10": "#424345", | ||||
|                   "dark2": "#161719", | ||||
|                   "dark3": "#1f1f20", | ||||
|                   "dark4": "#212124", | ||||
|                   "dark5": "#222426", | ||||
|                   "dark6": "#262628", | ||||
|                   "dark7": "#292a2d", | ||||
|                   "dark8": "#2f2f32", | ||||
|                   "dark9": "#343436", | ||||
|                   "formCheckboxBg": "#222426", | ||||
|                   "formCheckboxBgChecked": "#5794f2", | ||||
|                   "formCheckboxBgCheckedHover": "#3274d9", | ||||
|                   "formCheckboxCheckmark": "#343b40", | ||||
|                   "formDescription": "#9fa7b3", | ||||
|                   "formFocusOutline": "#1f60c4", | ||||
|                   "formInputBg": "#202226", | ||||
|                   "formInputBgDisabled": "#141619", | ||||
|                   "formInputBorder": "#343b40", | ||||
|                   "formInputBorderActive": "#5794f2", | ||||
|                   "formInputBorderHover": "#464c54", | ||||
|                   "formInputBorderInvalid": "#e02f44", | ||||
|                   "formInputDisabledText": "#9fa7b3", | ||||
|                   "formInputText": "#c7d0d9", | ||||
|                   "formInputTextStrong": "#c7d0d9", | ||||
|                   "formInputTextWhite": "#ffffff", | ||||
|                   "formLabel": "#9fa7b3", | ||||
|                   "formLegend": "#c7d0d9", | ||||
|                   "formSwitchBg": "#343b40", | ||||
|                   "formSwitchBgActive": "#5794f2", | ||||
|                   "formSwitchBgActiveHover": "#3274d9", | ||||
|                   "formSwitchBgDisabled": "#343b40", | ||||
|                   "formSwitchBgHover": "#464c54", | ||||
|                   "formSwitchDot": "#202226", | ||||
|                   "formValidationMessageBg": "#e02f44", | ||||
|                   "formValidationMessageText": "#ffffff", | ||||
|                   "gray05": "#0b0c0e", | ||||
|                   "gray1": "#555555", | ||||
|                   "gray10": "#141619", | ||||
|                   "gray15": "#202226", | ||||
|                   "gray2": "#8e8e8e", | ||||
|                   "gray25": "#343b40", | ||||
|                   "gray3": "#b3b3b3", | ||||
|                   "gray33": "#464c54", | ||||
|                   "gray4": "#d8d9da", | ||||
|                   "gray5": "#ececec", | ||||
|                   "gray6": "#f4f5f8", | ||||
|                   "gray7": "#fbfbfb", | ||||
|                   "gray70": "#9fa7b3", | ||||
|                   "gray85": "#c7d0d9", | ||||
|                   "gray95": "#e9edf2", | ||||
|                   "gray98": "#f7f8fa", | ||||
|                   "grayBlue": "#212327", | ||||
|                   "greenBase": "#299c46", | ||||
|                   "greenShade": "#23843b", | ||||
|                   "headingColor": "#d8d9da", | ||||
|                   "inputBlack": "#09090b", | ||||
|                   "link": "#d8d9da", | ||||
|                   "linkDisabled": "#8e8e8e", | ||||
|                   "linkExternal": "#33b5e5", | ||||
|                   "linkHover": "#ffffff", | ||||
|                   "online": "#299c46", | ||||
|                   "orange": "#eb7b18", | ||||
|                   "orangeDark": "#ff780a", | ||||
|                   "pageBg": "#161719", | ||||
|                   "pageHeaderBorder": "#343436", | ||||
|                   "purple": "#9933cc", | ||||
|                   "queryGreen": "#74e680", | ||||
|                   "queryKeyword": "#66d9ef", | ||||
|                   "queryOrange": "#eb7b18", | ||||
|                   "queryPurple": "#fe85fc", | ||||
|                   "queryRed": "#e02f44", | ||||
|                   "red": "#d44a3a", | ||||
|                   "red88": "#e02f44", | ||||
|                   "redBase": "#e02f44", | ||||
|                   "redShade": "#c4162a", | ||||
|                   "text": "#d8d9da", | ||||
|                   "textEmphasis": "#ececec", | ||||
|                   "textFaint": "#222426", | ||||
|                   "textStrong": "#ffffff", | ||||
|                   "textWeak": "#8e8e8e", | ||||
|                   "variable": "#32d1df", | ||||
|                   "warn": "#f79520", | ||||
|                   "white": "#ffffff", | ||||
|                   "yellow": "#ecbb13", | ||||
|                 }, | ||||
|                 "height": Object { | ||||
|                   "lg": "48px", | ||||
|                   "md": "32px", | ||||
|                   "sm": "24px", | ||||
|                 }, | ||||
|                 "isDark": true, | ||||
|                 "isLight": false, | ||||
|                 "name": "Grafana Dark", | ||||
|                 "panelHeaderHeight": 28, | ||||
|                 "panelPadding": 8, | ||||
|                 "shadow": Object { | ||||
|                   "pageHeader": "inset 0px -4px 14px #1f1f20", | ||||
|                 }, | ||||
|                 "spacing": Object { | ||||
|                   "d": "14px", | ||||
|                   "formButtonHeight": 32, | ||||
|                   "formFieldsetMargin": "16px", | ||||
|                   "formInputAffixPaddingHorizontal": "4px", | ||||
|                   "formInputHeight": "32px", | ||||
|                   "formInputMargin": "16px", | ||||
|                   "formInputPaddingHorizontal": "8px", | ||||
|                   "formLabelMargin": "0 0 4px 0", | ||||
|                   "formLabelPadding": "0 0 0 2px", | ||||
|                   "formLegendMargin": "0 0 16px 0", | ||||
|                   "formMargin": "32px", | ||||
|                   "formSpacingBase": 8, | ||||
|                   "formValidationMessageMargin": "4px 0 0 0", | ||||
|                   "formValidationMessagePadding": "4px 8px", | ||||
|                   "gutter": "30px", | ||||
|                   "insetSquishMd": "4px 8px", | ||||
|                   "lg": "24px", | ||||
|                   "md": "16px", | ||||
|                   "sm": "8px", | ||||
|                   "xl": "32px", | ||||
|                   "xs": "4px", | ||||
|                   "xxs": "2px", | ||||
|                 }, | ||||
|                 "type": "dark", | ||||
|                 "typography": Object { | ||||
|                   "fontFamily": Object { | ||||
|                     "monospace": "Menlo, Monaco, Consolas, 'Courier New', monospace", | ||||
|                     "sansSerif": "'Roboto', 'Helvetica Neue', Arial, sans-serif", | ||||
|                   }, | ||||
|                   "heading": Object { | ||||
|                     "h1": "28px", | ||||
|                     "h2": "24px", | ||||
|                     "h3": "21px", | ||||
|                     "h4": "18px", | ||||
|                     "h5": "16px", | ||||
|                     "h6": "14px", | ||||
|                   }, | ||||
|                   "lineHeight": Object { | ||||
|                     "lg": 1.5, | ||||
|                     "md": 1.3333333333333333, | ||||
|                     "sm": 1.1, | ||||
|                     "xs": 1, | ||||
|                   }, | ||||
|                   "link": Object { | ||||
|                     "decoration": "none", | ||||
|                     "hoverDecoration": "none", | ||||
|                   }, | ||||
|                   "size": Object { | ||||
|                     "base": "14px", | ||||
|                     "lg": "18px", | ||||
|                     "md": "14px", | ||||
|                     "sm": "12px", | ||||
|                     "xs": "10px", | ||||
|                   }, | ||||
|                   "weight": Object { | ||||
|                     "bold": 600, | ||||
|                     "light": 300, | ||||
|                     "regular": 400, | ||||
|                     "semibold": 500, | ||||
|                   }, | ||||
|                 }, | ||||
|                 "zIndex": Object { | ||||
|                   "dropdown": "1000", | ||||
|                   "modal": "1050", | ||||
|                   "modalBackdrop": "1040", | ||||
|                   "navbarFixed": "1020", | ||||
|                   "sidemenu": "1025", | ||||
|                   "tooltip": "1030", | ||||
|                   "typeahead": "1060", | ||||
|                 }, | ||||
|               } | ||||
|             } | ||||
|             value={ | ||||
|               Object { | ||||
|                 "from": "2019-12-17T07:48:27.433Z", | ||||
|                 "raw": Object { | ||||
|                   "from": "2019-12-17T07:48:27.433Z", | ||||
|                   "to": "2019-12-18T07:48:27.433Z", | ||||
|                 }, | ||||
|                 "to": "2019-12-18T07:48:27.433Z", | ||||
|               } | ||||
|             } | ||||
|           /> | ||||
|           <span | ||||
|             className="css-6x26ye" | ||||
|           > | ||||
|             <i | ||||
|               className="fa fa-caret-up fa-fw" | ||||
|             /> | ||||
|           </span> | ||||
|         </button> | ||||
|       </Component> | ||||
|       <ClickOutsideWrapper | ||||
|         onClick={[Function]} | ||||
|       > | ||||
|         <Component | ||||
|           onChange={[Function]} | ||||
|           otherOptions={ | ||||
|             Array [ | ||||
|               Object { | ||||
|                 "display": "Yesterday", | ||||
|                 "from": "now-1d/d", | ||||
|                 "section": 3, | ||||
|                 "to": "now-1d/d", | ||||
|               }, | ||||
|               Object { | ||||
|                 "display": "Day before yesterday", | ||||
|                 "from": "now-2d/d", | ||||
|                 "section": 3, | ||||
|                 "to": "now-2d/d", | ||||
|               }, | ||||
|               Object { | ||||
|                 "display": "This day last week", | ||||
|                 "from": "now-7d/d", | ||||
|                 "section": 3, | ||||
|                 "to": "now-7d/d", | ||||
|               }, | ||||
|               Object { | ||||
|                 "display": "Previous week", | ||||
|                 "from": "now-1w/w", | ||||
|                 "section": 3, | ||||
|                 "to": "now-1w/w", | ||||
|               }, | ||||
|               Object { | ||||
|                 "display": "Previous month", | ||||
|                 "from": "now-1M/M", | ||||
|                 "section": 3, | ||||
|                 "to": "now-1M/M", | ||||
|               }, | ||||
|               Object { | ||||
|                 "display": "Previous year", | ||||
|                 "from": "now-1y/y", | ||||
|                 "section": 3, | ||||
|                 "to": "now-1y/y", | ||||
|               }, | ||||
|               Object { | ||||
|                 "display": "Today", | ||||
|                 "from": "now/d", | ||||
|                 "section": 3, | ||||
|                 "to": "now/d", | ||||
|               }, | ||||
|               Object { | ||||
|                 "display": "Today so far", | ||||
|                 "from": "now/d", | ||||
|                 "section": 3, | ||||
|                 "to": "now", | ||||
|               }, | ||||
|               Object { | ||||
|                 "display": "This week", | ||||
|                 "from": "now/w", | ||||
|                 "section": 3, | ||||
|                 "to": "now/w", | ||||
|               }, | ||||
|               Object { | ||||
|                 "display": "This week so far", | ||||
|                 "from": "now/w", | ||||
|                 "section": 3, | ||||
|                 "to": "now", | ||||
|               }, | ||||
|               Object { | ||||
|                 "display": "This month", | ||||
|                 "from": "now/M", | ||||
|                 "section": 3, | ||||
|                 "to": "now/M", | ||||
|               }, | ||||
|               Object { | ||||
|                 "display": "This month so far", | ||||
|                 "from": "now/M", | ||||
|                 "section": 3, | ||||
|                 "to": "now", | ||||
|               }, | ||||
|               Object { | ||||
|                 "display": "This year", | ||||
|                 "from": "now/y", | ||||
|                 "section": 3, | ||||
|                 "to": "now/y", | ||||
|               }, | ||||
|               Object { | ||||
|                 "display": "This year so far", | ||||
|                 "from": "now/y", | ||||
|                 "section": 3, | ||||
|                 "to": "now", | ||||
|               }, | ||||
|             ] | ||||
|           } | ||||
|           quickOptions={ | ||||
|             Array [ | ||||
|               Object { | ||||
|                 "display": "Last 5 minutes", | ||||
|                 "from": "now-5m", | ||||
|                 "section": 3, | ||||
|                 "to": "now", | ||||
|               }, | ||||
|               Object { | ||||
|                 "display": "Last 15 minutes", | ||||
|                 "from": "now-15m", | ||||
|                 "section": 3, | ||||
|                 "to": "now", | ||||
|               }, | ||||
|               Object { | ||||
|                 "display": "Last 30 minutes", | ||||
|                 "from": "now-30m", | ||||
|                 "section": 3, | ||||
|                 "to": "now", | ||||
|               }, | ||||
|               Object { | ||||
|                 "display": "Last 1 hour", | ||||
|                 "from": "now-1h", | ||||
|                 "section": 3, | ||||
|                 "to": "now", | ||||
|               }, | ||||
|               Object { | ||||
|                 "display": "Last 3 hours", | ||||
|                 "from": "now-3h", | ||||
|                 "section": 3, | ||||
|                 "to": "now", | ||||
|               }, | ||||
|               Object { | ||||
|                 "display": "Last 6 hours", | ||||
|                 "from": "now-6h", | ||||
|                 "section": 3, | ||||
|                 "to": "now", | ||||
|               }, | ||||
|               Object { | ||||
|                 "display": "Last 12 hours", | ||||
|                 "from": "now-12h", | ||||
|                 "section": 3, | ||||
|                 "to": "now", | ||||
|               }, | ||||
|               Object { | ||||
|                 "display": "Last 24 hours", | ||||
|                 "from": "now-24h", | ||||
|                 "section": 3, | ||||
|                 "to": "now", | ||||
|               }, | ||||
|               Object { | ||||
|                 "display": "Last 2 days", | ||||
|                 "from": "now-2d", | ||||
|                 "section": 3, | ||||
|                 "to": "now", | ||||
|               }, | ||||
|               Object { | ||||
|                 "display": "Last 7 days", | ||||
|                 "from": "now-7d", | ||||
|                 "section": 3, | ||||
|                 "to": "now", | ||||
|               }, | ||||
|               Object { | ||||
|                 "display": "Last 30 days", | ||||
|                 "from": "now-30d", | ||||
|                 "section": 3, | ||||
|                 "to": "now", | ||||
|               }, | ||||
|               Object { | ||||
|                 "display": "Last 90 days", | ||||
|                 "from": "now-90d", | ||||
|                 "section": 3, | ||||
|                 "to": "now", | ||||
|               }, | ||||
|               Object { | ||||
|                 "display": "Last 6 months", | ||||
|                 "from": "now-6M", | ||||
|                 "section": 3, | ||||
|                 "to": "now", | ||||
|               }, | ||||
|               Object { | ||||
|                 "display": "Last 1 year", | ||||
|                 "from": "now-1y", | ||||
|                 "section": 3, | ||||
|                 "to": "now", | ||||
|               }, | ||||
|               Object { | ||||
|                 "display": "Last 2 years", | ||||
|                 "from": "now-2y", | ||||
|                 "section": 3, | ||||
|                 "to": "now", | ||||
|               }, | ||||
|               Object { | ||||
|                 "display": "Last 5 years", | ||||
|                 "from": "now-5y", | ||||
|                 "section": 3, | ||||
|                 "to": "now", | ||||
|               }, | ||||
|             ] | ||||
|           } | ||||
|           value={ | ||||
|             Object { | ||||
|               "from": "2019-12-17T07:48:27.433Z", | ||||
|               "raw": Object { | ||||
|                 "from": "2019-12-17T07:48:27.433Z", | ||||
|                 "to": "2019-12-18T07:48:27.433Z", | ||||
|               }, | ||||
|               "to": "2019-12-18T07:48:27.433Z", | ||||
|             } | ||||
|           } | ||||
|         /> | ||||
|       </ClickOutsideWrapper> | ||||
|     </div> | ||||
|     <button | ||||
|       className="btn navbar-button navbar-button--tight" | ||||
|       onClick={[Function]} | ||||
|     > | ||||
|       <i | ||||
|         className="fa fa-chevron-right" | ||||
|       /> | ||||
|     </button> | ||||
|     <Component | ||||
|       content={[Function]} | ||||
|       placement="bottom" | ||||
|     > | ||||
|       <button | ||||
|         className="btn navbar-button navbar-button--zoom" | ||||
|         onClick={[Function]} | ||||
|       > | ||||
|         <i | ||||
|           className="fa fa-search-minus" | ||||
|         /> | ||||
|       </button> | ||||
|     </Component> | ||||
|   </div> | ||||
| </div> | ||||
| `; | ||||
|  | @ -12,6 +12,5 @@ | |||
| @import 'Table/TableInputCSV'; | ||||
| @import 'ThresholdsEditor/ThresholdsEditor'; | ||||
| @import 'TimePicker/TimeOfDayPicker'; | ||||
| @import 'TimePicker/TimePicker'; | ||||
| @import 'Tooltip/Tooltip'; | ||||
| @import 'ValueMappingsEditor/ValueMappingsEditor'; | ||||
|  |  | |||
|  | @ -0,0 +1,37 @@ | |||
| import React, { PureComponent } from 'react'; | ||||
| import store from '../../store'; | ||||
| 
 | ||||
| export interface Props<T> { | ||||
|   storageKey: string; | ||||
|   defaultValue?: T; | ||||
|   children: (value: T, onSaveToStore: (value: T) => void) => React.ReactNode; | ||||
| } | ||||
| 
 | ||||
| interface State<T> { | ||||
|   value: T; | ||||
| } | ||||
| 
 | ||||
| export class LocalStorageValueProvider<T> extends PureComponent<Props<T>, State<T>> { | ||||
|   constructor(props: Props<T>) { | ||||
|     super(props); | ||||
| 
 | ||||
|     const { storageKey, defaultValue } = props; | ||||
| 
 | ||||
|     this.state = { | ||||
|       value: store.getObject(storageKey, defaultValue), | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   onSaveToStore = (value: T) => { | ||||
|     const { storageKey } = this.props; | ||||
|     store.setObject(storageKey, value); | ||||
|     this.setState({ value }); | ||||
|   }; | ||||
| 
 | ||||
|   render() { | ||||
|     const { children } = this.props; | ||||
|     const { value } = this.state; | ||||
| 
 | ||||
|     return <>{children(value, this.onSaveToStore)}</>; | ||||
|   } | ||||
| } | ||||
|  | @ -0,0 +1 @@ | |||
| export { LocalStorageValueProvider } from './LocalStorageValueProvider'; | ||||
|  | @ -0,0 +1,42 @@ | |||
| import React from 'react'; | ||||
| import { LocalStorageValueProvider } from '../LocalStorageValueProvider'; | ||||
| import { TimeRange, isDateTime } from '@grafana/data'; | ||||
| import { Props as TimePickerProps, TimePicker } from '@grafana/ui/src/components/TimePicker/TimePicker'; | ||||
| 
 | ||||
| const LOCAL_STORAGE_KEY = 'grafana.dashboard.timepicker.history'; | ||||
| 
 | ||||
| interface Props extends Omit<TimePickerProps, 'history' | 'theme'> {} | ||||
| 
 | ||||
| export const TimePickerWithHistory: React.FC<Props> = props => { | ||||
|   return ( | ||||
|     <LocalStorageValueProvider<TimeRange[]> storageKey={LOCAL_STORAGE_KEY} defaultValue={[]}> | ||||
|       {(values, onSaveToStore) => { | ||||
|         return ( | ||||
|           <TimePicker | ||||
|             {...props} | ||||
|             history={values} | ||||
|             onChange={value => { | ||||
|               onAppendToHistory(value, values, onSaveToStore); | ||||
|               props.onChange(value); | ||||
|             }} | ||||
|           /> | ||||
|         ); | ||||
|       }} | ||||
|     </LocalStorageValueProvider> | ||||
|   ); | ||||
| }; | ||||
| function onAppendToHistory(toAppend: TimeRange, values: TimeRange[], onSaveToStore: (values: TimeRange[]) => void) { | ||||
|   if (!isAbsolute(toAppend)) { | ||||
|     return; | ||||
|   } | ||||
|   const toStore = limit([toAppend, ...values]); | ||||
|   onSaveToStore(toStore); | ||||
| } | ||||
| 
 | ||||
| function isAbsolute(value: TimeRange): boolean { | ||||
|   return isDateTime(value.raw.from) || isDateTime(value.raw.to); | ||||
| } | ||||
| 
 | ||||
| function limit(value: TimeRange[]): TimeRange[] { | ||||
|   return value.slice(0, 4); | ||||
| } | ||||
|  | @ -1,30 +1,40 @@ | |||
| // Libaries
 | ||||
| import React, { Component } from 'react'; | ||||
| import { dateMath } from '@grafana/data'; | ||||
| import { dateMath, GrafanaTheme } from '@grafana/data'; | ||||
| import { css } from 'emotion'; | ||||
| 
 | ||||
| // Types
 | ||||
| import { DashboardModel } from '../../state'; | ||||
| import { LocationState, CoreEvents } from 'app/types'; | ||||
| import { TimeRange, TimeOption, RawTimeRange } from '@grafana/data'; | ||||
| import { TimeRange } from '@grafana/data'; | ||||
| 
 | ||||
| // State
 | ||||
| import { updateLocation } from 'app/core/actions'; | ||||
| 
 | ||||
| // Components
 | ||||
| import { TimePicker, RefreshPicker } from '@grafana/ui'; | ||||
| import { RefreshPicker, withTheme, stylesFactory, Themeable } from '@grafana/ui'; | ||||
| import { TimePickerWithHistory } from 'app/core/components/TimePicker/TimePickerWithHistory'; | ||||
| 
 | ||||
| // Utils & Services
 | ||||
| import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv'; | ||||
| import { defaultSelectOptions } from '@grafana/ui/src/components/TimePicker/TimePicker'; | ||||
| 
 | ||||
| export interface Props { | ||||
| const getStyles = stylesFactory((theme: GrafanaTheme) => { | ||||
|   return { | ||||
|     container: css` | ||||
|       position: relative; | ||||
|       display: flex; | ||||
|       padding: 2px 2px; | ||||
|     `,
 | ||||
|   }; | ||||
| }); | ||||
| 
 | ||||
| export interface Props extends Themeable { | ||||
|   $injector: any; | ||||
|   dashboard: DashboardModel; | ||||
|   updateLocation: typeof updateLocation; | ||||
|   location: LocationState; | ||||
| } | ||||
| 
 | ||||
| export class DashNavTimeControls extends Component<Props> { | ||||
| class UnthemedDashNavTimeControls extends Component<Props> { | ||||
|   timeSrv: TimeSrv = getTimeSrv(); | ||||
|   $rootScope = this.props.$injector.get('$rootScope'); | ||||
| 
 | ||||
|  | @ -83,37 +93,22 @@ export class DashNavTimeControls extends Component<Props> { | |||
|     this.$rootScope.appEvent(CoreEvents.zoomOut, 2); | ||||
|   }; | ||||
| 
 | ||||
|   setActiveTimeOption = (timeOptions: TimeOption[], rawTimeRange: RawTimeRange): TimeOption[] => { | ||||
|     return timeOptions.map(option => { | ||||
|       if (option.to === rawTimeRange.to && option.from === rawTimeRange.from) { | ||||
|         return { | ||||
|           ...option, | ||||
|           active: true, | ||||
|         }; | ||||
|       } | ||||
|       return { | ||||
|         ...option, | ||||
|         active: false, | ||||
|       }; | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   render() { | ||||
|     const { dashboard } = this.props; | ||||
|     const { dashboard, theme } = this.props; | ||||
|     const intervals = dashboard.timepicker.refresh_intervals; | ||||
|     const timePickerValue = this.timeSrv.timeRange(); | ||||
|     const timeZone = dashboard.getTimezone(); | ||||
|     const styles = getStyles(theme); | ||||
| 
 | ||||
|     return ( | ||||
|       <div className="dashboard-timepicker-wrapper"> | ||||
|         <TimePicker | ||||
|       <div className={styles.container}> | ||||
|         <TimePickerWithHistory | ||||
|           value={timePickerValue} | ||||
|           onChange={this.onChangeTimePicker} | ||||
|           timeZone={timeZone} | ||||
|           onMoveBackward={this.onMoveBack} | ||||
|           onMoveForward={this.onMoveForward} | ||||
|           onZoom={this.onZoom} | ||||
|           selectOptions={this.setActiveTimeOption(defaultSelectOptions, timePickerValue.raw)} | ||||
|         /> | ||||
|         <RefreshPicker | ||||
|           onIntervalChanged={this.onChangeRefreshInterval} | ||||
|  | @ -126,3 +121,5 @@ export class DashNavTimeControls extends Component<Props> { | |||
|     ); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export const DashNavTimeControls = withTheme(UnthemedDashNavTimeControls); | ||||
|  |  | |||
|  | @ -3,16 +3,15 @@ import React, { Component } from 'react'; | |||
| 
 | ||||
| // Types
 | ||||
| import { ExploreId } from 'app/types'; | ||||
| import { TimeRange, TimeOption, TimeZone, RawTimeRange, dateTimeForTimeZone } from '@grafana/data'; | ||||
| import { TimeRange, TimeZone, RawTimeRange, dateTimeForTimeZone } from '@grafana/data'; | ||||
| 
 | ||||
| // State
 | ||||
| 
 | ||||
| // Components
 | ||||
| import { TimePicker } from '@grafana/ui'; | ||||
| import { TimeSyncButton } from './TimeSyncButton'; | ||||
| import { TimePickerWithHistory } from 'app/core/components/TimePicker/TimePickerWithHistory'; | ||||
| 
 | ||||
| // Utils & Services
 | ||||
| import { defaultSelectOptions } from '@grafana/ui/src/components/TimePicker/TimePicker'; | ||||
| import { getShiftedTimeRange, getZoomedTimeRange } from 'app/core/utils/timePicker'; | ||||
| 
 | ||||
| export interface Props { | ||||
|  | @ -56,35 +55,25 @@ export class ExploreTimeControls extends Component<Props> { | |||
|     onChangeTime(nextTimeRange); | ||||
|   }; | ||||
| 
 | ||||
|   setActiveTimeOption = (timeOptions: TimeOption[], rawTimeRange: RawTimeRange): TimeOption[] => { | ||||
|     return timeOptions.map(option => { | ||||
|       if (option.to === rawTimeRange.to && option.from === rawTimeRange.from) { | ||||
|         return { | ||||
|           ...option, | ||||
|           active: true, | ||||
|         }; | ||||
|       } | ||||
|       return { | ||||
|         ...option, | ||||
|         active: false, | ||||
|       }; | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   render() { | ||||
|     const { range, timeZone, splitted, syncedTimes, onChangeTimeSync, hideText } = this.props; | ||||
|     const timeSyncButton = splitted ? <TimeSyncButton onClick={onChangeTimeSync} isSynced={syncedTimes} /> : undefined; | ||||
|     const timePickerCommonProps = { | ||||
|       value: range, | ||||
|       onChange: this.onChangeTimePicker, | ||||
|       timeZone, | ||||
|       onMoveBackward: this.onMoveBack, | ||||
|       onMoveForward: this.onMoveForward, | ||||
|       onZoom: this.onZoom, | ||||
|       selectOptions: this.setActiveTimeOption(defaultSelectOptions, range.raw), | ||||
|       hideText, | ||||
|     }; | ||||
| 
 | ||||
|     return <TimePicker {...timePickerCommonProps} timeSyncButton={timeSyncButton} isSynced={syncedTimes} />; | ||||
|     return ( | ||||
|       <TimePickerWithHistory | ||||
|         {...timePickerCommonProps} | ||||
|         timeSyncButton={timeSyncButton} | ||||
|         isSynced={syncedTimes} | ||||
|         onChange={this.onChangeTimePicker} | ||||
|       /> | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue