diff --git a/packages/grafana-ui/src/components/Select/ButtonSelect.tsx b/packages/grafana-ui/src/components/Select/ButtonSelect.tsx index 5ed41006707..8bc80ca49df 100644 --- a/packages/grafana-ui/src/components/Select/ButtonSelect.tsx +++ b/packages/grafana-ui/src/components/Select/ButtonSelect.tsx @@ -1,9 +1,9 @@ -import React, { PureComponent } from 'react'; +import React, { PureComponent, ReactElement } from 'react'; import Select, { SelectOptionItem } from './Select'; import { PopperContent } from '../Tooltip/PopperController'; interface ButtonComponentProps { - label: string | undefined; + label: ReactElement | string | undefined; className: string | undefined; iconClass?: string; } @@ -21,7 +21,8 @@ const ButtonComponent = (buttonProps: ButtonComponentProps) => (props: any) => {
{iconClass && } {label ? label : ''} - + {!props.menuIsOpen && } + {props.menuIsOpen && }
); @@ -30,8 +31,8 @@ const ButtonComponent = (buttonProps: ButtonComponentProps) => (props: any) => { export interface Props { className: string | undefined; options: Array>; - value: SelectOptionItem; - label?: string; + value?: SelectOptionItem; + label?: ReactElement | string; iconClass?: string; components?: any; maxMenuHeight?: number; @@ -40,6 +41,7 @@ export interface Props { isMenuOpen?: boolean; onOpenMenu?: () => void; onCloseMenu?: () => void; + tabSelectsValue?: boolean; } export class ButtonSelect extends PureComponent> { @@ -61,6 +63,7 @@ export class ButtonSelect extends PureComponent> { isMenuOpen, onOpenMenu, onCloseMenu, + tabSelectsValue, } = this.props; const combinedComponents = { ...components, @@ -75,13 +78,14 @@ export class ButtonSelect extends PureComponent> { options={options} onChange={this.onChange} value={value} + isOpen={isMenuOpen} + onOpenMenu={onOpenMenu} + onCloseMenu={onCloseMenu} maxMenuHeight={maxMenuHeight} components={combinedComponents} className="gf-form-select-box-button-select" tooltipContent={tooltipContent} - isOpen={isMenuOpen} - onOpenMenu={onOpenMenu} - onCloseMenu={onCloseMenu} + tabSelectsValue={tabSelectsValue} /> ); } diff --git a/packages/grafana-ui/src/components/Select/Select.tsx b/packages/grafana-ui/src/components/Select/Select.tsx index d2c784ca883..7463ed6d8c2 100644 --- a/packages/grafana-ui/src/components/Select/Select.tsx +++ b/packages/grafana-ui/src/components/Select/Select.tsx @@ -53,6 +53,7 @@ export interface CommonProps { tooltipContent?: PopperContent; onOpenMenu?: () => void; onCloseMenu?: () => void; + tabSelectsValue?: boolean; } export interface SelectProps extends CommonProps { @@ -65,26 +66,6 @@ interface AsyncProps extends CommonProps { loadingMessage?: () => string; } -const wrapInTooltip = ( - component: React.ReactElement, - tooltipContent: PopperContent | undefined, - isMenuOpen: boolean | undefined -) => { - const showTooltip = isMenuOpen ? false : undefined; - if (tooltipContent) { - return ( - -
- {/* div needed for tooltip */} - {component} -
-
- ); - } else { - return
{component}
; - } -}; - export const MenuList = (props: any) => { return ( @@ -107,6 +88,7 @@ export class Select extends PureComponent> { isLoading: false, backspaceRemovesValue: true, maxMenuHeight: 300, + tabSelectsValue: true, components: { Option: SelectOption, SingleValue, @@ -116,20 +98,6 @@ export class Select extends PureComponent> { }, }; - onOpenMenu = () => { - const { onOpenMenu } = this.props; - if (onOpenMenu) { - onOpenMenu(); - } - }; - - onCloseMenu = () => { - const { onCloseMenu } = this.props; - if (onCloseMenu) { - onCloseMenu(); - } - }; - render() { const { defaultValue, @@ -155,6 +123,9 @@ export class Select extends PureComponent> { isOpen, components, tooltipContent, + tabSelectsValue, + onCloseMenu, + onOpenMenu, } = this.props; let widthClass = ''; @@ -164,37 +135,43 @@ export class Select extends PureComponent> { const selectClassNames = classNames('gf-form-input', 'gf-form-input--form-dropdown', widthClass, className); const selectComponents = { ...Select.defaultProps.components, ...components }; - return wrapInTooltip( - , - tooltipContent, - isOpen + + return ( + + {(onOpenMenuInternal, onCloseMenuInternal) => { + return ( + + ); + }} + ); } } @@ -239,6 +216,9 @@ export class AsyncSelect extends PureComponent> { maxMenuHeight, isMulti, tooltipContent, + onCloseMenu, + onOpenMenu, + isOpen, } = this.props; let widthClass = ''; @@ -248,43 +228,105 @@ export class AsyncSelect extends PureComponent> { const selectClassNames = classNames('gf-form-input', 'gf-form-input--form-dropdown', widthClass, className); - return wrapInTooltip( - + {(onOpenMenuInternal, onCloseMenuInternal) => { + return ( + + ); }} - defaultValue={defaultValue} - value={value} - getOptionLabel={getOptionLabel} - getOptionValue={getOptionValue} - menuShouldScrollIntoView={false} - onChange={onChange} - loadOptions={loadOptions} - isLoading={isLoading} - defaultOptions={defaultOptions} - placeholder={placeholder || 'Choose'} - styles={resetSelectStyles()} - loadingMessage={loadingMessage} - noOptionsMessage={noOptionsMessage} - isDisabled={isDisabled} - isSearchable={isSearchable} - isClearable={isClearable} - autoFocus={autoFocus} - onBlur={onBlur} - openMenuOnFocus={openMenuOnFocus} - maxMenuHeight={maxMenuHeight} - isMulti={isMulti} - backspaceRemovesValue={backspaceRemovesValue} - />, - tooltipContent, - false + ); } } +export interface TooltipWrapperProps { + children: (onOpenMenu: () => void, onCloseMenu: () => void) => React.ReactNode; + onOpenMenu?: () => void; + onCloseMenu?: () => void; + isOpen?: boolean; + tooltipContent?: PopperContent; +} + +export interface TooltipWrapperState { + isOpenInternal: boolean; +} + +export class WrapInTooltip extends PureComponent { + state: TooltipWrapperState = { + isOpenInternal: false, + }; + + onOpenMenu = () => { + const { onOpenMenu } = this.props; + if (onOpenMenu) { + onOpenMenu(); + } + this.setState({ isOpenInternal: true }); + }; + + onCloseMenu = () => { + const { onCloseMenu } = this.props; + if (onCloseMenu) { + onCloseMenu(); + } + this.setState({ isOpenInternal: false }); + }; + + render() { + const { children, isOpen, tooltipContent } = this.props; + const { isOpenInternal } = this.state; + + let showTooltip: boolean | undefined = undefined; + + if (isOpenInternal || isOpen) { + showTooltip = false; + } + + if (tooltipContent) { + return ( + +
+ {/* div needed for tooltip */} + {children(this.onOpenMenu, this.onCloseMenu)} +
+
+ ); + } else { + return
{children(this.onOpenMenu, this.onCloseMenu)}
; + } + } +} + export default Select; diff --git a/packages/grafana-ui/src/components/Select/_Select.scss b/packages/grafana-ui/src/components/Select/_Select.scss index 608c69bea13..e4b493055a1 100644 --- a/packages/grafana-ui/src/components/Select/_Select.scss +++ b/packages/grafana-ui/src/components/Select/_Select.scss @@ -53,7 +53,7 @@ $select-input-bg-disabled: $input-bg-disabled; } .gf-form-select-box__menu { - background: $input-bg; + background: $menu-dropdown-bg; box-shadow: $menu-dropdown-shadow; position: absolute; z-index: $zindex-dropdown; diff --git a/packages/grafana-ui/src/components/TimePicker/TimePicker.story.tsx b/packages/grafana-ui/src/components/TimePicker/TimePicker.story.tsx index 36c6eaf3f11..b37e9386e69 100644 --- a/packages/grafana-ui/src/components/TimePicker/TimePicker.story.tsx +++ b/packages/grafana-ui/src/components/TimePicker/TimePicker.story.tsx @@ -9,168 +9,6 @@ import { TimeFragment } from '../../types/time'; import { dateTime } from '../../utils/moment_wrapper'; const TimePickerStories = storiesOf('UI/TimePicker', module); -export const popoverOptions = { - '0': [ - { - from: 'now-2d', - to: 'now', - display: 'Last 2 days', - section: 0, - active: false, - }, - { - from: 'now-7d', - to: 'now', - display: 'Last 7 days', - section: 0, - active: false, - }, - { - from: 'now-30d', - to: 'now', - display: 'Last 30 days', - section: 0, - active: false, - }, - { - from: 'now-90d', - to: 'now', - display: 'Last 90 days', - section: 0, - active: false, - }, - { - from: 'now-6M', - to: 'now', - display: 'Last 6 months', - section: 0, - active: false, - }, - { - from: 'now-1y', - to: 'now', - display: 'Last 1 year', - section: 0, - active: false, - }, - { - from: 'now-2y', - to: 'now', - display: 'Last 2 years', - section: 0, - active: false, - }, - { - from: 'now-5y', - to: 'now', - display: 'Last 5 years', - section: 0, - active: false, - }, - ], - '1': [ - { - from: 'now-1d/d', - to: 'now-1d/d', - display: 'Yesterday', - section: 1, - active: false, - }, - { - from: 'now-2d/d', - to: 'now-2d/d', - display: 'Day before yesterday', - section: 1, - active: false, - }, - { - from: 'now-7d/d', - to: 'now-7d/d', - display: 'This day last week', - section: 1, - active: false, - }, - { - from: 'now-1w/w', - to: 'now-1w/w', - display: 'Previous week', - section: 1, - active: false, - }, - { - from: 'now-1M/M', - to: 'now-1M/M', - display: 'Previous month', - section: 1, - active: false, - }, - { - from: 'now-1y/y', - to: 'now-1y/y', - display: 'Previous year', - section: 1, - active: false, - }, - ], - '2': [ - { - from: 'now/d', - to: 'now/d', - display: 'Today', - section: 2, - active: true, - }, - { - from: 'now/d', - to: 'now', - display: 'Today so far', - section: 2, - active: false, - }, - { - from: 'now/w', - to: 'now/w', - display: 'This week', - section: 2, - active: false, - }, - { - from: 'now/w', - to: 'now', - display: 'This week so far', - section: 2, - active: false, - }, - { - from: 'now/M', - to: 'now/M', - display: 'This month', - section: 2, - active: false, - }, - { - from: 'now/M', - to: 'now', - display: 'This month so far', - section: 2, - active: false, - }, - { - from: 'now/y', - to: 'now/y', - display: 'This year', - section: 2, - active: false, - }, - { - from: 'now/y', - to: 'now', - display: 'This year so far', - section: 2, - active: false, - }, - ], -}; TimePickerStories.addDecorator(withRighAlignedStory); @@ -186,20 +24,18 @@ TimePickerStories.add('default', () => { {(value, updateValue) => { return ( { action('onChange fired')(timeRange); updateValue(timeRange); diff --git a/packages/grafana-ui/src/components/TimePicker/TimePicker.tsx b/packages/grafana-ui/src/components/TimePicker/TimePicker.tsx index 5fb625df257..454abfe4e0d 100644 --- a/packages/grafana-ui/src/components/TimePicker/TimePicker.tsx +++ b/packages/grafana-ui/src/components/TimePicker/TimePicker.tsx @@ -1,275 +1,142 @@ -import React, { PureComponent } from 'react'; +// Libraries +import React, { PureComponent, createRef } from 'react'; + +// Components import { ButtonSelect } from '../Select/ButtonSelect'; -import { mapTimeOptionToTimeRange, mapTimeRangeToRangeString } from './time'; -import { Props as TimePickerPopoverProps } from './TimePickerPopover'; -import { TimePickerOptionGroup } from './TimePickerOptionGroup'; -import { PopperContent } from '../Tooltip/PopperController'; -import { Timezone } from '../../utils/datemath'; -import { TimeRange, TimeOption, TimeOptions } from '../../types/time'; -import { SelectOptionItem } from '../Select/Select'; +import { Tooltip } from '../Tooltip/Tooltip'; +import { TimePickerPopover } from './TimePickerPopover'; +import { ClickOutsideWrapper } from '../ClickOutsideWrapper/ClickOutsideWrapper'; + +// Utils & Services import { isDateTime } from '../../utils/moment_wrapper'; +import * as rangeUtil from '../../utils/rangeutil'; +import { rawToTimeRange } from './time'; + +// Types +import { TimeRange, TimeOption, TimeZone, TIME_FORMAT } from '../../types/time'; +import { SelectOptionItem } from '../Select/Select'; export interface Props { value: TimeRange; - isTimezoneUtc: boolean; - popoverOptions: TimeOptions; selectOptions: TimeOption[]; - timezone?: Timezone; + timeZone?: TimeZone; onChange: (timeRange: TimeRange) => void; onMoveBackward: () => void; onMoveForward: () => void; onZoom: () => void; - tooltipContent?: PopperContent; } -const defaultSelectOptions = [ - { from: 'now-5m', to: 'now', display: 'Last 5 minutes', section: 3, active: false }, - { from: 'now-15m', to: 'now', display: 'Last 15 minutes', section: 3, active: false }, - { from: 'now-30m', to: 'now', display: 'Last 30 minutes', section: 3, active: false }, - { from: 'now-1h', to: 'now', display: 'Last 1 hour', section: 3, active: false }, - { from: 'now-3h', to: 'now', display: 'Last 3 hours', section: 3, active: false }, - { from: 'now-6h', to: 'now', display: 'Last 6 hours', section: 3, active: false }, - { from: 'now-12h', to: 'now', display: 'Last 12 hours', section: 3, active: false }, - { from: 'now-24h', to: 'now', display: 'Last 24 hours', section: 3, active: false }, +export const defaultSelectOptions: TimeOption[] = [ + { from: 'now-5m', to: 'now', display: 'Last 5 minutes', section: 3 }, + { from: 'now-15m', to: 'now', display: 'Last 15 minutes', section: 3 }, + { from: 'now-30m', to: 'now', display: 'Last 30 minutes', section: 3 }, + { from: 'now-1h', to: 'now', display: 'Last 1 hour', section: 3 }, + { from: 'now-3h', to: 'now', display: 'Last 3 hours', section: 3 }, + { from: 'now-6h', to: 'now', display: 'Last 6 hours', section: 3 }, + { from: 'now-12h', to: 'now', display: 'Last 12 hours', section: 3 }, + { from: 'now-24h', to: 'now', display: 'Last 24 hours', section: 3 }, + { from: 'now-2d', to: 'now', display: 'Last 2 days', section: 3 }, + { from: 'now-7d', to: 'now', display: 'Last 7 days', section: 3 }, + { from: 'now-30d', to: 'now', display: 'Last 30 days', section: 3 }, + { from: 'now-90d', to: 'now', display: 'Last 90 days', section: 3 }, + { from: 'now-6M', to: 'now', display: 'Last 6 months', section: 3 }, + { from: 'now-1y', to: 'now', display: 'Last 1 year', section: 3 }, + { from: 'now-2y', to: 'now', display: 'Last 2 years', section: 3 }, + { from: 'now-5y', to: 'now', display: 'Last 5 years', section: 3 }, + { from: 'now-1d/d', to: 'now-1d/d', display: 'Yesterday', section: 3 }, + { from: 'now-2d/d', to: 'now-2d/d', display: 'Day before yesterday', section: 3 }, + { from: 'now-7d/d', to: 'now-7d/d', display: 'This day last week', section: 3 }, + { from: 'now-1w/w', to: 'now-1w/w', display: 'Previous week', section: 3 }, + { from: 'now-1M/M', to: 'now-1M/M', display: 'Previous month', section: 3 }, + { from: 'now-1y/y', to: 'now-1y/y', display: 'Previous year', section: 3 }, + { from: 'now/d', to: 'now/d', display: 'Today', section: 3 }, + { from: 'now/d', to: 'now', display: 'Today so far', section: 3 }, + { from: 'now/w', to: 'now/w', display: 'This week', section: 3 }, + { from: 'now/w', to: 'now', display: 'This week so far', section: 3 }, + { from: 'now/M', to: 'now/M', display: 'This month', section: 3 }, + { from: 'now/M', to: 'now', display: 'This month so far', section: 3 }, + { from: 'now/y', to: 'now/y', display: 'This year', section: 3 }, + { from: 'now/y', to: 'now', display: 'This year so far', section: 3 }, ]; -const defaultPopoverOptions = { - '0': [ - { - from: 'now-2d', - to: 'now', - display: 'Last 2 days', - section: 0, - active: false, - }, - { - from: 'now-7d', - to: 'now', - display: 'Last 7 days', - section: 0, - active: false, - }, - { - from: 'now-30d', - to: 'now', - display: 'Last 30 days', - section: 0, - active: false, - }, - { - from: 'now-90d', - to: 'now', - display: 'Last 90 days', - section: 0, - active: false, - }, - { - from: 'now-6M', - to: 'now', - display: 'Last 6 months', - section: 0, - active: false, - }, - { - from: 'now-1y', - to: 'now', - display: 'Last 1 year', - section: 0, - active: false, - }, - { - from: 'now-2y', - to: 'now', - display: 'Last 2 years', - section: 0, - active: false, - }, - { - from: 'now-5y', - to: 'now', - display: 'Last 5 years', - section: 0, - active: false, - }, - ], - '1': [ - { - from: 'now-1d/d', - to: 'now-1d/d', - display: 'Yesterday', - section: 1, - active: false, - }, - { - from: 'now-2d/d', - to: 'now-2d/d', - display: 'Day before yesterday', - section: 1, - active: false, - }, - { - from: 'now-7d/d', - to: 'now-7d/d', - display: 'This day last week', - section: 1, - active: false, - }, - { - from: 'now-1w/w', - to: 'now-1w/w', - display: 'Previous week', - section: 1, - active: false, - }, - { - from: 'now-1M/M', - to: 'now-1M/M', - display: 'Previous month', - section: 1, - active: false, - }, - { - from: 'now-1y/y', - to: 'now-1y/y', - display: 'Previous year', - section: 1, - active: false, - }, - ], - '2': [ - { - from: 'now/d', - to: 'now/d', - display: 'Today', - section: 2, - active: true, - }, - { - from: 'now/d', - to: 'now', - display: 'Today so far', - section: 2, - active: false, - }, - { - from: 'now/w', - to: 'now/w', - display: 'This week', - section: 2, - active: false, - }, - { - from: 'now/w', - to: 'now', - display: 'This week so far', - section: 2, - active: false, - }, - { - from: 'now/M', - to: 'now/M', - display: 'This month', - section: 2, - active: false, - }, - { - from: 'now/M', - to: 'now', - display: 'This month so far', - section: 2, - active: false, - }, - { - from: 'now/y', - to: 'now/y', - display: 'This year', - section: 2, - active: false, - }, - { - from: 'now/y', - to: 'now', - display: 'This year so far', - section: 2, - active: false, - }, - ], +const defaultZoomOutTooltip = () => { + return ( + <> + Time range zoom out
CTRL+Z + + ); }; export interface State { - isMenuOpen: boolean; + isCustomOpen: boolean; } - export class TimePicker extends PureComponent { - static defaultSelectOptions = defaultSelectOptions; - static defaultPopoverOptions = defaultPopoverOptions; + pickerTriggerRef = createRef(); + state: State = { - isMenuOpen: false, + isCustomOpen: false, }; mapTimeOptionsToSelectOptionItems = (selectOptions: TimeOption[]) => { - const { value, popoverOptions, isTimezoneUtc, timezone } = this.props; const options = selectOptions.map(timeOption => { - return { label: timeOption.display, value: timeOption }; + return { + label: timeOption.display, + value: timeOption, + }; }); - const popoverProps: TimePickerPopoverProps = { - value, - options: popoverOptions, - isTimezoneUtc, - timezone, - }; + options.unshift({ + label: 'Custom time range', + value: { from: 'custom', to: 'custom', display: 'Custom', section: 1 }, + }); - return [ - { - label: 'Custom', - expanded: true, - options, - onPopoverOpen: () => undefined, - onPopoverClose: (timeRange: TimeRange) => this.onPopoverClose(timeRange), - popoverProps, - }, - ]; + return options; }; onSelectChanged = (item: SelectOptionItem) => { - const { isTimezoneUtc, onChange, timezone } = this.props; + const { onChange, timeZone } = this.props; - // @ts-ignore - onChange(mapTimeOptionToTimeRange(item.value, isTimezoneUtc, timezone)); + 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)); + } }; - onChangeMenuOpenState = (isOpen: boolean) => { - this.setState({ - isMenuOpen: isOpen, - }); - }; - onOpenMenu = () => this.onChangeMenuOpenState(true); - onCloseMenu = () => this.onChangeMenuOpenState(false); - - onPopoverClose = (timeRange: TimeRange) => { + onCustomChange = (timeRange: TimeRange) => { const { onChange } = this.props; onChange(timeRange); - // Here we should also close the Select but no sure how to solve this without introducing state in this component - // Edit: State introduced - this.onCloseMenu(); + this.setState({ isCustomOpen: false }); + }; + + onCloseCustom = () => { + this.setState({ isCustomOpen: false }); }; render() { - const { - selectOptions: selectTimeOptions, - value, - onMoveBackward, - onMoveForward, - onZoom, - tooltipContent, - } = this.props; + const { selectOptions: selectTimeOptions, value, onMoveBackward, onMoveForward, onZoom, timeZone } = this.props; + const { isCustomOpen } = this.state; const options = this.mapTimeOptionsToSelectOptionItems(selectTimeOptions); - const rangeString = mapTimeRangeToRangeString(value); + const currentOption = options.find(item => isTimeOptionEqualToTimeRange(item.value, value)); + const rangeString = rangeUtil.describeTimeRange(value.raw); + + const label = ( + <> + {isCustomOpen && Custom time range} + {!isCustomOpen && {rangeString}} + {timeZone === 'utc' && UTC} + + ); const isAbsolute = isDateTime(value.raw.to); return ( -
+
{isAbsolute && ( )} - + + + + + + {isCustomOpen && ( + + + + )}
); } } + +const TimePickerTooltipContent = ({ timeRange }: { timeRange: TimeRange }) => ( + <> + {timeRange.from.format(TIME_FORMAT)} +
+ to +
+ {timeRange.to.format(TIME_FORMAT)} + +); + +function isTimeOptionEqualToTimeRange(option: TimeOption, range: TimeRange): boolean { + return range.raw.from === option.from && range.raw.to === option.to; +} diff --git a/packages/grafana-ui/src/components/TimePicker/TimePickerCalendar.story.tsx b/packages/grafana-ui/src/components/TimePicker/TimePickerCalendar.story.tsx index 5a6687d17fc..a6ccb3baf3f 100644 --- a/packages/grafana-ui/src/components/TimePicker/TimePickerCalendar.story.tsx +++ b/packages/grafana-ui/src/components/TimePicker/TimePickerCalendar.story.tsx @@ -16,7 +16,7 @@ TimePickerCalendarStories.add('default', () => ( {(value, updateValue) => { return ( { action('onChange fired')(timeRange); diff --git a/packages/grafana-ui/src/components/TimePicker/TimePickerCalendar.tsx b/packages/grafana-ui/src/components/TimePicker/TimePickerCalendar.tsx index 84dcbc90b66..f0cd06e3595 100644 --- a/packages/grafana-ui/src/components/TimePicker/TimePickerCalendar.tsx +++ b/packages/grafana-ui/src/components/TimePicker/TimePickerCalendar.tsx @@ -1,45 +1,51 @@ import React, { PureComponent } from 'react'; import Calendar from 'react-calendar/dist/entry.nostyle'; -import { TimeFragment } from '../../types/time'; -import { Timezone } from '../../utils/datemath'; -import { DateTime, dateTime, isDateTime } from '../../utils/moment_wrapper'; - +import { TimeFragment, TimeZone, TIME_FORMAT } from '../../types/time'; +import { DateTime, dateTime, toUtc } from '../../utils/moment_wrapper'; import { stringToDateTimeType } from './time'; export interface Props { value: TimeFragment; - isTimezoneUtc: boolean; roundup?: boolean; - timezone?: Timezone; + timeZone?: TimeZone; onChange: (value: DateTime) => void; } export class TimePickerCalendar extends PureComponent { onCalendarChange = (date: Date | Date[]) => { - const { onChange } = this.props; + const { onChange, timeZone } = this.props; if (Array.isArray(date)) { return; } - onChange(dateTime(date)); + let newDate = dateTime(date); + + if (timeZone === 'utc') { + newDate = toUtc(newDate.format(TIME_FORMAT)); + } + + onChange(newDate); }; render() { - const { value, isTimezoneUtc, roundup, timezone } = this.props; - const dateValue = isDateTime(value) - ? value.toDate() - : stringToDateTimeType(value, isTimezoneUtc, roundup, timezone).toDate(); - const calendarValue = dateValue instanceof Date && !isNaN(dateValue.getTime()) ? dateValue : dateTime().toDate(); + const { value, roundup, timeZone } = this.props; + let date = stringToDateTimeType(value, roundup, timeZone); + + if (!date.isValid()) { + date = dateTime(); + } return ( } + prevLabel={} /> ); } diff --git a/packages/grafana-ui/src/components/TimePicker/TimePickerInput.tsx b/packages/grafana-ui/src/components/TimePicker/TimePickerInput.tsx index 26eaf2a18f7..a291aa5fa73 100644 --- a/packages/grafana-ui/src/components/TimePicker/TimePickerInput.tsx +++ b/packages/grafana-ui/src/components/TimePicker/TimePickerInput.tsx @@ -1,27 +1,27 @@ import React, { PureComponent, ChangeEvent } from 'react'; -import { TimeFragment, TIME_FORMAT } from '../../types/time'; +import { TimeFragment, TIME_FORMAT, TimeZone } from '../../types/time'; import { Input } from '../Input/Input'; import { stringToDateTimeType, isValidTimeString } from './time'; import { isDateTime } from '../../utils/moment_wrapper'; export interface Props { value: TimeFragment; - isTimezoneUtc: boolean; roundup?: boolean; - timezone?: string; + timeZone?: TimeZone; onChange: (value: string, isValid: boolean) => void; + tabIndex?: number; } export class TimePickerInput extends PureComponent { isValid = (value: string) => { - const { isTimezoneUtc } = this.props; + const { timeZone, roundup } = this.props; if (value.indexOf('now') !== -1) { const isValid = isValidTimeString(value); return isValid; } - const parsed = stringToDateTimeType(value, isTimezoneUtc); + const parsed = stringToDateTimeType(value, roundup, timeZone); const isValid = parsed.isValid(); return isValid; }; @@ -42,7 +42,7 @@ export class TimePickerInput extends PureComponent { }; render() { - const { value } = this.props; + const { value, tabIndex } = this.props; const valueString = this.valueToString(value); const error = !this.isValid(valueString); @@ -54,6 +54,7 @@ export class TimePickerInput extends PureComponent { hideErrorMessage={true} value={valueString} className={`time-picker-input${error ? '-error' : ''}`} + tabIndex={tabIndex} /> ); } diff --git a/packages/grafana-ui/src/components/TimePicker/TimePickerOptionGroup.story.tsx b/packages/grafana-ui/src/components/TimePicker/TimePickerOptionGroup.story.tsx deleted file mode 100644 index 795c536ece2..00000000000 --- a/packages/grafana-ui/src/components/TimePicker/TimePickerOptionGroup.story.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import React, { ComponentType } from 'react'; -import { storiesOf } from '@storybook/react'; -import { action } from '@storybook/addon-actions'; - -import { TimePickerOptionGroup } from './TimePickerOptionGroup'; -import { TimeRange } from '../../types/time'; -import { withRighAlignedStory } from '../../utils/storybook/withRightAlignedStory'; -import { popoverOptions } from './TimePicker.story'; -import { dateTime } from '../../utils/moment_wrapper'; - -const TimePickerOptionGroupStories = storiesOf('UI/TimePicker/TimePickerOptionGroup', module); - -TimePickerOptionGroupStories.addDecorator(withRighAlignedStory); - -const data = { - isPopoverOpen: false, - onPopoverOpen: () => { - action('onPopoverOpen fired')(); - }, - onPopoverClose: (timeRange: TimeRange) => { - action('onPopoverClose fired')(timeRange); - }, - popoverProps: { - value: { from: dateTime(), to: dateTime(), raw: { from: 'now/d', to: 'now/d' } }, - options: popoverOptions, - isTimezoneUtc: false, - onChange: (timeRange: TimeRange) => { - action('onChange fired')(timeRange); - }, - }, -}; - -TimePickerOptionGroupStories.add('default', () => ( - {}} - className={''} - cx={() => {}} - getStyles={(name, props) => ({})} - getValue={() => {}} - hasValue - isMulti={false} - options={[]} - selectOption={() => {}} - selectProps={''} - setValue={(value, action) => {}} - label={'Custom'} - children={null} - Heading={(null as any) as ComponentType} - data={data} - /> -)); diff --git a/packages/grafana-ui/src/components/TimePicker/TimePickerOptionGroup.tsx b/packages/grafana-ui/src/components/TimePicker/TimePickerOptionGroup.tsx deleted file mode 100644 index 017668b4056..00000000000 --- a/packages/grafana-ui/src/components/TimePicker/TimePickerOptionGroup.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import React, { PureComponent, createRef } from 'react'; -import { GroupProps } from 'react-select/lib/components/Group'; -import { Props as TimePickerProps, TimePickerPopover } from './TimePickerPopover'; -import { TimeRange } from '../../types/time'; -import { Popper } from '../Tooltip/Popper'; - -export interface DataProps { - onPopoverOpen: () => void; - onPopoverClose: (timeRange: TimeRange) => void; - popoverProps: TimePickerProps; -} - -interface Props extends GroupProps { - data: DataProps; -} - -interface State { - isPopoverOpen: boolean; -} - -export class TimePickerOptionGroup extends PureComponent { - pickerTriggerRef = createRef(); - state: State = { isPopoverOpen: false }; - - onClick = () => { - this.setState({ isPopoverOpen: true }); - this.props.data.onPopoverOpen(); - }; - - render() { - const { children, label } = this.props; - const { isPopoverOpen } = this.state; - const { onPopoverClose } = this.props.data; - const popover = TimePickerPopover; - const popoverElement = React.createElement(popover, { - ...this.props.data.popoverProps, - onChange: (timeRange: TimeRange) => { - onPopoverClose(timeRange); - this.setState({ isPopoverOpen: false }); - }, - }); - - return ( - <> -
-
- {label} - -
- {children} -
-
- {this.pickerTriggerRef.current && ( - - )} -
- - ); - } -} diff --git a/packages/grafana-ui/src/components/TimePicker/TimePickerPopover.story.tsx b/packages/grafana-ui/src/components/TimePicker/TimePickerPopover.story.tsx index 03dfaa1fb63..e7a397cee84 100644 --- a/packages/grafana-ui/src/components/TimePicker/TimePickerPopover.story.tsx +++ b/packages/grafana-ui/src/components/TimePicker/TimePickerPopover.story.tsx @@ -5,7 +5,6 @@ import { storiesOf } from '@storybook/react'; import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; import { TimePickerPopover } from './TimePickerPopover'; import { UseState } from '../../utils/storybook/UseState'; -import { popoverOptions } from './TimePicker.story'; import { dateTime, DateTime } from '../../utils/moment_wrapper'; const TimePickerPopoverStories = storiesOf('UI/TimePicker/TimePickerPopover', module); @@ -24,12 +23,11 @@ TimePickerPopoverStories.add('default', () => ( return ( { action('onChange fired')(timeRange); updateValue(timeRange); }} - options={popoverOptions} /> ); }} diff --git a/packages/grafana-ui/src/components/TimePicker/TimePickerPopover.tsx b/packages/grafana-ui/src/components/TimePicker/TimePickerPopover.tsx index 37c05863a03..0f255df51b0 100644 --- a/packages/grafana-ui/src/components/TimePicker/TimePickerPopover.tsx +++ b/packages/grafana-ui/src/components/TimePicker/TimePickerPopover.tsx @@ -1,167 +1,118 @@ -import React, { Component, SyntheticEvent } from 'react'; -import { TimeRange, TimeOptions, TimeOption } from '../../types/time'; +// Libraries +import React, { Component } from 'react'; +// Components import { TimePickerCalendar } from './TimePickerCalendar'; import { TimePickerInput } from './TimePickerInput'; -import { mapTimeOptionToTimeRange } from './time'; -import { Timezone } from '../../utils/datemath'; +import { rawToTimeRange } from './time'; + +// Types import { DateTime } from '../../utils/moment_wrapper'; +import { TimeRange, TimeZone } from '../../types/time'; export interface Props { value: TimeRange; - options: TimeOptions; - isTimezoneUtc: boolean; - timezone?: Timezone; - onChange?: (timeRange: TimeRange) => void; + timeZone?: TimeZone; + onChange: (timeRange: TimeRange) => void; } export interface State { - value: TimeRange; + from: DateTime | string; + to: DateTime | string; isFromInputValid: boolean; isToInputValid: boolean; } export class TimePickerPopover extends Component { static popoverClassName = 'time-picker-popover'; + constructor(props: Props) { super(props); - this.state = { value: props.value, isFromInputValid: true, isToInputValid: true }; + + this.state = { + from: props.value.raw.from, + to: props.value.raw.to, + isFromInputValid: true, + isToInputValid: true, + }; } onFromInputChanged = (value: string, valid: boolean) => { - this.setState({ - value: { ...this.state.value, raw: { ...this.state.value.raw, from: value } }, - isFromInputValid: valid, - }); + this.setState({ from: value, isFromInputValid: valid }); }; onToInputChanged = (value: string, valid: boolean) => { - this.setState({ - value: { ...this.state.value, raw: { ...this.state.value.raw, to: value } }, - isToInputValid: valid, - }); + this.setState({ to: value, isToInputValid: valid }); }; onFromCalendarChanged = (value: DateTime) => { - this.setState({ - value: { ...this.state.value, raw: { ...this.state.value.raw, from: value } }, - }); + this.setState({ from: value }); }; onToCalendarChanged = (value: DateTime) => { - this.setState({ - value: { ...this.state.value, raw: { ...this.state.value.raw, to: value } }, - }); - }; - - onTimeOptionClick = (timeOption: TimeOption) => { - const { isTimezoneUtc, timezone, onChange } = this.props; - - if (onChange) { - onChange(mapTimeOptionToTimeRange(timeOption, isTimezoneUtc, timezone)); - } + this.setState({ to: value }); }; onApplyClick = () => { - const { onChange } = this.props; - if (onChange) { - onChange(this.state.value); - } + const { onChange, timeZone } = this.props; + const { from, to } = this.state; + + onChange(rawToTimeRange({ from, to }, timeZone)); }; render() { - const { options, isTimezoneUtc, timezone } = this.props; - const { isFromInputValid, isToInputValid, value } = this.state; + const { timeZone } = this.props; + const { isFromInputValid, isToInputValid, from, to } = this.state; + const isValid = isFromInputValid && isToInputValid; return (
-
-
- Quick ranges +
+
+
+
+ + +
+
+
+ +
-
- {Object.keys(options).map(key => { - return ( - - ); - })} +
+
+
+ + +
+
+
+ +
-
-
- Custom range -
-
-
-
- From: - -
-
- -
-
-
-
- To: - -
-
- -
-
-
-
- -
+
+
); diff --git a/packages/grafana-ui/src/components/TimePicker/_TimePicker.scss b/packages/grafana-ui/src/components/TimePicker/_TimePicker.scss index ffbbd009d7c..398cac9925c 100644 --- a/packages/grafana-ui/src/components/TimePicker/_TimePicker.scss +++ b/packages/grafana-ui/src/components/TimePicker/_TimePicker.scss @@ -6,119 +6,158 @@ display: flex; } } + .time-picker-popover-popper { z-index: $zindex-timepicker-popover; } +.time-picker-utc { + color: $orange; + font-size: 75%; + padding: 3px; + font-weight: 500; + margin-left: 4px; + position: relative; +} + .time-picker-popover { display: flex; flex-flow: row nowrap; justify-content: space-around; border: 1px solid $popover-border-color; border-radius: $border-radius; - background-color: $popover-border-color; + background: $popover-bg; color: $popover-color; + box-shadow: $popover-shadow; + position: absolute; + flex-direction: column; + max-width: 600px; + top: 48px; + right: 20px; - .time-picker-popover-box { - max-width: 500px; - padding: 20px; - - ul { - padding-right: $spacer; - padding-top: $spacer; - list-style-type: none; - - li { - line-height: 22px; - display: list-item; - text-align: left; - } - - li.active { - border-bottom: 1px solid $blue; - font-weight: $font-weight-semi-bold; - } - } - - .time-picker-popover-box-body { - display: flex; - flex-flow: row nowrap; - justify-content: space-around; - } + .time-picker-popover-body { + display: flex; + flex-flow: row nowrap; + justify-content: space-around; + padding: $space-md; + padding-bottom: 0; } - .time-picker-popover-box-title { - font-size: $font-size-lg; + .time-picker-popover-title { + font-size: $font-size-md; font-weight: $font-weight-semi-bold; } - .time-picker-popover-box:first-child { - border-right: 1px ridge; + .time-picker-popover-body-custom-ranges:first-child { + margin-right: $space-md; } - .time-picker-popover-box-body-custom-ranges:first-child { - margin-right: $spacer; - } - - .time-picker-popover-box-body-custom-ranges-input { + .time-picker-popover-body-custom-ranges-input { display: flex; flex-flow: row nowrap; align-items: center; - margin: $spacer 0; + margin-bottom: $space-sm; - .our-custom-wrapper-class { - margin-left: $spacer; - width: 100%; - - .time-picker-input-error { - box-shadow: inset 0 0px 5px $red; - } + .time-picker-input-error { + box-shadow: inset 0 0px 5px $red; } } - .time-picker-popover-box-footer { + .time-picker-popover-footer { display: flex; flex-flow: row nowrap; - justify-content: flex-end; - margin-top: $spacer; + 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 $popover-border-color; - max-width: 220px; color: $black; .react-calendar__navigation__label, .react-calendar__navigation__arrow, .react-calendar__navigation { color: $input-color; - background-color: $input-bg; + background-color: $input-label-bg; border: 0; } .react-calendar__month-view__weekdays { - background-color: $popover-border-color; + background-color: $input-bg; text-align: center; abbr { border: 0; text-decoration: none; cursor: default; - color: $popover-color; + color: $orange; font-weight: $font-weight-semi-bold; + display: block; + padding: 4px 0 0 0; } } .time-picker-calendar-tile { - color: $input-color; - background-color: $input-bg; - border: 0; - line-height: 22px; + 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; + } } - button.time-picker-calendar-tile:hover { - font-weight: $font-weight-semi-bold; + .react-calendar__month-view__days { + background-color: $calendar-bg-days; + } + .react-calendar__tile--now { + background-color: $calendar-bg-now; } .react-calendar__navigation__label, @@ -128,47 +167,46 @@ } .react-calendar__tile--now { - color: $orange; + border-radius: $border-radius; } - .react-calendar__tile--active { - color: $blue; + .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; } } -@media only screen and (max-width: 1116px) { +.time-picker-popover-custom-range-label { + padding-right: $space-xs; +} + +@include media-breakpoint-down(md) { .time-picker-popover { margin-left: $spacer; display: flex; flex-flow: column nowrap; + max-width: 400px; - .time-picker-popover-box { - padding: $spacer / 2 $spacer; - - .time-picker-popover-box-title { - font-size: $font-size-md; - font-weight: $font-weight-semi-bold; - } + .time-picker-popover-title { + font-size: $font-size-md; } - .time-picker-popover-box:first-child { - border-right: none; - border-bottom: 1px ridge; + .time-picker-popover-body { + padding: $space-sm; + display: flex; + flex-flow: column nowrap; } - .time-picker-popover-box:last-child { - .time-picker-popover-box-body { - display: flex; - flex-flow: column nowrap; - - .time-picker-popover-box-body-custom-ranges:first-child { - margin: 0; - } - } + .time-picker-popover-body-custom-ranges:first-child { + margin-right: 0; + margin-bottom: $space-sm; } - .time-picker-popover-box-footer { + .time-picker-popover-footer { display: flex; flex-flow: row nowrap; justify-content: flex-end; @@ -177,13 +215,6 @@ } .time-picker-calendar { - max-width: 500px; width: 100%; } } - -@media only screen and (max-width: 746px) { - .time-picker-popover { - margin-top: 48px; - } -} diff --git a/packages/grafana-ui/src/components/TimePicker/time.ts b/packages/grafana-ui/src/components/TimePicker/time.ts index f697456eeb3..c794bf406dc 100644 --- a/packages/grafana-ui/src/components/TimePicker/time.ts +++ b/packages/grafana-ui/src/components/TimePicker/time.ts @@ -1,43 +1,38 @@ -import { TimeOption, TimeRange, TIME_FORMAT } from '../../types/time'; +import { TimeRange, TIME_FORMAT, RawTimeRange, TimeZone } from '../../types/time'; import { describeTimeRange } from '../../utils/rangeutil'; import * as dateMath from '../../utils/datemath'; -import { dateTime, DateTime, toUtc } from '../../utils/moment_wrapper'; +import { isDateTime, dateTime, DateTime, toUtc } from '../../utils/moment_wrapper'; -export const mapTimeOptionToTimeRange = ( - timeOption: TimeOption, - isTimezoneUtc: boolean, - timezone?: dateMath.Timezone -): TimeRange => { - const fromMoment = stringToDateTimeType(timeOption.from, isTimezoneUtc, false, timezone); - const toMoment = stringToDateTimeType(timeOption.to, isTimezoneUtc, true, timezone); +export const rawToTimeRange = (raw: RawTimeRange, timeZone?: TimeZone): TimeRange => { + const from = stringToDateTimeType(raw.from, false, timeZone); + const to = stringToDateTimeType(raw.to, true, timeZone); - return { from: fromMoment, to: toMoment, raw: { from: timeOption.from, to: timeOption.to } }; + return { from, to, raw }; }; -export const stringToDateTimeType = ( - value: string, - isTimezoneUtc: boolean, - roundUp?: boolean, - timezone?: dateMath.Timezone -): DateTime => { +export const stringToDateTimeType = (value: string | DateTime, roundUp?: boolean, timeZone?: TimeZone): DateTime => { + if (isDateTime(value)) { + return value; + } + if (value.indexOf('now') !== -1) { if (!dateMath.isValid(value)) { return dateTime(); } - const parsed = dateMath.parse(value, roundUp, timezone); + const parsed = dateMath.parse(value, roundUp, timeZone); return parsed || dateTime(); } - if (isTimezoneUtc) { + if (timeZone === 'utc') { return toUtc(value, TIME_FORMAT); } return dateTime(value, TIME_FORMAT); }; -export const mapTimeRangeToRangeString = (timeRange: TimeRange): string => { - return describeTimeRange(timeRange.raw); +export const mapTimeRangeToRangeString = (timeRange: RawTimeRange): string => { + return describeTimeRange(timeRange); }; export const isValidTimeString = (text: string) => dateMath.isValid(text); diff --git a/packages/grafana-ui/src/components/Tooltip/Popper.tsx b/packages/grafana-ui/src/components/Tooltip/Popper.tsx index e439ecf0437..84b92d8233a 100644 --- a/packages/grafana-ui/src/components/Tooltip/Popper.tsx +++ b/packages/grafana-ui/src/components/Tooltip/Popper.tsx @@ -26,9 +26,14 @@ interface Props extends React.HTMLAttributes { referenceElement: PopperJS.ReferenceObject; wrapperClassName?: string; renderArrow?: RenderPopperArrowFn; + eventsEnabled?: boolean; } class Popper extends PureComponent { + static defaultProps: Partial = { + eventsEnabled: true, + }; + render() { const { content, @@ -39,6 +44,8 @@ class Popper extends PureComponent { className, wrapperClassName, renderArrow, + referenceElement, + eventsEnabled, } = this.props; return ( @@ -49,7 +56,8 @@ class Popper extends PureComponent { diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 860dd5a97ab..abc4d7c2c9f 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -33,6 +33,7 @@ export { UnitPicker } from './UnitPicker/UnitPicker'; export { StatsPicker } from './StatsPicker/StatsPicker'; export { Input, InputStatus } from './Input/Input'; export { RefreshPicker } from './RefreshPicker/RefreshPicker'; +export { TimePicker } from './TimePicker/TimePicker'; export { List } from './List/List'; // Renderless diff --git a/packages/grafana-ui/src/themes/_variables.dark.scss.tmpl.ts b/packages/grafana-ui/src/themes/_variables.dark.scss.tmpl.ts index eef3e6eedf7..c51de327841 100644 --- a/packages/grafana-ui/src/themes/_variables.dark.scss.tmpl.ts +++ b/packages/grafana-ui/src/themes/_variables.dark.scss.tmpl.ts @@ -39,6 +39,7 @@ $gray-2: ${theme.colors.gray2}; $gray-3: ${theme.colors.gray3}; $gray-4: ${theme.colors.gray4}; $gray-5: ${theme.colors.gray5}; +$gray-6: ${theme.colors.gray6}; $gray-blue: ${theme.colors.grayBlue}; $input-black: #09090b; @@ -282,6 +283,7 @@ $alert-info-bg: linear-gradient(100deg, $blue-base, $blue-shade); $popover-bg: $dark-2; $popover-color: $text-color; $popover-border-color: $dark-9; +$popover-header-bg: $dark-9; $popover-shadow: 0 0 20px black; $popover-help-bg: $btn-secondary-bg; @@ -395,4 +397,8 @@ $button-toggle-group-btn-seperator-border: 1px solid $dark-2; $vertical-resize-handle-bg: $dark-10; $vertical-resize-handle-dots: $gray-1; $vertical-resize-handle-dots-hover: $gray-2; + +// Calendar +$calendar-bg-days: $input-bg; +$calendar-bg-now: $dark-10; `; diff --git a/packages/grafana-ui/src/themes/_variables.light.scss.tmpl.ts b/packages/grafana-ui/src/themes/_variables.light.scss.tmpl.ts index 96b2936354c..b37e91ba3f4 100644 --- a/packages/grafana-ui/src/themes/_variables.light.scss.tmpl.ts +++ b/packages/grafana-ui/src/themes/_variables.light.scss.tmpl.ts @@ -27,6 +27,8 @@ $black: ${theme.colors.black}; $dark-1: ${theme.colors.dark1}; $dark-2: ${theme.colors.dark2}; +$dark-4: ${theme.colors.dark4}; +$dark-10: ${theme.colors.dark10}; $gray-1: ${theme.colors.gray1}; $gray-2: ${theme.colors.gray2}; $gray-3: ${theme.colors.gray3}; @@ -269,6 +271,7 @@ $alert-info-bg: linear-gradient(100deg, $blue-base, $blue-shade); $popover-bg: $page-bg; $popover-color: $text-color; $popover-border-color: $gray-5; +$popover-header-bg: $gray-5; $popover-shadow: 0 0 20px $white; $popover-help-bg: $btn-secondary-bg; @@ -382,4 +385,8 @@ $button-toggle-group-btn-seperator-border: 1px solid $gray-6; $vertical-resize-handle-bg: $gray-4; $vertical-resize-handle-dots: $gray-3; $vertical-resize-handle-dots-hover: $gray-2; + +// Calendar +$calendar-bg-days: $white; +$calendar-bg-now: $gray-6; `; diff --git a/packages/grafana-ui/src/types/time.ts b/packages/grafana-ui/src/types/time.ts index c7120776fac..ae8137277f3 100644 --- a/packages/grafana-ui/src/types/time.ts +++ b/packages/grafana-ui/src/types/time.ts @@ -21,26 +21,17 @@ export interface IntervalValues { intervalMs: number; } -export interface TimeZone { - raw: string; - isUtc: boolean; -} +export type TimeZoneUtc = 'utc'; +export type TimeZoneBrowser = 'browser'; +export type TimeZone = TimeZoneBrowser | TimeZoneUtc | string; -export const parseTimeZone = (raw: string): TimeZone => { - return { - raw, - isUtc: raw === 'utc', - }; -}; - -export const DefaultTimeZone = parseTimeZone('browser'); +export const DefaultTimeZone: TimeZone = 'browser'; export interface TimeOption { from: string; to: string; display: string; section: number; - active: boolean; } export interface TimeOptions { diff --git a/packages/grafana-ui/src/utils/datemath.ts b/packages/grafana-ui/src/utils/datemath.ts index 5fde248313f..bd14403070b 100644 --- a/packages/grafana-ui/src/utils/datemath.ts +++ b/packages/grafana-ui/src/utils/datemath.ts @@ -1,11 +1,10 @@ import includes from 'lodash/includes'; import isDate from 'lodash/isDate'; import { DateTime, dateTime, toUtc, ISO_8601, isDateTime, DurationUnit } from '../utils/moment_wrapper'; +import { TimeZone } from '../types'; const units: DurationUnit[] = ['y', 'M', 'w', 'd', 'h', 'm', 's']; -export type Timezone = 'utc'; - /** * Parses different types input to a moment instance. There is a specific formatting language that can be used * if text arg is string. See unit tests for examples. @@ -13,7 +12,7 @@ export type Timezone = 'utc'; * @param roundUp See parseDateMath function. * @param timezone Only string 'utc' is acceptable here, for anything else, local timezone is used. */ -export function parse(text: string | DateTime | Date, roundUp?: boolean, timezone?: Timezone): DateTime | undefined { +export function parse(text: string | DateTime | Date, roundUp?: boolean, timezone?: TimeZone): DateTime | undefined { if (!text) { return undefined; } diff --git a/packages/grafana-ui/src/utils/index.ts b/packages/grafana-ui/src/utils/index.ts index 11eb7b72736..c757451dac2 100644 --- a/packages/grafana-ui/src/utils/index.ts +++ b/packages/grafana-ui/src/utils/index.ts @@ -10,6 +10,7 @@ export * from './fieldDisplay'; export * from './deprecationWarning'; export * from './logs'; export * from './labels'; +export * from './labels'; export { getMappedValue } from './valueMappings'; export * from './validate'; export { getFlotPairs } from './flotPairs'; diff --git a/packages/grafana-ui/src/utils/rangeutil.ts b/packages/grafana-ui/src/utils/rangeutil.ts index bcc27ecfd8e..01dc6a89cf8 100644 --- a/packages/grafana-ui/src/utils/rangeutil.ts +++ b/packages/grafana-ui/src/utils/rangeutil.ts @@ -51,7 +51,6 @@ const rangeOptions = [ { from: 'now-6h', to: 'now', display: 'Last 6 hours', section: 3 }, { from: 'now-12h', to: 'now', display: 'Last 12 hours', section: 3 }, { from: 'now-24h', to: 'now', display: 'Last 24 hours', section: 3 }, - { from: 'now-2d', to: 'now', display: 'Last 2 days', section: 0 }, { from: 'now-7d', to: 'now', display: 'Last 7 days', section: 0 }, { from: 'now-30d', to: 'now', display: 'Last 30 days', section: 0 }, @@ -62,7 +61,7 @@ const rangeOptions = [ { from: 'now-5y', to: 'now', display: 'Last 5 years', section: 0 }, ]; -const absoluteFormat = 'MMM D, YYYY HH:mm:ss'; +const absoluteFormat = 'YYYY-MM-DD HH:mm:ss'; const rangeIndex: any = {}; _.each(rangeOptions, (frame: any) => { diff --git a/public/app/core/specs/rangeutil.test.ts b/public/app/core/specs/rangeutil.test.ts index d23bbf9f0eb..3ecdb8b6c71 100644 --- a/public/app/core/specs/rangeutil.test.ts +++ b/public/app/core/specs/rangeutil.test.ts @@ -72,7 +72,7 @@ describe('rangeUtil', () => { from: dateTime([2014, 10, 10, 2, 3, 4]), to: 'now', }); - expect(text).toBe('Nov 10, 2014 02:03:04 to a few seconds ago'); + expect(text).toBe('2014-11-10 02:03:04 to a few seconds ago'); }); it('Date range with absolute to relative', () => { @@ -80,7 +80,7 @@ describe('rangeUtil', () => { from: dateTime([2014, 10, 10, 2, 3, 4]), to: 'now-1d', }); - expect(text).toBe('Nov 10, 2014 02:03:04 to a day ago'); + expect(text).toBe('2014-11-10 02:03:04 to a day ago'); }); it('Date range with relative to absolute', () => { @@ -88,7 +88,7 @@ describe('rangeUtil', () => { from: 'now-7d', to: dateTime([2014, 10, 10, 2, 3, 4]), }); - expect(text).toBe('7 days ago to Nov 10, 2014 02:03:04'); + expect(text).toBe('7 days ago to 2014-11-10 02:03:04'); }); it('Date range with non matching default ranges', () => { diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index 811950a9251..135490e6305 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -366,8 +366,8 @@ export const getQueryKeys = (queries: DataQuery[], datasourceInstance: DataSourc export const getTimeRange = (timeZone: TimeZone, rawRange: RawTimeRange): TimeRange => { return { - from: dateMath.parse(rawRange.from, false, timeZone.raw as any), - to: dateMath.parse(rawRange.to, true, timeZone.raw as any), + from: dateMath.parse(rawRange.from, false, timeZone as any), + to: dateMath.parse(rawRange.to, true, timeZone as any), raw: rawRange, }; }; @@ -406,8 +406,8 @@ export const getTimeRangeFromUrl = (range: RawTimeRange, timeZone: TimeZone): Ti }; return { - from: dateMath.parse(raw.from, false, timeZone.raw as any), - to: dateMath.parse(raw.to, true, timeZone.raw as any), + from: dateMath.parse(raw.from, false, timeZone as any), + to: dateMath.parse(raw.to, true, timeZone as any), raw, }; }; diff --git a/public/app/features/dashboard/components/DashNav/DashNav.tsx b/public/app/features/dashboard/components/DashNav/DashNav.tsx index 8db88e9ba55..89f28fe4041 100644 --- a/public/app/features/dashboard/components/DashNav/DashNav.tsx +++ b/public/app/features/dashboard/components/DashNav/DashNav.tsx @@ -3,7 +3,6 @@ import React, { PureComponent } from 'react'; import { connect } from 'react-redux'; // Utils & Services -import { AngularComponent, getAngularLoader } from '@grafana/runtime'; import { appEvents } from 'app/core/app_events'; import { PlaylistSrv } from 'app/features/playlist/playlist_srv'; @@ -36,8 +35,6 @@ export interface StateProps { type Props = StateProps & OwnProps; export class DashNav extends PureComponent { - timePickerEl: HTMLElement; - timepickerCmp: AngularComponent; playlistSrv: PlaylistSrv; constructor(props: Props) { @@ -45,21 +42,6 @@ export class DashNav extends PureComponent { this.playlistSrv = this.props.$injector.get('playlistSrv'); } - componentDidMount() { - const loader = getAngularLoader(); - const template = - ''; - const scopeProps = { dashboard: this.props.dashboard }; - - this.timepickerCmp = loader.load(this.timePickerEl, scopeProps, template); - } - - componentWillUnmount() { - if (this.timepickerCmp) { - this.timepickerCmp.destroy(); - } - } - onDahboardNameClick = () => { appEvents.emit('show-dash-search'); }; @@ -187,7 +169,7 @@ export class DashNav extends PureComponent { } render() { - const { dashboard, onAddPanel, location } = this.props; + const { dashboard, onAddPanel, location, $injector } = this.props; const { canStar, canSave, canShare, showSettings, isStarred } = dashboard.meta; const { snapshot } = dashboard; const snapshotUrl = snapshot && snapshot.originalUrl; @@ -281,8 +263,12 @@ export class DashNav extends PureComponent { {!dashboard.timepicker.hidden && (
-
(this.timePickerEl = element)} /> - +
)}
diff --git a/public/app/features/dashboard/components/DashNav/DashNavTimeControls.tsx b/public/app/features/dashboard/components/DashNav/DashNavTimeControls.tsx index 42d3a64fc4b..065596d73df 100644 --- a/public/app/features/dashboard/components/DashNav/DashNavTimeControls.tsx +++ b/public/app/features/dashboard/components/DashNav/DashNavTimeControls.tsx @@ -1,20 +1,24 @@ // Libaries import React, { Component } from 'react'; +import { toUtc } from '@grafana/ui/src/utils/moment_wrapper'; // Types import { DashboardModel } from '../../state'; import { LocationState } from 'app/types'; +import { TimeRange, TimeOption } from '@grafana/ui'; // State import { updateLocation } from 'app/core/actions'; // Components -import { RefreshPicker } from '@grafana/ui'; +import { TimePicker, RefreshPicker, RawTimeRange } from '@grafana/ui'; // Utils & Services import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv'; +import { defaultSelectOptions } from '@grafana/ui/src/components/TimePicker/TimePicker'; export interface Props { + $injector: any; dashboard: DashboardModel; updateLocation: typeof updateLocation; location: LocationState; @@ -22,6 +26,7 @@ export interface Props { export class DashNavTimeControls extends Component { timeSrv: TimeSrv = getTimeSrv(); + $rootScope = this.props.$injector.get('$rootScope'); get refreshParamInUrl(): string { return this.props.location.query.refresh as string; @@ -37,17 +42,92 @@ export class DashNavTimeControls extends Component { return Promise.resolve(); }; + onMoveTimePicker = (direction: number) => { + const range = this.timeSrv.timeRange(); + const timespan = (range.to.valueOf() - range.from.valueOf()) / 2; + let to: number, from: number; + + if (direction === -1) { + to = range.to.valueOf() - timespan; + from = range.from.valueOf() - timespan; + } else if (direction === 1) { + to = range.to.valueOf() + timespan; + from = range.from.valueOf() + timespan; + if (to > Date.now() && range.to.valueOf() < Date.now()) { + to = Date.now(); + from = range.from.valueOf(); + } + } else { + to = range.to.valueOf(); + from = range.from.valueOf(); + } + + this.timeSrv.setTime({ + from: toUtc(from), + to: toUtc(to), + }); + }; + + onMoveForward = () => this.onMoveTimePicker(1); + onMoveBack = () => this.onMoveTimePicker(-1); + + onChangeTimePicker = (timeRange: TimeRange) => { + const { dashboard } = this.props; + const panel = dashboard.timepicker; + const hasDelay = panel.nowDelay && timeRange.raw.to === 'now'; + + const nextRange = { + from: timeRange.raw.from, + to: hasDelay ? 'now-' + panel.nowDelay : timeRange.raw.to, + }; + + this.timeSrv.setTime(nextRange); + }; + + onZoom = () => { + this.$rootScope.appEvent('zoom-out', 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 intervals = dashboard.timepicker.refresh_intervals; + const timePickerValue = this.timeSrv.timeRange(); + const timeZone = dashboard.getTimezone(); + return ( - + <> + + + ); } } diff --git a/public/app/features/dashboard/services/TimeSrv.ts b/public/app/features/dashboard/services/TimeSrv.ts index 9c415b9aea5..ca9b244b956 100644 --- a/public/app/features/dashboard/services/TimeSrv.ts +++ b/public/app/features/dashboard/services/TimeSrv.ts @@ -7,7 +7,7 @@ import coreModule from 'app/core/core_module'; import * as dateMath from '@grafana/ui/src/utils/datemath'; // Types -import { TimeRange, RawTimeRange } from '@grafana/ui'; +import { TimeRange, RawTimeRange, TimeZone } from '@grafana/ui'; import { ITimeoutService, ILocationService } from 'angular'; import { ContextSrv } from 'app/core/services/context_srv'; import { DashboardModel } from '../state/DashboardModel'; @@ -224,7 +224,7 @@ export class TimeSrv { to: isDateTime(this.time.to) ? dateTime(this.time.to) : this.time.to, }; - const timezone = this.dashboard && this.dashboard.getTimezone(); + const timezone: TimeZone = this.dashboard ? this.dashboard.getTimezone() : undefined; return { from: dateMath.parse(raw.from, false, timezone), diff --git a/public/app/features/dashboard/state/DashboardModel.ts b/public/app/features/dashboard/state/DashboardModel.ts index 88e92cd09df..ff8fe5ac8d6 100644 --- a/public/app/features/dashboard/state/DashboardModel.ts +++ b/public/app/features/dashboard/state/DashboardModel.ts @@ -13,7 +13,7 @@ import sortByKeys from 'app/core/utils/sort_by_keys'; // Types import { PanelModel, GridPos } from './PanelModel'; import { DashboardMigrator } from './DashboardMigrator'; -import { TimeRange } from '@grafana/ui'; +import { TimeRange, TimeZone } from '@grafana/ui'; import { UrlQueryValue } from '@grafana/runtime'; import { KIOSK_MODE_TV, DashboardMeta } from 'app/types'; import { toUtc, DateTimeInput, dateTime, isDateTime } from '@grafana/ui/src/utils/moment_wrapper'; @@ -832,8 +832,8 @@ export class DashboardModel { return this.snapshot !== undefined; } - getTimezone() { - return this.timezone ? this.timezone : contextSrv.user.timezone; + getTimezone(): TimeZone { + return (this.timezone ? this.timezone : contextSrv.user.timezone) as TimeZone; } private updateSchema(old: any) { diff --git a/public/app/features/explore/ExploreToolbar.tsx b/public/app/features/explore/ExploreToolbar.tsx index 9d3cb984120..75f8cc75b7c 100644 --- a/public/app/features/explore/ExploreToolbar.tsx +++ b/public/app/features/explore/ExploreToolbar.tsx @@ -216,7 +216,7 @@ export class UnConnectedExploreToolbar extends PureComponent {
{!isLive && ( - + )} diff --git a/public/app/features/explore/Graph.tsx b/public/app/features/explore/Graph.tsx index b5cdca318af..1a65bd2397c 100644 --- a/public/app/features/explore/Graph.tsx +++ b/public/app/features/explore/Graph.tsx @@ -129,7 +129,7 @@ export class Graph extends PureComponent { this.$el.unbind('plotselected', this.onPlotSelected); } - onPlotSelected = (event, ranges) => { + onPlotSelected = (event: JQueryEventObject, ranges) => { const { onChangeTime } = this.props; if (onChangeTime) { this.props.onChangeTime({ @@ -151,7 +151,7 @@ export class Graph extends PureComponent { max: max, label: 'Datetime', ticks: ticks, - timezone: timeZone.raw, + timezone: timeZone, timeformat: time_format(ticks, min, max), }, }; diff --git a/public/app/features/explore/GraphContainer.tsx b/public/app/features/explore/GraphContainer.tsx index 6d1bb6c4e38..632613f60c0 100644 --- a/public/app/features/explore/GraphContainer.tsx +++ b/public/app/features/explore/GraphContainer.tsx @@ -34,8 +34,8 @@ export class GraphContainer extends PureComponent { onChangeTime = (absRange: AbsoluteTimeRange) => { const { exploreId, timeZone, changeTime } = this.props; const range = { - from: timeZone.isUtc ? toUtc(absRange.from) : dateTime(absRange.from), - to: timeZone.isUtc ? toUtc(absRange.to) : dateTime(absRange.to), + from: timeZone === 'utc' ? toUtc(absRange.from) : dateTime(absRange.from), + to: timeZone === 'utc' ? toUtc(absRange.to) : dateTime(absRange.to), }; changeTime(exploreId, range); diff --git a/public/app/features/explore/LogsContainer.tsx b/public/app/features/explore/LogsContainer.tsx index c6ba84aceb0..975923015c5 100644 --- a/public/app/features/explore/LogsContainer.tsx +++ b/public/app/features/explore/LogsContainer.tsx @@ -57,8 +57,8 @@ export class LogsContainer extends PureComponent { onChangeTime = (absRange: AbsoluteTimeRange) => { const { exploreId, timeZone, changeTime } = this.props; const range = { - from: timeZone.isUtc ? toUtc(absRange.from) : dateTime(absRange.from), - to: timeZone.isUtc ? toUtc(absRange.to) : dateTime(absRange.to), + from: timeZone === 'utc' ? toUtc(absRange.from) : dateTime(absRange.from), + to: timeZone === 'utc' ? toUtc(absRange.to) : dateTime(absRange.to), }; changeTime(exploreId, range); diff --git a/public/app/features/explore/TimePicker.test.tsx b/public/app/features/explore/TimePicker.test.tsx index 9cdf7a9b517..ea793096374 100644 --- a/public/app/features/explore/TimePicker.test.tsx +++ b/public/app/features/explore/TimePicker.test.tsx @@ -29,7 +29,7 @@ const fromRaw = (rawRange: RawTimeRange): TimeRange => { describe('', () => { it('render default values when closed and relative time range', () => { const range = fromRaw(DEFAULT_RANGE); - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.state('fromRaw')).toBe(DEFAULT_RANGE.from); expect(wrapper.state('toRaw')).toBe(DEFAULT_RANGE.to); expect(wrapper.find('.timepicker-rangestring').text()).toBe('Last 6 hours'); @@ -39,7 +39,7 @@ describe('', () => { it('render default values when closed, utc and relative time range', () => { const range = fromRaw(DEFAULT_RANGE); - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.state('fromRaw')).toBe(DEFAULT_RANGE.from); expect(wrapper.state('toRaw')).toBe(DEFAULT_RANGE.to); expect(wrapper.find('.timepicker-rangestring').text()).toBe('Last 6 hours'); @@ -49,7 +49,7 @@ describe('', () => { it('renders default values when open and relative range', () => { const range = fromRaw(DEFAULT_RANGE); - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.state('fromRaw')).toBe(DEFAULT_RANGE.from); expect(wrapper.state('toRaw')).toBe(DEFAULT_RANGE.to); expect(wrapper.find('.timepicker-rangestring').text()).toBe('Last 6 hours'); @@ -61,7 +61,7 @@ describe('', () => { it('renders default values when open, utc and relative range', () => { const range = fromRaw(DEFAULT_RANGE); - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.state('fromRaw')).toBe(DEFAULT_RANGE.from); expect(wrapper.state('toRaw')).toBe(DEFAULT_RANGE.to); expect(wrapper.find('.timepicker-rangestring').text()).toBe('Last 6 hours'); @@ -91,7 +91,7 @@ describe('', () => { const expectedRangeString = rangeUtil.describeTimeRange(localRange); const onChangeTime = sinon.spy(); - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.state('fromRaw')).toBe(localRange.from.format(TIME_FORMAT)); expect(wrapper.state('toRaw')).toBe(localRange.to.format(TIME_FORMAT)); expect(wrapper.state('initialRange')).toBe(range.raw); @@ -118,11 +118,11 @@ describe('', () => { }, }; const onChangeTime = sinon.spy(); - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.state('fromRaw')).toBe('1970-01-01 00:00:00'); expect(wrapper.state('toRaw')).toBe('1970-01-01 00:00:01'); expect(wrapper.state('initialRange')).toBe(range.raw); - expect(wrapper.find('.timepicker-rangestring').text()).toBe('Jan 1, 1970 00:00:00 to Jan 1, 1970 00:00:01'); + expect(wrapper.find('.timepicker-rangestring').text()).toBe('1970-01-01 00:00:00 to 1970-01-01 00:00:01'); expect(wrapper.find('.timepicker-from').props().value).toBe('1970-01-01 00:00:00'); expect(wrapper.find('.timepicker-to').props().value).toBe('1970-01-01 00:00:01'); @@ -132,7 +132,7 @@ describe('', () => { expect(onChangeTime.getCall(0).args[0].to.valueOf()).toBe(1000); expect(wrapper.state('isOpen')).toBeFalsy(); - expect(wrapper.state('rangeString')).toBe('Jan 1, 1970 00:00:00 to Jan 1, 1970 00:00:01'); + expect(wrapper.state('rangeString')).toBe('1970-01-01 00:00:00 to 1970-01-01 00:00:01'); }); it('moves ranges backward by half the range on left arrow click when utc', () => { @@ -147,7 +147,7 @@ describe('', () => { const range = fromRaw(rawRange); const onChangeTime = sinon.spy(); - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.state('fromRaw')).toBe('1970-01-01 00:00:02'); expect(wrapper.state('toRaw')).toBe('1970-01-01 00:00:04'); @@ -176,7 +176,7 @@ describe('', () => { }; const onChangeTime = sinon.spy(); - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.state('fromRaw')).toBe(localRange.from.format(TIME_FORMAT)); expect(wrapper.state('toRaw')).toBe(localRange.to.format(TIME_FORMAT)); @@ -197,7 +197,7 @@ describe('', () => { }; const onChangeTime = sinon.spy(); - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.state('fromRaw')).toBe('1970-01-01 00:00:01'); expect(wrapper.state('toRaw')).toBe('1970-01-01 00:00:03'); @@ -226,7 +226,7 @@ describe('', () => { }; const onChangeTime = sinon.spy(); - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.state('fromRaw')).toBe(localRange.from.format(TIME_FORMAT)); expect(wrapper.state('toRaw')).toBe(localRange.to.format(TIME_FORMAT)); diff --git a/public/app/features/explore/TimePicker.tsx b/public/app/features/explore/TimePicker.tsx index 9a10edc0625..510646dcfe4 100644 --- a/public/app/features/explore/TimePicker.tsx +++ b/public/app/features/explore/TimePicker.tsx @@ -1,12 +1,12 @@ -import React, { PureComponent } from 'react'; +import React, { PureComponent, ChangeEvent } from 'react'; import * as rangeUtil from '@grafana/ui/src/utils/rangeutil'; -import { Input, RawTimeRange, TimeRange, TIME_FORMAT } from '@grafana/ui'; +import { Input, RawTimeRange, TimeRange, TIME_FORMAT, TimeZone } from '@grafana/ui'; import { toUtc, isDateTime, dateTime } from '@grafana/ui/src/utils/moment_wrapper'; interface TimePickerProps { isOpen?: boolean; - isUtc?: boolean; range: TimeRange; + timeZone: TimeZone; onChangeTime?: (range: RawTimeRange, scanning?: boolean) => void; } @@ -22,21 +22,21 @@ interface TimePickerState { toRaw: string; } -const getRaw = (isUtc: boolean, range: any) => { +const getRaw = (range: any, timeZone: TimeZone) => { const rawRange = { from: range.raw.from, to: range.raw.to, }; if (isDateTime(rawRange.from)) { - if (!isUtc) { + if (timeZone === 'browser') { rawRange.from = rawRange.from.local(); } rawRange.from = rawRange.from.format(TIME_FORMAT); } if (isDateTime(rawRange.to)) { - if (!isUtc) { + if (timeZone === 'browser') { rawRange.to = rawRange.to.local(); } rawRange.to = rawRange.to.format(TIME_FORMAT); @@ -61,19 +61,19 @@ export default class TimePicker extends PureComponent { + handleChangeFrom = (event: ChangeEvent) => { this.setState({ - fromRaw: e.target.value, + fromRaw: event.target.value, }); }; - handleChangeTo = e => { + handleChangeTo = (event: ChangeEvent) => { this.setState({ - toRaw: e.target.value, + toRaw: event.target.value, }); }; handleClickApply = () => { - const { onChangeTime, isUtc } = this.props; + const { onChangeTime, timeZone } = this.props; let rawRange; + this.setState( state => { const { toRaw, fromRaw } = this.state; @@ -149,11 +153,11 @@ export default class TimePicker extends PureComponent parseTimeZone(state.timeZone); +export const getTimeZone = (state: UserState) => state.timeZone; diff --git a/public/app/features/teams/__snapshots__/TeamMemberRow.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamMemberRow.test.tsx.snap index 96ff1b4cbc9..501a0829942 100644 --- a/public/app/features/teams/__snapshots__/TeamMemberRow.test.tsx.snap +++ b/public/app/features/teams/__snapshots__/TeamMemberRow.test.tsx.snap @@ -115,6 +115,7 @@ exports[`Render when feature toggle editorsCanAdmin is turned off should not ren }, ] } + tabSelectsValue={true} value={ Object { "description": "Is team member", @@ -199,6 +200,7 @@ exports[`Render when feature toggle editorsCanAdmin is turned on should render p }, ] } + tabSelectsValue={true} value={ Object { "description": "Is team member", diff --git a/public/app/plugins/datasource/grafana/specs/datasource.test.ts b/public/app/plugins/datasource/grafana/specs/datasource.test.ts index 4c189ac4caa..47202c428cc 100644 --- a/public/app/plugins/datasource/grafana/specs/datasource.test.ts +++ b/public/app/plugins/datasource/grafana/specs/datasource.test.ts @@ -6,14 +6,14 @@ describe('grafana data source', () => { describe('when executing an annotations query', () => { let calledBackendSrvParams; const backendSrvStub = { - get: (url, options) => { + get: (url: string, options) => { calledBackendSrvParams = options; return q.resolve([]); }, }; const templateSrvStub = { - replace: val => { + replace: (val: string) => { return val.replace('$var2', 'replaced__delimiter__replaced2').replace('$var', 'replaced'); }, }; diff --git a/public/app/plugins/datasource/prometheus/components/__snapshots__/PromQueryEditor.test.tsx.snap b/public/app/plugins/datasource/prometheus/components/__snapshots__/PromQueryEditor.test.tsx.snap index e9f83ccafd3..a705b312ea4 100644 --- a/public/app/plugins/datasource/prometheus/components/__snapshots__/PromQueryEditor.test.tsx.snap +++ b/public/app/plugins/datasource/prometheus/components/__snapshots__/PromQueryEditor.test.tsx.snap @@ -123,6 +123,7 @@ exports[`Render PromQueryEditor with basic options should render 1`] = ` }, ] } + tabSelectsValue={true} value={ Object { "label": "1/1", @@ -176,6 +177,7 @@ exports[`Render PromQueryEditor with basic options should render 1`] = ` }, ] } + tabSelectsValue={true} value={ Object { "label": "Time series", diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 5f7db2de60b..657e687a281 100644 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -149,7 +149,7 @@ class GraphElement { } } - onPlotSelected(event, ranges) { + onPlotSelected(event: JQueryEventObject, ranges) { if (this.panel.xaxis.mode !== 'time') { // Skip if panel in histogram or series mode this.plot.clearSelection(); @@ -171,7 +171,7 @@ class GraphElement { } } - onPlotClick(event, pos, item) { + onPlotClick(event: JQueryEventObject, pos, item) { if (this.panel.xaxis.mode !== 'time') { // Skip if panel in histogram or series mode return; diff --git a/public/app/types/user.ts b/public/app/types/user.ts index 954bf278011..ec3761c7137 100644 --- a/public/app/types/user.ts +++ b/public/app/types/user.ts @@ -1,3 +1,5 @@ +import { TimeZone } from '@grafana/ui/src/types'; + export interface OrgUser { avatarUrl: string; email: string; @@ -46,7 +48,7 @@ export interface UsersState { export interface UserState { orgId: number; - timeZone: string; + timeZone: TimeZone; } export interface UserSession { diff --git a/public/sass/_variables.dark.generated.scss b/public/sass/_variables.dark.generated.scss index 9a2f4491750..fb157a92eef 100644 --- a/public/sass/_variables.dark.generated.scss +++ b/public/sass/_variables.dark.generated.scss @@ -42,6 +42,7 @@ $gray-2: #8e8e8e; $gray-3: #b3b3b3; $gray-4: #d8d9da; $gray-5: #ececec; +$gray-6: #f4f5f8; $gray-blue: #212327; $input-black: #09090b; @@ -285,6 +286,7 @@ $alert-info-bg: linear-gradient(100deg, $blue-base, $blue-shade); $popover-bg: $dark-2; $popover-color: $text-color; $popover-border-color: $dark-9; +$popover-header-bg: $dark-9; $popover-shadow: 0 0 20px black; $popover-help-bg: $btn-secondary-bg; @@ -398,3 +400,7 @@ $button-toggle-group-btn-seperator-border: 1px solid $dark-2; $vertical-resize-handle-bg: $dark-10; $vertical-resize-handle-dots: $gray-1; $vertical-resize-handle-dots-hover: $gray-2; + +// Calendar +$calendar-bg-days: $input-bg; +$calendar-bg-now: $dark-10; diff --git a/public/sass/_variables.light.generated.scss b/public/sass/_variables.light.generated.scss index b989af2eb21..c61211fe30a 100644 --- a/public/sass/_variables.light.generated.scss +++ b/public/sass/_variables.light.generated.scss @@ -30,6 +30,8 @@ $black: #000000; $dark-1: #1e2028; $dark-2: #41444b; +$dark-4: #35373f; +$dark-10: #424345; $gray-1: #52545c; $gray-2: #767980; $gray-3: #acb6bf; @@ -272,6 +274,7 @@ $alert-info-bg: linear-gradient(100deg, $blue-base, $blue-shade); $popover-bg: $page-bg; $popover-color: $text-color; $popover-border-color: $gray-5; +$popover-header-bg: $gray-5; $popover-shadow: 0 0 20px $white; $popover-help-bg: $btn-secondary-bg; @@ -385,3 +388,7 @@ $button-toggle-group-btn-seperator-border: 1px solid $gray-6; $vertical-resize-handle-bg: $gray-4; $vertical-resize-handle-dots: $gray-3; $vertical-resize-handle-dots-hover: $gray-2; + +// Calendar +$calendar-bg-days: $white; +$calendar-bg-now: $gray-6; diff --git a/public/sass/components/_navbar.scss b/public/sass/components/_navbar.scss index 67071bb61c8..434ecbacb1e 100644 --- a/public/sass/components/_navbar.scss +++ b/public/sass/components/_navbar.scss @@ -134,7 +134,7 @@ i.navbar-page-btn__search { align-items: center; font-weight: $btn-font-weight; padding: 6px $space-sm; - line-height: 16px; + line-height: 18px; color: $text-muted; border: 1px solid $navbar-button-border; margin-left: $space-xs; diff --git a/public/sass/components/_panel_graph.scss b/public/sass/components/_panel_graph.scss index bc4cc773c39..48c6384a148 100644 --- a/public/sass/components/_panel_graph.scss +++ b/public/sass/components/_panel_graph.scss @@ -304,7 +304,7 @@ } .graph-annotation__header { - background-color: $popover-border-color; + background: $popover-header-bg; padding: 6px 10px; display: flex; } diff --git a/public/sass/components/_panel_logs.scss b/public/sass/components/_panel_logs.scss index 3c6ffd83a6f..b0156ce9cee 100644 --- a/public/sass/components/_panel_logs.scss +++ b/public/sass/components/_panel_logs.scss @@ -252,7 +252,7 @@ $column-horizontal-spacing: 10px; } .logs-stats__header { - background-color: $popover-border-color; + background: $popover-header-bg; padding: 6px 10px; display: flex; } diff --git a/public/test/specs/helpers.ts b/public/test/specs/helpers.ts index 648b73de400..aeef0404582 100644 --- a/public/test/specs/helpers.ts +++ b/public/test/specs/helpers.ts @@ -19,7 +19,7 @@ export function ControllerTestContext(this: any) { getMetricSources: () => {}, get: () => { return { - then: (callback: (a: any) => void) => { + then: (callback: (ds: any) => void) => { callback(self.datasource); }, }; diff --git a/public/vendor/flot/jquery.flot.pie.js b/public/vendor/flot/jquery.flot.pie.js index dee47e6e504..e69de29bb2d 100644 --- a/public/vendor/flot/jquery.flot.pie.js +++ b/public/vendor/flot/jquery.flot.pie.js @@ -1,818 +0,0 @@ -/* Flot plugin for rendering pie charts. - -Copyright (c) 2007-2013 IOLA and Ole Laursen. -Licensed under the MIT license. - -The plugin assumes that each series has a single data value, and that each -value is a positive integer or zero. Negative numbers don't make sense for a -pie chart, and have unpredictable results. The values do NOT need to be -passed in as percentages; the plugin will calculate the total and per-slice -percentages internally. - -* Created by Brian Medendorp - -* Updated with contributions from btburnett3, Anthony Aragues and Xavi Ivars - -The plugin supports these options: - - series: { - pie: { - show: true/false - radius: 0-1 for percentage of fullsize, or a specified pixel length, or 'auto' - innerRadius: 0-1 for percentage of fullsize or a specified pixel length, for creating a donut effect - startAngle: 0-2 factor of PI used for starting angle (in radians) i.e 3/2 starts at the top, 0 and 2 have the same result - tilt: 0-1 for percentage to tilt the pie, where 1 is no tilt, and 0 is completely flat (nothing will show) - offset: { - top: integer value to move the pie up or down - left: integer value to move the pie left or right, or 'auto' - }, - stroke: { - color: any hexidecimal color value (other formats may or may not work, so best to stick with something like '#FFF') - width: integer pixel width of the stroke - }, - label: { - show: true/false, or 'auto' - formatter: a user-defined function that modifies the text/style of the label text - radius: 0-1 for percentage of fullsize, or a specified pixel length - background: { - color: any hexidecimal color value (other formats may or may not work, so best to stick with something like '#000') - opacity: 0-1 - }, - threshold: 0-1 for the percentage value at which to hide labels (if they're too small) - }, - combine: { - threshold: 0-1 for the percentage value at which to combine slices (if they're too small) - color: any hexidecimal color value (other formats may or may not work, so best to stick with something like '#CCC'), if null, the plugin will automatically use the color of the first slice to be combined - label: any text value of what the combined slice should be labeled - } - highlight: { - opacity: 0-1 - } - } - } - -More detail and specific examples can be found in the included HTML file. - -*/ - -(function($) { - - // Maximum redraw attempts when fitting labels within the plot - - var REDRAW_ATTEMPTS = 10; - - // Factor by which to shrink the pie when fitting labels within the plot - - var REDRAW_SHRINK = 0.95; - - function init(plot) { - - var canvas = null, - target = null, - maxRadius = null, - centerLeft = null, - centerTop = null, - processed = false, - options = null, - ctx = null; - - // interactive variables - - var highlights = []; - - // add hook to determine if pie plugin in enabled, and then perform necessary operations - - plot.hooks.processOptions.push(function(plot, options) { - if (options.series.pie.show) { - - options.grid.show = false; - - // set labels.show - - if (options.series.pie.label.show == "auto") { - if (options.legend.show) { - options.series.pie.label.show = false; - } else { - options.series.pie.label.show = true; - } - } - - // set radius - - if (options.series.pie.radius == "auto") { - if (options.series.pie.label.show) { - options.series.pie.radius = 3/4; - } else { - options.series.pie.radius = 1; - } - } - - // ensure sane tilt - - if (options.series.pie.tilt > 1) { - options.series.pie.tilt = 1; - } else if (options.series.pie.tilt < 0) { - options.series.pie.tilt = 0; - } - } - }); - - plot.hooks.bindEvents.push(function(plot, eventHolder) { - var options = plot.getOptions(); - if (options.series.pie.show) { - if (options.grid.hoverable) { - eventHolder.unbind("mousemove").mousemove(onMouseMove); - } - if (options.grid.clickable) { - eventHolder.unbind("click").click(onClick); - } - } - }); - - plot.hooks.processDatapoints.push(function(plot, series, data, datapoints) { - var options = plot.getOptions(); - if (options.series.pie.show) { - processDatapoints(plot, series, data, datapoints); - } - }); - - plot.hooks.drawOverlay.push(function(plot, octx) { - var options = plot.getOptions(); - if (options.series.pie.show) { - drawOverlay(plot, octx); - } - }); - - plot.hooks.draw.push(function(plot, newCtx) { - var options = plot.getOptions(); - if (options.series.pie.show) { - draw(plot, newCtx); - } - }); - - function processDatapoints(plot, series, datapoints) { - if (!processed) { - processed = true; - canvas = plot.getCanvas(); - target = $(canvas).parent(); - options = plot.getOptions(); - plot.setData(combine(plot.getData())); - } - } - - function combine(data) { - - var total = 0, - combined = 0, - numCombined = 0, - color = options.series.pie.combine.color, - newdata = []; - - // Fix up the raw data from Flot, ensuring the data is numeric - - for (var i = 0; i < data.length; ++i) { - - var value = data[i].data; - - // If the data is an array, we'll assume that it's a standard - // Flot x-y pair, and are concerned only with the second value. - - // Note how we use the original array, rather than creating a - // new one; this is more efficient and preserves any extra data - // that the user may have stored in higher indexes. - - if ($.isArray(value) && value.length == 1) { - value = value[0]; - } - - if ($.isArray(value)) { - // Equivalent to $.isNumeric() but compatible with jQuery < 1.7 - if (!isNaN(parseFloat(value[1])) && isFinite(value[1])) { - value[1] = +value[1]; - } else { - value[1] = 0; - } - } else if (!isNaN(parseFloat(value)) && isFinite(value)) { - value = [1, +value]; - } else { - value = [1, 0]; - } - - data[i].data = [value]; - } - - // Sum up all the slices, so we can calculate percentages for each - - for (var i = 0; i < data.length; ++i) { - total += data[i].data[0][1]; - } - - // Count the number of slices with percentages below the combine - // threshold; if it turns out to be just one, we won't combine. - - for (var i = 0; i < data.length; ++i) { - var value = data[i].data[0][1]; - if (value / total <= options.series.pie.combine.threshold) { - combined += value; - numCombined++; - if (!color) { - color = data[i].color; - } - } - } - - for (var i = 0; i < data.length; ++i) { - var value = data[i].data[0][1]; - if (numCombined < 2 || value / total > options.series.pie.combine.threshold) { - newdata.push({ - data: [[1, value]], - color: data[i].color, - label: data[i].label, - angle: value * Math.PI * 2 / total, - percent: value / (total / 100) - }); - } - } - - if (numCombined > 1) { - newdata.push({ - data: [[1, combined]], - color: color, - label: options.series.pie.combine.label, - angle: combined * Math.PI * 2 / total, - percent: combined / (total / 100) - }); - } - - return newdata; - } - - function draw(plot, newCtx) { - - if (!target) { - return; // if no series were passed - } - - var canvasWidth = plot.getPlaceholder().width(), - canvasHeight = plot.getPlaceholder().height(), - legendWidth = target.children().filter(".legend").children().width() || 0; - - ctx = newCtx; - - // WARNING: HACK! REWRITE THIS CODE AS SOON AS POSSIBLE! - - // When combining smaller slices into an 'other' slice, we need to - // add a new series. Since Flot gives plugins no way to modify the - // list of series, the pie plugin uses a hack where the first call - // to processDatapoints results in a call to setData with the new - // list of series, then subsequent processDatapoints do nothing. - - // The plugin-global 'processed' flag is used to control this hack; - // it starts out false, and is set to true after the first call to - // processDatapoints. - - // Unfortunately this turns future setData calls into no-ops; they - // call processDatapoints, the flag is true, and nothing happens. - - // To fix this we'll set the flag back to false here in draw, when - // all series have been processed, so the next sequence of calls to - // processDatapoints once again starts out with a slice-combine. - // This is really a hack; in 0.9 we need to give plugins a proper - // way to modify series before any processing begins. - - processed = false; - - // calculate maximum radius and center point - - maxRadius = Math.min(canvasWidth, canvasHeight / options.series.pie.tilt) / 2; - centerTop = canvasHeight / 2 + options.series.pie.offset.top; - centerLeft = canvasWidth / 2; - - if (options.series.pie.offset.left == "auto") { - if (options.legend.position.match("w")) { - centerLeft += legendWidth / 2; - } else { - centerLeft -= legendWidth / 2; - } - } else { - centerLeft += options.series.pie.offset.left; - } - - if (centerLeft < maxRadius) { - centerLeft = maxRadius; - } else if (centerLeft > canvasWidth - maxRadius) { - centerLeft = canvasWidth - maxRadius; - } - - var slices = plot.getData(), - attempts = 0; - - // Keep shrinking the pie's radius until drawPie returns true, - // indicating that all the labels fit, or we try too many times. - - do { - if (attempts > 0) { - maxRadius *= REDRAW_SHRINK; - } - attempts += 1; - clear(); - if (options.series.pie.tilt <= 0.8) { - drawShadow(); - } - } while (!drawPie() && attempts < REDRAW_ATTEMPTS) - - if (attempts >= REDRAW_ATTEMPTS) { - clear(); - target.prepend("
Could not draw pie with labels contained inside canvas
"); - } - - if (plot.setSeries && plot.insertLegend) { - plot.setSeries(slices); - plot.insertLegend(); - } - - // we're actually done at this point, just defining internal functions at this point - - function clear() { - ctx.clearRect(0, 0, canvasWidth, canvasHeight); - target.children().filter(".pieLabel, .pieLabelBackground").remove(); - } - - function drawShadow() { - - var shadowLeft = options.series.pie.shadow.left; - var shadowTop = options.series.pie.shadow.top; - var edge = 10; - var alpha = options.series.pie.shadow.alpha; - var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius; - - if (radius >= canvasWidth / 2 - shadowLeft || radius * options.series.pie.tilt >= canvasHeight / 2 - shadowTop || radius <= edge) { - return; // shadow would be outside canvas, so don't draw it - } - - ctx.save(); - ctx.translate(shadowLeft,shadowTop); - ctx.globalAlpha = alpha; - ctx.fillStyle = "#000"; - - // center and rotate to starting position - - ctx.translate(centerLeft,centerTop); - ctx.scale(1, options.series.pie.tilt); - - //radius -= edge; - - for (var i = 1; i <= edge; i++) { - ctx.beginPath(); - ctx.arc(0, 0, radius, 0, Math.PI * 2, false); - ctx.fill(); - radius -= i; - } - - ctx.restore(); - } - - function drawPie() { - - var startAngle = Math.PI * options.series.pie.startAngle; - var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius; - - // center and rotate to starting position - - ctx.save(); - ctx.translate(centerLeft,centerTop); - ctx.scale(1, options.series.pie.tilt); - //ctx.rotate(startAngle); // start at top; -- This doesn't work properly in Opera - - // draw slices - - ctx.save(); - var currentAngle = startAngle; - for (var i = 0; i < slices.length; ++i) { - slices[i].startAngle = currentAngle; - drawSlice(slices[i].angle, slices[i].color, true); - } - ctx.restore(); - - // draw slice outlines - - if (options.series.pie.stroke.width > 0) { - ctx.save(); - ctx.lineWidth = options.series.pie.stroke.width; - currentAngle = startAngle; - for (var i = 0; i < slices.length; ++i) { - drawSlice(slices[i].angle, options.series.pie.stroke.color, false); - } - ctx.restore(); - } - - // draw donut hole - - drawDonutHole(ctx); - - ctx.restore(); - - // Draw the labels, returning true if they fit within the plot - - if (options.series.pie.label.show) { - return drawLabels(); - } else return true; - - function drawSlice(angle, color, fill) { - - if (angle <= 0 || isNaN(angle)) { - return; - } - - if (fill) { - ctx.fillStyle = color; - } else { - ctx.strokeStyle = color; - ctx.lineJoin = "round"; - } - - ctx.beginPath(); - if (Math.abs(angle - Math.PI * 2) > 0.000000001) { - ctx.moveTo(0, 0); // Center of the pie - } - - //ctx.arc(0, 0, radius, 0, angle, false); // This doesn't work properly in Opera - ctx.arc(0, 0, radius,currentAngle, currentAngle + angle / 2, false); - ctx.arc(0, 0, radius,currentAngle + angle / 2, currentAngle + angle, false); - ctx.closePath(); - //ctx.rotate(angle); // This doesn't work properly in Opera - currentAngle += angle; - - if (fill) { - ctx.fill(); - } else { - ctx.stroke(); - } - } - - function drawLabels() { - - var currentAngle = startAngle; - var radius = options.series.pie.label.radius > 1 ? options.series.pie.label.radius : maxRadius * options.series.pie.label.radius; - - for (var i = 0; i < slices.length; ++i) { - if (slices[i].percent >= options.series.pie.label.threshold * 100) { - if (!drawLabel(slices[i], currentAngle, i)) { - return false; - } - } - currentAngle += slices[i].angle; - } - - return true; - - function drawLabel(slice, startAngle, index) { - - if (slice.data[0][1] == 0) { - return true; - } - - // format label text - - var lf = options.legend.labelFormatter, text, plf = options.series.pie.label.formatter; - - if (lf) { - text = lf(slice.label, slice); - } else { - text = slice.label; - } - - if (plf) { - text = plf(text, slice); - } - - var halfAngle = ((startAngle + slice.angle) + startAngle) / 2; - var x = centerLeft + Math.round(Math.cos(halfAngle) * radius); - var y = centerTop + Math.round(Math.sin(halfAngle) * radius) * options.series.pie.tilt; - - var html = "" + text + ""; - target.append(html); - - var label = target.children("#pieLabel" + index); - var labelTop = (y - label.height() / 2); - var labelLeft = (x - label.width() / 2); - - label.css("top", labelTop); - label.css("left", labelLeft); - - // check to make sure that the label is not outside the canvas - - if (0 - labelTop > 0 || 0 - labelLeft > 0 || canvasHeight - (labelTop + label.height()) < 0 || canvasWidth - (labelLeft + label.width()) < 0) { - return false; - } - - if (options.series.pie.label.background.opacity != 0) { - - // put in the transparent background separately to avoid blended labels and label boxes - - var c = options.series.pie.label.background.color; - - if (c == null) { - c = slice.color; - } - - var pos = "top:" + labelTop + "px;left:" + labelLeft + "px;"; - $("
") - .css("opacity", options.series.pie.label.background.opacity) - .insertBefore(label); - } - - return true; - } // end individual label function - } // end drawLabels function - } // end drawPie function - } // end draw function - - // Placed here because it needs to be accessed from multiple locations - - function drawDonutHole(layer) { - if (options.series.pie.innerRadius > 0) { - - // subtract the center - - layer.save(); - var innerRadius = options.series.pie.innerRadius > 1 ? options.series.pie.innerRadius : maxRadius * options.series.pie.innerRadius; - layer.globalCompositeOperation = "destination-out"; // this does not work with excanvas, but it will fall back to using the stroke color - layer.beginPath(); - layer.fillStyle = options.series.pie.stroke.color; - layer.arc(0, 0, innerRadius, 0, Math.PI * 2, false); - layer.fill(); - layer.closePath(); - layer.restore(); - - // add inner stroke - - layer.save(); - layer.beginPath(); - layer.strokeStyle = options.series.pie.stroke.color; - layer.arc(0, 0, innerRadius, 0, Math.PI * 2, false); - layer.stroke(); - layer.closePath(); - layer.restore(); - - // TODO: add extra shadow inside hole (with a mask) if the pie is tilted. - } - } - - //-- Additional Interactive related functions -- - - function isPointInPoly(poly, pt) { - for(var c = false, i = -1, l = poly.length, j = l - 1; ++i < l; j = i) - ((poly[i][1] <= pt[1] && pt[1] < poly[j][1]) || (poly[j][1] <= pt[1] && pt[1]< poly[i][1])) - && (pt[0] < (poly[j][0] - poly[i][0]) * (pt[1] - poly[i][1]) / (poly[j][1] - poly[i][1]) + poly[i][0]) - && (c = !c); - return c; - } - - function findNearbySlice(mouseX, mouseY) { - - var slices = plot.getData(), - options = plot.getOptions(), - radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius, - x, y; - - for (var i = 0; i < slices.length; ++i) { - - var s = slices[i]; - - if (s.pie.show) { - - ctx.save(); - ctx.beginPath(); - ctx.moveTo(0, 0); // Center of the pie - //ctx.scale(1, options.series.pie.tilt); // this actually seems to break everything when here. - ctx.arc(0, 0, radius, s.startAngle, s.startAngle + s.angle / 2, false); - ctx.arc(0, 0, radius, s.startAngle + s.angle / 2, s.startAngle + s.angle, false); - ctx.closePath(); - x = mouseX - centerLeft; - y = mouseY - centerTop; - - if (ctx.isPointInPath) { - if (ctx.isPointInPath(mouseX - centerLeft, mouseY - centerTop)) { - ctx.restore(); - return { - datapoint: [s.percent, s.data], - dataIndex: 0, - series: s, - seriesIndex: i - }; - } - } else { - - // excanvas for IE doesn;t support isPointInPath, this is a workaround. - - var p1X = radius * Math.cos(s.startAngle), - p1Y = radius * Math.sin(s.startAngle), - p2X = radius * Math.cos(s.startAngle + s.angle / 4), - p2Y = radius * Math.sin(s.startAngle + s.angle / 4), - p3X = radius * Math.cos(s.startAngle + s.angle / 2), - p3Y = radius * Math.sin(s.startAngle + s.angle / 2), - p4X = radius * Math.cos(s.startAngle + s.angle / 1.5), - p4Y = radius * Math.sin(s.startAngle + s.angle / 1.5), - p5X = radius * Math.cos(s.startAngle + s.angle), - p5Y = radius * Math.sin(s.startAngle + s.angle), - arrPoly = [[0, 0], [p1X, p1Y], [p2X, p2Y], [p3X, p3Y], [p4X, p4Y], [p5X, p5Y]], - arrPoint = [x, y]; - - // TODO: perhaps do some mathmatical trickery here with the Y-coordinate to compensate for pie tilt? - - if (isPointInPoly(arrPoly, arrPoint)) { - ctx.restore(); - return { - datapoint: [s.percent, s.data], - dataIndex: 0, - series: s, - seriesIndex: i - }; - } - } - - ctx.restore(); - } - } - - return null; - } - - function onMouseMove(e) { - triggerClickHoverEvent("plothover", e); - } - - function onClick(e) { - triggerClickHoverEvent("plotclick", e); - } - - // trigger click or hover event (they send the same parameters so we share their code) - - function triggerClickHoverEvent(eventname, e) { - - var offset = plot.offset(); - var canvasX = parseInt(e.pageX - offset.left); - var canvasY = parseInt(e.pageY - offset.top); - var item = findNearbySlice(canvasX, canvasY); - - if (options.grid.autoHighlight) { - - // clear auto-highlights - - for (var i = 0; i < highlights.length; ++i) { - var h = highlights[i]; - if (h.auto == eventname && !(item && h.series == item.series)) { - unhighlight(h.series); - } - } - } - - // highlight the slice - - if (item) { - highlight(item.series, eventname); - } - - // trigger any hover bind events - - var pos = { pageX: e.pageX, pageY: e.pageY }; - target.trigger(eventname, [pos, item]); - } - - function highlight(s, auto) { - //if (typeof s == "number") { - // s = series[s]; - //} - - var i = indexOfHighlight(s); - - if (i == -1) { - highlights.push({ series: s, auto: auto }); - plot.triggerRedrawOverlay(); - } else if (!auto) { - highlights[i].auto = false; - } - } - - function unhighlight(s) { - if (s == null) { - highlights = []; - plot.triggerRedrawOverlay(); - } - - //if (typeof s == "number") { - // s = series[s]; - //} - - var i = indexOfHighlight(s); - - if (i != -1) { - highlights.splice(i, 1); - plot.triggerRedrawOverlay(); - } - } - - function indexOfHighlight(s) { - for (var i = 0; i < highlights.length; ++i) { - var h = highlights[i]; - if (h.series == s) - return i; - } - return -1; - } - - function drawOverlay(plot, octx) { - - var options = plot.getOptions(); - - var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius; - - octx.save(); - octx.translate(centerLeft, centerTop); - octx.scale(1, options.series.pie.tilt); - - for (var i = 0; i < highlights.length; ++i) { - drawHighlight(highlights[i].series); - } - - drawDonutHole(octx); - - octx.restore(); - - function drawHighlight(series) { - - if (series.angle <= 0 || isNaN(series.angle)) { - return; - } - - //octx.fillStyle = parseColor(options.series.pie.highlight.color).scale(null, null, null, options.series.pie.highlight.opacity).toString(); - octx.fillStyle = "rgba(255, 255, 255, " + options.series.pie.highlight.opacity + ")"; // this is temporary until we have access to parseColor - octx.beginPath(); - if (Math.abs(series.angle - Math.PI * 2) > 0.000000001) { - octx.moveTo(0, 0); // Center of the pie - } - octx.arc(0, 0, radius, series.startAngle, series.startAngle + series.angle / 2, false); - octx.arc(0, 0, radius, series.startAngle + series.angle / 2, series.startAngle + series.angle, false); - octx.closePath(); - octx.fill(); - } - } - } // end init (plugin body) - - // define pie specific options and their default values - - var options = { - series: { - pie: { - show: false, - radius: "auto", // actual radius of the visible pie (based on full calculated radius if <=1, or hard pixel value) - innerRadius: 0, /* for donut */ - startAngle: 3/2, - tilt: 1, - shadow: { - left: 5, // shadow left offset - top: 15, // shadow top offset - alpha: 0.02 // shadow alpha - }, - offset: { - top: 0, - left: "auto" - }, - stroke: { - color: "#fff", - width: 1 - }, - label: { - show: "auto", - formatter: function(label, slice) { - return "
" + label + "
" + Math.round(slice.percent) + "%
"; - }, // formatter function - radius: 1, // radius at which to place the labels (based on full calculated radius if <=1, or hard pixel value) - background: { - color: null, - opacity: 0 - }, - threshold: 0 // percentage at which to hide the label (i.e. the slice is too narrow) - }, - combine: { - threshold: -1, // percentage at which to combine little slices into one larger slice - color: null, // color to give the new slice (auto-generated if null) - label: "Other" // label to give the new slice - }, - highlight: { - //color: "#fff", // will add this functionality once parseColor is available - opacity: 0.5 - } - } - } - }; - - $.plot.plugins.push({ - init: init, - options: options, - name: "pie", - version: "1.1" - }); - -})(jQuery);