Selaa lähdekoodia

TimePicker: New time picker dropdown & custom range UI (#16811)

* feat: Add new picker to DashNavTimeControls

* chore: noImplicitAny limit reached

* chore: noImplicityAny fix

* chore: Add momentUtc helper to avoid the isUtc conditionals

* chore: Move getRaw from Explore's time picker to grafana/ui utils and rename to getRawRange

* feat: Use helper functions to convert utc to browser time

* fix: Dont Select current value when pressing tab when using Time Picker

* fix: Add tabIndex to time range inputs so tab works smoothly and prevent mouseDown event to propagate to react-select

* fix: Add spacing to custom range labels

* fix: Updated snapshot

* fix: Re-adding getRaw() temporary to fix the build

* fix: Disable scroll event in Popper when we're using the TimePicker so the popup wont "follow" the menu

* fix: Move all "Last xxxx" quick ranges to the menu and show a "UTC" text when applicable

* fix: Add zoom functionality

* feat: Add logic to mark selected option as active

* fix: Add tooltip to zoom button

* fix: lint fix after rebase

* chore: Remove old time picker from DashNav

* TimePicker: minor design update

* chore: Move all time picker quick ranges to the menu

* fix: Remove the popover border-right, since the quick ranges are gone

* chore: Remove function not in use

* Fix: Close time picker on resize event

* Fix: Remove border bottom

* Fix: Use fa icons on prev/next arrows

* Fix: Pass ref from TimePicker to TimePickerOptionGroup so the popover will align as it should

* Fix: time picker ui adjustments to get better touch area on buttons

* Fix: Dont increase line height on large screens

* TimePicker: style updates

* Fix: Add more prominent colors for selected dates and fade out dates in previous/next month

* TimePicker: style updates2

* TimePicker: Big refactorings and style changes

* Removed use of Popper not sure we need that here?
* Made active selected item in the list have the "selected" checkmark
* Changed design of popover
* Changed design of and implementation of the Custom selection in the dropdown it did not feel like a item you
could select like the rest now the list is just a normal list

* TimePicker: Refactoring & style changes

* TimePicker: use same date format everywhere

* TimePicker: Calendar style updates

* TimePicker: fixed unit test

* fixed unit test

* TimeZone: refactoring time zone type

* TimePicker: refactoring

* TimePicker: finally to UTC to work

* TimePicker: better way to handle calendar utc dates

* TimePicker: Fixed tooltip issues

* Updated snapshot

* TimePicker: moved tooltip from DashNavControls into TimePicker
Johannes Schill 6 vuotta sitten
vanhempi
commit
0412a28d2e
47 muutettua tiedostoa jossa 724 lisäystä ja 1809 poistoa
  1. 12 8
      packages/grafana-ui/src/components/Select/ButtonSelect.tsx
  2. 141 99
      packages/grafana-ui/src/components/Select/Select.tsx
  3. 1 1
      packages/grafana-ui/src/components/Select/_Select.scss
  4. 9 173
      packages/grafana-ui/src/components/TimePicker/TimePicker.story.tsx
  5. 125 239
      packages/grafana-ui/src/components/TimePicker/TimePicker.tsx
  6. 1 1
      packages/grafana-ui/src/components/TimePicker/TimePickerCalendar.story.tsx
  7. 20 14
      packages/grafana-ui/src/components/TimePicker/TimePickerCalendar.tsx
  8. 7 6
      packages/grafana-ui/src/components/TimePicker/TimePickerInput.tsx
  9. 0 51
      packages/grafana-ui/src/components/TimePicker/TimePickerOptionGroup.story.tsx
  10. 0 66
      packages/grafana-ui/src/components/TimePicker/TimePickerOptionGroup.tsx
  11. 1 3
      packages/grafana-ui/src/components/TimePicker/TimePickerPopover.story.tsx
  12. 62 111
      packages/grafana-ui/src/components/TimePicker/TimePickerPopover.tsx
  13. 120 89
      packages/grafana-ui/src/components/TimePicker/_TimePicker.scss
  14. 15 20
      packages/grafana-ui/src/components/TimePicker/time.ts
  15. 9 1
      packages/grafana-ui/src/components/Tooltip/Popper.tsx
  16. 1 0
      packages/grafana-ui/src/components/index.ts
  17. 6 0
      packages/grafana-ui/src/themes/_variables.dark.scss.tmpl.ts
  18. 7 0
      packages/grafana-ui/src/themes/_variables.light.scss.tmpl.ts
  19. 4 13
      packages/grafana-ui/src/types/time.ts
  20. 2 3
      packages/grafana-ui/src/utils/datemath.ts
  21. 1 0
      packages/grafana-ui/src/utils/index.ts
  22. 1 2
      packages/grafana-ui/src/utils/rangeutil.ts
  23. 3 3
      public/app/core/specs/rangeutil.test.ts
  24. 4 4
      public/app/core/utils/explore.ts
  25. 7 21
      public/app/features/dashboard/components/DashNav/DashNav.tsx
  26. 88 8
      public/app/features/dashboard/components/DashNav/DashNavTimeControls.tsx
  27. 2 2
      public/app/features/dashboard/services/TimeSrv.ts
  28. 3 3
      public/app/features/dashboard/state/DashboardModel.ts
  29. 1 1
      public/app/features/explore/ExploreToolbar.tsx
  30. 2 2
      public/app/features/explore/Graph.tsx
  31. 2 2
      public/app/features/explore/GraphContainer.tsx
  32. 2 2
      public/app/features/explore/LogsContainer.tsx
  33. 12 12
      public/app/features/explore/TimePicker.test.tsx
  34. 24 20
      public/app/features/explore/TimePicker.tsx
  35. 1 2
      public/app/features/profile/state/selectors.ts
  36. 2 0
      public/app/features/teams/__snapshots__/TeamMemberRow.test.tsx.snap
  37. 2 2
      public/app/plugins/datasource/grafana/specs/datasource.test.ts
  38. 2 0
      public/app/plugins/datasource/prometheus/components/__snapshots__/PromQueryEditor.test.tsx.snap
  39. 2 2
      public/app/plugins/panel/graph/graph.ts
  40. 3 1
      public/app/types/user.ts
  41. 6 0
      public/sass/_variables.dark.generated.scss
  42. 7 0
      public/sass/_variables.light.generated.scss
  43. 1 1
      public/sass/components/_navbar.scss
  44. 1 1
      public/sass/components/_panel_graph.scss
  45. 1 1
      public/sass/components/_panel_logs.scss
  46. 1 1
      public/test/specs/helpers.ts
  47. 0 818
      public/vendor/flot/jquery.flot.pie.js

+ 12 - 8
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) => {
       <div className="select-button">
         {iconClass && <i className={`select-button-icon ${iconClass}`} />}
         <span className="select-button-value">{label ? label : ''}</span>
-        <i className="fa fa-caret-down fa-fw" />
+        {!props.menuIsOpen && <i className="fa fa-caret-down fa-fw" />}
+        {props.menuIsOpen && <i className="fa fa-caret-up fa-fw" />}
       </div>
     </button>
   );
@@ -30,8 +31,8 @@ const ButtonComponent = (buttonProps: ButtonComponentProps) => (props: any) => {
 export interface Props<T> {
   className: string | undefined;
   options: Array<SelectOptionItem<T>>;
-  value: SelectOptionItem<T>;
-  label?: string;
+  value?: SelectOptionItem<T>;
+  label?: ReactElement | string;
   iconClass?: string;
   components?: any;
   maxMenuHeight?: number;
@@ -40,6 +41,7 @@ export interface Props<T> {
   isMenuOpen?: boolean;
   onOpenMenu?: () => void;
   onCloseMenu?: () => void;
+  tabSelectsValue?: boolean;
 }
 
 export class ButtonSelect<T> extends PureComponent<Props<T>> {
@@ -61,6 +63,7 @@ export class ButtonSelect<T> extends PureComponent<Props<T>> {
       isMenuOpen,
       onOpenMenu,
       onCloseMenu,
+      tabSelectsValue,
     } = this.props;
     const combinedComponents = {
       ...components,
@@ -75,13 +78,14 @@ export class ButtonSelect<T> extends PureComponent<Props<T>> {
         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}
       />
     );
   }

+ 141 - 99
packages/grafana-ui/src/components/Select/Select.tsx

@@ -53,6 +53,7 @@ export interface CommonProps<T> {
   tooltipContent?: PopperContent<any>;
   onOpenMenu?: () => void;
   onCloseMenu?: () => void;
+  tabSelectsValue?: boolean;
 }
 
 export interface SelectProps<T> extends CommonProps<T> {
@@ -65,26 +66,6 @@ interface AsyncProps<T> extends CommonProps<T> {
   loadingMessage?: () => string;
 }
 
-const wrapInTooltip = (
-  component: React.ReactElement,
-  tooltipContent: PopperContent<any> | undefined,
-  isMenuOpen: boolean | undefined
-) => {
-  const showTooltip = isMenuOpen ? false : undefined;
-  if (tooltipContent) {
-    return (
-      <Tooltip show={showTooltip} content={tooltipContent} placement="bottom">
-        <div>
-          {/* div needed for tooltip */}
-          {component}
-        </div>
-      </Tooltip>
-    );
-  } else {
-    return <div>{component}</div>;
-  }
-};
-
 export const MenuList = (props: any) => {
   return (
     <components.MenuList {...props}>
@@ -107,6 +88,7 @@ export class Select<T> extends PureComponent<SelectProps<T>> {
     isLoading: false,
     backspaceRemovesValue: true,
     maxMenuHeight: 300,
+    tabSelectsValue: true,
     components: {
       Option: SelectOption,
       SingleValue,
@@ -116,20 +98,6 @@ export class Select<T> extends PureComponent<SelectProps<T>> {
     },
   };
 
-  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<T> extends PureComponent<SelectProps<T>> {
       isOpen,
       components,
       tooltipContent,
+      tabSelectsValue,
+      onCloseMenu,
+      onOpenMenu,
     } = this.props;
 
     let widthClass = '';
@@ -164,37 +135,43 @@ export class Select<T> extends PureComponent<SelectProps<T>> {
 
     const selectClassNames = classNames('gf-form-input', 'gf-form-input--form-dropdown', widthClass, className);
     const selectComponents = { ...Select.defaultProps.components, ...components };
-    return wrapInTooltip(
-      <ReactSelect
-        classNamePrefix="gf-form-select-box"
-        className={selectClassNames}
-        components={selectComponents}
-        defaultValue={defaultValue}
-        value={value}
-        getOptionLabel={getOptionLabel}
-        getOptionValue={getOptionValue}
-        menuShouldScrollIntoView={false}
-        isSearchable={isSearchable}
-        onChange={onChange}
-        options={options}
-        placeholder={placeholder || 'Choose'}
-        styles={resetSelectStyles()}
-        isDisabled={isDisabled}
-        isLoading={isLoading}
-        isClearable={isClearable}
-        autoFocus={autoFocus}
-        onBlur={onBlur}
-        openMenuOnFocus={openMenuOnFocus}
-        maxMenuHeight={maxMenuHeight}
-        noOptionsMessage={noOptionsMessage}
-        isMulti={isMulti}
-        backspaceRemovesValue={backspaceRemovesValue}
-        menuIsOpen={isOpen}
-        onMenuOpen={this.onOpenMenu}
-        onMenuClose={this.onCloseMenu}
-      />,
-      tooltipContent,
-      isOpen
+
+    return (
+      <WrapInTooltip onCloseMenu={onCloseMenu} onOpenMenu={onOpenMenu} tooltipContent={tooltipContent} isOpen={isOpen}>
+        {(onOpenMenuInternal, onCloseMenuInternal) => {
+          return (
+            <ReactSelect
+              classNamePrefix="gf-form-select-box"
+              className={selectClassNames}
+              components={selectComponents}
+              defaultValue={defaultValue}
+              value={value}
+              getOptionLabel={getOptionLabel}
+              getOptionValue={getOptionValue}
+              menuShouldScrollIntoView={false}
+              isSearchable={isSearchable}
+              onChange={onChange}
+              options={options}
+              placeholder={placeholder || 'Choose'}
+              styles={resetSelectStyles()}
+              isDisabled={isDisabled}
+              isLoading={isLoading}
+              isClearable={isClearable}
+              autoFocus={autoFocus}
+              onBlur={onBlur}
+              openMenuOnFocus={openMenuOnFocus}
+              maxMenuHeight={maxMenuHeight}
+              noOptionsMessage={noOptionsMessage}
+              isMulti={isMulti}
+              backspaceRemovesValue={backspaceRemovesValue}
+              menuIsOpen={isOpen}
+              onMenuOpen={onOpenMenuInternal}
+              onMenuClose={onCloseMenuInternal}
+              tabSelectsValue={tabSelectsValue}
+            />
+          );
+        }}
+      </WrapInTooltip>
     );
   }
 }
@@ -239,6 +216,9 @@ export class AsyncSelect<T> extends PureComponent<AsyncProps<T>> {
       maxMenuHeight,
       isMulti,
       tooltipContent,
+      onCloseMenu,
+      onOpenMenu,
+      isOpen,
     } = this.props;
 
     let widthClass = '';
@@ -248,43 +228,105 @@ export class AsyncSelect<T> extends PureComponent<AsyncProps<T>> {
 
     const selectClassNames = classNames('gf-form-input', 'gf-form-input--form-dropdown', widthClass, className);
 
-    return wrapInTooltip(
-      <ReactAsyncSelect
-        classNamePrefix="gf-form-select-box"
-        className={selectClassNames}
-        components={{
-          Option: SelectOption,
-          SingleValue,
-          IndicatorsContainer,
-          NoOptionsMessage,
+    return (
+      <WrapInTooltip onCloseMenu={onCloseMenu} onOpenMenu={onOpenMenu} tooltipContent={tooltipContent} isOpen={isOpen}>
+        {(onOpenMenuInternal, onCloseMenuInternal) => {
+          return (
+            <ReactAsyncSelect
+              classNamePrefix="gf-form-select-box"
+              className={selectClassNames}
+              components={{
+                Option: SelectOption,
+                SingleValue,
+                IndicatorsContainer,
+                NoOptionsMessage,
+              }}
+              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}
+            />
+          );
         }}
-        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
+      </WrapInTooltip>
     );
   }
 }
 
+export interface TooltipWrapperProps {
+  children: (onOpenMenu: () => void, onCloseMenu: () => void) => React.ReactNode;
+  onOpenMenu?: () => void;
+  onCloseMenu?: () => void;
+  isOpen?: boolean;
+  tooltipContent?: PopperContent<any>;
+}
+
+export interface TooltipWrapperState {
+  isOpenInternal: boolean;
+}
+
+export class WrapInTooltip extends PureComponent<TooltipWrapperProps, TooltipWrapperState> {
+  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 (
+        <Tooltip show={showTooltip} content={tooltipContent} placement="bottom">
+          <div>
+            {/* div needed for tooltip */}
+            {children(this.onOpenMenu, this.onCloseMenu)}
+          </div>
+        </Tooltip>
+      );
+    } else {
+      return <div>{children(this.onOpenMenu, this.onCloseMenu)}</div>;
+    }
+  }
+}
+
 export default Select;

+ 1 - 1
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;

+ 9 - 173
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 (
           <TimePicker
-            isTimezoneUtc={false}
+            timeZone="browser"
             value={value}
-            tooltipContent="TimePicker tooltip"
             selectOptions={[
-              { 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 },
+              { 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 },
             ]}
-            popoverOptions={popoverOptions}
             onChange={timeRange => {
               action('onChange fired')(timeRange);
               updateValue(timeRange);

+ 125 - 239
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<any>;
 }
 
-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 <br /> CTRL+Z
+    </>
+  );
 };
 
 export interface State {
-  isMenuOpen: boolean;
+  isCustomOpen: boolean;
 }
-
 export class TimePicker extends PureComponent<Props, State> {
-  static defaultSelectOptions = defaultSelectOptions;
-  static defaultPopoverOptions = defaultPopoverOptions;
+  pickerTriggerRef = createRef<HTMLDivElement>();
+
   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<TimeOption>) => {
-    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;
+    }
 
-  onChangeMenuOpenState = (isOpen: boolean) => {
-    this.setState({
-      isMenuOpen: isOpen,
-    });
+    if (item.value) {
+      onChange(rawToTimeRange({ from: item.value.from, to: item.value.to }, timeZone));
+    }
   };
-  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 && <span>Custom time range</span>}
+        {!isCustomOpen && <span>{rangeString}</span>}
+        {timeZone === 'utc' && <span className="time-picker-utc">UTC</span>}
+      </>
+    );
     const isAbsolute = isDateTime(value.raw.to);
 
     return (
-      <div className="time-picker">
+      <div className="time-picker" ref={this.pickerTriggerRef}>
         <div className="time-picker-buttons">
           {isAbsolute && (
             <button className="btn navbar-button navbar-button--tight" onClick={onMoveBackward}>
@@ -278,27 +145,46 @@ export class TimePicker extends PureComponent<Props, State> {
           )}
           <ButtonSelect
             className="time-picker-button-select"
-            value={value}
-            label={rangeString}
+            value={currentOption}
+            label={label}
             options={options}
             onChange={this.onSelectChanged}
-            components={{ Group: TimePickerOptionGroup }}
             iconClass={'fa fa-clock-o fa-fw'}
-            tooltipContent={tooltipContent}
-            isMenuOpen={this.state.isMenuOpen}
-            onOpenMenu={this.onOpenMenu}
-            onCloseMenu={this.onCloseMenu}
+            tooltipContent={<TimePickerTooltipContent timeRange={value} />}
           />
           {isAbsolute && (
             <button className="btn navbar-button navbar-button--tight" onClick={onMoveForward}>
               <i className="fa fa-chevron-right" />
             </button>
           )}
-          <button className="btn navbar-button navbar-button--zoom" onClick={onZoom}>
-            <i className="fa fa-search-minus" />
-          </button>
+
+          <Tooltip content={defaultZoomOutTooltip} placement="bottom">
+            <button className="btn navbar-button navbar-button--zoom" onClick={onZoom}>
+              <i className="fa fa-search-minus" />
+            </button>
+          </Tooltip>
+
+          {isCustomOpen && (
+            <ClickOutsideWrapper onClick={this.onCloseCustom}>
+              <TimePickerPopover value={value} timeZone={timeZone} onChange={this.onCustomChange} />
+            </ClickOutsideWrapper>
+          )}
         </div>
       </div>
     );
   }
 }
+
+const TimePickerTooltipContent = ({ timeRange }: { timeRange: TimeRange }) => (
+  <>
+    {timeRange.from.format(TIME_FORMAT)}
+    <br />
+    to
+    <br />
+    {timeRange.to.format(TIME_FORMAT)}
+  </>
+);
+
+function isTimeOptionEqualToTimeRange(option: TimeOption, range: TimeRange): boolean {
+  return range.raw.from === option.from && range.raw.to === option.to;
+}

+ 1 - 1
packages/grafana-ui/src/components/TimePicker/TimePickerCalendar.story.tsx

@@ -16,7 +16,7 @@ TimePickerCalendarStories.add('default', () => (
     {(value, updateValue) => {
       return (
         <TimePickerCalendar
-          isTimezoneUtc={false}
+          timeZone="browser"
           value={value}
           onChange={timeRange => {
             action('onChange fired')(timeRange);

+ 20 - 14
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<Props> {
   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 (
       <Calendar
-        value={calendarValue}
+        value={date.toDate()}
         next2Label={null}
         prev2Label={null}
         className="time-picker-calendar"
         tileClassName="time-picker-calendar-tile"
         onChange={this.onCalendarChange}
+        nextLabel={<span className="fa fa-angle-right" />}
+        prevLabel={<span className="fa fa-angle-left" />}
       />
     );
   }

+ 7 - 6
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<Props> {
   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<Props> {
   };
 
   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<Props> {
         hideErrorMessage={true}
         value={valueString}
         className={`time-picker-input${error ? '-error' : ''}`}
+        tabIndex={tabIndex}
       />
     );
   }

+ 0 - 51
packages/grafana-ui/src/components/TimePicker/TimePickerOptionGroup.story.tsx

@@ -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', () => (
-  <TimePickerOptionGroup
-    clearValue={() => {}}
-    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<any>}
-    data={data}
-  />
-));

+ 0 - 66
packages/grafana-ui/src/components/TimePicker/TimePickerOptionGroup.tsx

@@ -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<any> {
-  data: DataProps;
-}
-
-interface State {
-  isPopoverOpen: boolean;
-}
-
-export class TimePickerOptionGroup extends PureComponent<Props, State> {
-  pickerTriggerRef = createRef<HTMLDivElement>();
-  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 (
-      <>
-        <div className="gf-form-select-box__option-group">
-          <div className="gf-form-select-box__option-group__header" ref={this.pickerTriggerRef} onClick={this.onClick}>
-            <span className="flex-grow-1">{label}</span>
-            <i className="fa fa-calendar fa-fw" />
-          </div>
-          {children}
-        </div>
-        <div>
-          {this.pickerTriggerRef.current && (
-            <Popper
-              show={isPopoverOpen}
-              content={popoverElement}
-              referenceElement={this.pickerTriggerRef.current}
-              placement={'left-start'}
-              wrapperClassName="time-picker-popover-popper"
-            />
-          )}
-        </div>
-      </>
-    );
-  }
-}

+ 1 - 3
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 (
         <TimePickerPopover
           value={value}
-          isTimezoneUtc={false}
+          timeZone="browser"
           onChange={timeRange => {
             action('onChange fired')(timeRange);
             updateValue(timeRange);
           }}
-          options={popoverOptions}
         />
       );
     }}

+ 62 - 111
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<Props, State> {
   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 (
       <div className={TimePickerPopover.popoverClassName}>
-        <div className="time-picker-popover-box">
-          <div className="time-picker-popover-box-header">
-            <span className="time-picker-popover-box-title">Quick ranges</span>
-          </div>
-          <div className="time-picker-popover-box-body">
-            {Object.keys(options).map(key => {
-              return (
-                <ul key={`popover-quickranges-${key}`}>
-                  {options[key].map(timeOption => (
-                    <li
-                      key={`popover-timeoption-${timeOption.from}-${timeOption.to}`}
-                      className={timeOption.active ? 'active' : ''}
-                    >
-                      <a
-                        onClick={(event: SyntheticEvent) => {
-                          event.preventDefault();
-                          this.onTimeOptionClick(timeOption);
-                        }}
-                      >
-                        {timeOption.display}
-                      </a>
-                    </li>
-                  ))}
-                </ul>
-              );
-            })}
-          </div>
-        </div>
-        <div className="time-picker-popover-box">
-          <div className="time-picker-popover-box-header">
-            <span className="time-picker-popover-box-title">Custom range</span>
-          </div>
-          <div className="time-picker-popover-box-body">
-            <div className="time-picker-popover-box-body-custom-ranges">
-              <div className="time-picker-popover-box-body-custom-ranges-input">
-                <span>From:</span>
+        <div className="time-picker-popover-body">
+          <div className="time-picker-popover-body-custom-ranges">
+            <div className="time-picker-popover-body-custom-ranges-input">
+              <div className="gf-form">
+                <label className="gf-form-label">From</label>
                 <TimePickerInput
-                  isTimezoneUtc={isTimezoneUtc}
                   roundup={false}
-                  timezone={timezone}
-                  value={value.raw.from}
+                  timeZone={timeZone}
+                  value={from}
                   onChange={this.onFromInputChanged}
+                  tabIndex={1}
                 />
               </div>
-              <div className="time-picker-popover-box-body-custom-ranges-calendar">
-                <TimePickerCalendar
-                  isTimezoneUtc={isTimezoneUtc}
-                  roundup={false}
-                  timezone={timezone}
-                  value={value.raw.from}
-                  onChange={this.onFromCalendarChanged}
-                />
-              </div>
             </div>
-            <div className="time-picker-popover-box-body-custom-ranges">
-              <div className="time-picker-popover-box-body-custom-ranges-input">
-                <span>To:</span>
+            <div className="time-picker-popover-body-custom-ranges-calendar">
+              <TimePickerCalendar
+                timeZone={timeZone}
+                roundup={false}
+                value={from}
+                onChange={this.onFromCalendarChanged}
+              />
+            </div>
+          </div>
+          <div className="time-picker-popover-body-custom-ranges">
+            <div className="time-picker-popover-body-custom-ranges-input">
+              <div className="gf-form">
+                <label className="gf-form-label">To</label>
                 <TimePickerInput
-                  isTimezoneUtc={isTimezoneUtc}
                   roundup={true}
-                  timezone={timezone}
-                  value={value.raw.to}
+                  timeZone={timeZone}
+                  value={to}
                   onChange={this.onToInputChanged}
-                />
-              </div>
-              <div className="time-picker-popover-box-body-custom-ranges-calendar">
-                <TimePickerCalendar
-                  isTimezoneUtc={isTimezoneUtc}
-                  roundup={true}
-                  timezone={timezone}
-                  value={value.raw.to}
-                  onChange={this.onToCalendarChanged}
+                  tabIndex={2}
                 />
               </div>
             </div>
+            <div className="time-picker-popover-body-custom-ranges-calendar">
+              <TimePickerCalendar roundup={true} timeZone={timeZone} value={to} onChange={this.onToCalendarChanged} />
+            </div>
           </div>
-          <div className="time-picker-popover-box-footer">
-            <button
-              type="submit"
-              className="btn gf-form-btn btn-success"
-              disabled={!isValid}
-              onClick={this.onApplyClick}
-            >
-              Apply
-            </button>
-          </div>
+        </div>
+        <div className="time-picker-popover-footer">
+          <button type="submit" className="btn btn-success" disabled={!isValid} onClick={this.onApplyClick}>
+            Apply
+          </button>
         </div>
       </div>
     );

+ 120 - 89
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;
-
-  .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;
-    }
+  box-shadow: $popover-shadow;
+  position: absolute;
+  flex-direction: column;
+  max-width: 600px;
+  top: 48px;
+  right: 20px;
+
+  .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-box-body-custom-ranges:first-child {
-    margin-right: $spacer;
+  .time-picker-popover-body-custom-ranges:first-child {
+    margin-right: $space-md;
   }
 
-  .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;
-  }
-}

+ 15 - 20
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);

+ 9 - 1
packages/grafana-ui/src/components/Tooltip/Popper.tsx

@@ -26,9 +26,14 @@ interface Props extends React.HTMLAttributes<HTMLDivElement> {
   referenceElement: PopperJS.ReferenceObject;
   wrapperClassName?: string;
   renderArrow?: RenderPopperArrowFn;
+  eventsEnabled?: boolean;
 }
 
 class Popper extends PureComponent<Props> {
+  static defaultProps: Partial<Props> = {
+    eventsEnabled: true,
+  };
+
   render() {
     const {
       content,
@@ -39,6 +44,8 @@ class Popper extends PureComponent<Props> {
       className,
       wrapperClassName,
       renderArrow,
+      referenceElement,
+      eventsEnabled,
     } = this.props;
 
     return (
@@ -49,7 +56,8 @@ class Popper extends PureComponent<Props> {
               <Portal>
                 <ReactPopper
                   placement={placement}
-                  referenceElement={this.props.referenceElement}
+                  referenceElement={referenceElement}
+                  eventsEnabled={eventsEnabled}
                   // TODO: move modifiers config to popper controller
                   modifiers={{ preventOverflow: { enabled: true, boundariesElement: 'window' } }}
                 >

+ 1 - 0
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

+ 6 - 0
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;
 `;

+ 7 - 0
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;
 `;

+ 4 - 13
packages/grafana-ui/src/types/time.ts

@@ -21,26 +21,17 @@ export interface IntervalValues {
   intervalMs: number;
 }
 
-export interface TimeZone {
-  raw: string;
-  isUtc: boolean;
-}
-
-export const parseTimeZone = (raw: string): TimeZone => {
-  return {
-    raw,
-    isUtc: raw === 'utc',
-  };
-};
+export type TimeZoneUtc = 'utc';
+export type TimeZoneBrowser = 'browser';
+export type TimeZone = TimeZoneBrowser | TimeZoneUtc | string;
 
-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 {

+ 2 - 3
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;
   }

+ 1 - 0
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';

+ 1 - 2
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) => {

+ 3 - 3
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', () => {

+ 4 - 4
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,
   };
 };

+ 7 - 21
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<Props> {
-  timePickerEl: HTMLElement;
-  timepickerCmp: AngularComponent;
   playlistSrv: PlaylistSrv;
 
   constructor(props: Props) {
@@ -45,21 +42,6 @@ export class DashNav extends PureComponent<Props> {
     this.playlistSrv = this.props.$injector.get('playlistSrv');
   }
 
-  componentDidMount() {
-    const loader = getAngularLoader();
-    const template =
-      '<gf-time-picker class="gf-timepicker-nav" dashboard="dashboard" ng-if="!dashboard.timepicker.hidden" />';
-    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<Props> {
   }
 
   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<Props> {
 
         {!dashboard.timepicker.hidden && (
           <div className="navbar-buttons">
-            <div className="gf-timepicker-nav" ref={element => (this.timePickerEl = element)} />
-            <DashNavTimeControls dashboard={dashboard} location={location} updateLocation={updateLocation} />
+            <DashNavTimeControls
+              $injector={$injector}
+              dashboard={dashboard}
+              location={location}
+              updateLocation={updateLocation}
+            />
           </div>
         )}
       </div>

+ 88 - 8
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<Props> {
   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<Props> {
     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 (
-      <RefreshPicker
-        onIntervalChanged={this.onChangeRefreshInterval}
-        onRefresh={this.onRefresh}
-        value={dashboard.refresh}
-        intervals={intervals}
-        tooltip="Refresh dashboard"
-      />
+      <>
+        <TimePicker
+          value={timePickerValue}
+          onChange={this.onChangeTimePicker}
+          timeZone={timeZone}
+          onMoveBackward={this.onMoveBack}
+          onMoveForward={this.onMoveForward}
+          onZoom={this.onZoom}
+          selectOptions={this.setActiveTimeOption(defaultSelectOptions, timePickerValue.raw)}
+        />
+        <RefreshPicker
+          onIntervalChanged={this.onChangeRefreshInterval}
+          onRefresh={this.onRefresh}
+          value={dashboard.refresh}
+          intervals={intervals}
+          tooltip="Refresh dashboard"
+        />
+      </>
     );
   }
 }

+ 2 - 2
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),

+ 3 - 3
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) {

+ 1 - 1
public/app/features/explore/ExploreToolbar.tsx

@@ -216,7 +216,7 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
             <div className="explore-toolbar-content-item timepicker">
               {!isLive && (
                 <ClickOutsideWrapper onClick={this.onCloseTimePicker}>
-                  <TimePicker ref={timepickerRef} range={range} isUtc={timeZone.isUtc} onChangeTime={onChangeTime} />
+                  <TimePicker ref={timepickerRef} range={range} timeZone={timeZone} onChangeTime={onChangeTime} />
                 </ClickOutsideWrapper>
               )}
 

+ 2 - 2
public/app/features/explore/Graph.tsx

@@ -129,7 +129,7 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
     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<GraphProps, GraphState> {
         max: max,
         label: 'Datetime',
         ticks: ticks,
-        timezone: timeZone.raw,
+        timezone: timeZone,
         timeformat: time_format(ticks, min, max),
       },
     };

+ 2 - 2
public/app/features/explore/GraphContainer.tsx

@@ -34,8 +34,8 @@ export class GraphContainer extends PureComponent<GraphContainerProps> {
   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);

+ 2 - 2
public/app/features/explore/LogsContainer.tsx

@@ -57,8 +57,8 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
   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);

+ 12 - 12
public/app/features/explore/TimePicker.test.tsx

@@ -29,7 +29,7 @@ const fromRaw = (rawRange: RawTimeRange): TimeRange => {
 describe('<TimePicker />', () => {
   it('render default values when closed and relative time range', () => {
     const range = fromRaw(DEFAULT_RANGE);
-    const wrapper = shallow(<TimePicker range={range} />);
+    const wrapper = shallow(<TimePicker range={range} timeZone="browser" />);
     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('<TimePicker />', () => {
 
   it('render default values when closed, utc and relative time range', () => {
     const range = fromRaw(DEFAULT_RANGE);
-    const wrapper = shallow(<TimePicker range={range} isUtc />);
+    const wrapper = shallow(<TimePicker range={range} timeZone="utc" />);
     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('<TimePicker />', () => {
 
   it('renders default values when open and relative range', () => {
     const range = fromRaw(DEFAULT_RANGE);
-    const wrapper = shallow(<TimePicker range={range} isOpen />);
+    const wrapper = shallow(<TimePicker range={range} isOpen timeZone="browser" />);
     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('<TimePicker />', () => {
 
   it('renders default values when open, utc and relative range', () => {
     const range = fromRaw(DEFAULT_RANGE);
-    const wrapper = shallow(<TimePicker range={range} isOpen isUtc />);
+    const wrapper = shallow(<TimePicker range={range} isOpen timeZone="utc" />);
     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('<TimePicker />', () => {
     const expectedRangeString = rangeUtil.describeTimeRange(localRange);
 
     const onChangeTime = sinon.spy();
-    const wrapper = shallow(<TimePicker range={range} isOpen onChangeTime={onChangeTime} />);
+    const wrapper = shallow(<TimePicker range={range} isOpen onChangeTime={onChangeTime} timeZone="browser" />);
     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('<TimePicker />', () => {
       },
     };
     const onChangeTime = sinon.spy();
-    const wrapper = shallow(<TimePicker range={range} isUtc isOpen onChangeTime={onChangeTime} />);
+    const wrapper = shallow(<TimePicker range={range} timeZone="utc" isOpen onChangeTime={onChangeTime} />);
     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('<TimePicker />', () => {
     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('<TimePicker />', () => {
     const range = fromRaw(rawRange);
 
     const onChangeTime = sinon.spy();
-    const wrapper = shallow(<TimePicker range={range} isUtc isOpen onChangeTime={onChangeTime} />);
+    const wrapper = shallow(<TimePicker range={range} isOpen onChangeTime={onChangeTime} timeZone="utc" />);
     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('<TimePicker />', () => {
     };
 
     const onChangeTime = sinon.spy();
-    const wrapper = shallow(<TimePicker range={range} isUtc={false} isOpen onChangeTime={onChangeTime} />);
+    const wrapper = shallow(<TimePicker range={range} isOpen onChangeTime={onChangeTime} timeZone="browser" />);
     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('<TimePicker />', () => {
     };
 
     const onChangeTime = sinon.spy();
-    const wrapper = shallow(<TimePicker range={range} isUtc isOpen onChangeTime={onChangeTime} />);
+    const wrapper = shallow(<TimePicker range={range} isOpen onChangeTime={onChangeTime} timeZone="utc" />);
     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('<TimePicker />', () => {
     };
 
     const onChangeTime = sinon.spy();
-    const wrapper = shallow(<TimePicker range={range} isOpen onChangeTime={onChangeTime} />);
+    const wrapper = shallow(<TimePicker range={range} isOpen onChangeTime={onChangeTime} timeZone="browser" />);
     expect(wrapper.state('fromRaw')).toBe(localRange.from.format(TIME_FORMAT));
     expect(wrapper.state('toRaw')).toBe(localRange.to.format(TIME_FORMAT));
 

+ 24 - 20
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<TimePickerProps, TimePicke
   constructor(props) {
     super(props);
 
-    const { range, isUtc, isOpen } = props;
-    const rawRange = getRaw(props.isUtc, range);
+    const { range, timeZone, isOpen } = props;
+    const rawRange = getRaw(range, timeZone);
 
     this.state = {
       isOpen: isOpen,
-      isUtc: isUtc,
+      isUtc: timeZone === 'utc',
       rangeString: rangeUtil.describeTimeRange(range.raw),
       fromRaw: rawRange.from,
       toRaw: rawRange.to,
       initialRange: range.raw,
       refreshInterval: '',
     };
-  } //Temp solution... How do detect if ds supports table format?
+  }
 
   static getDerivedStateFromProps(props: TimePickerProps, state: TimePickerState) {
     if (
@@ -85,7 +85,7 @@ export default class TimePicker extends PureComponent<TimePickerProps, TimePicke
     }
 
     const { range } = props;
-    const rawRange = getRaw(props.isUtc, range);
+    const rawRange = getRaw(range, props.timeZone);
 
     return {
       ...state,
@@ -102,8 +102,10 @@ export default class TimePicker extends PureComponent<TimePickerProps, TimePicke
       from: toUtc(origRange.from),
       to: toUtc(origRange.to),
     };
+
     const timespan = (range.to.valueOf() - range.from.valueOf()) / 2;
     let to, from;
+
     if (direction === -1) {
       to = range.to.valueOf() - timespan;
       from = range.from.valueOf() - timespan;
@@ -116,30 +118,32 @@ export default class TimePicker extends PureComponent<TimePickerProps, TimePicke
     }
 
     const nextTimeRange = {
-      from: this.props.isUtc ? toUtc(from) : dateTime(from),
-      to: this.props.isUtc ? toUtc(to) : dateTime(to),
+      from: this.props.timeZone === 'utc' ? toUtc(from) : dateTime(from),
+      to: this.props.timeZone === 'utc' ? toUtc(to) : dateTime(to),
     };
+
     if (onChangeTime) {
       onChangeTime(nextTimeRange);
     }
     return nextTimeRange;
   }
 
-  handleChangeFrom = e => {
+  handleChangeFrom = (event: ChangeEvent<HTMLInputElement>) => {
     this.setState({
-      fromRaw: e.target.value,
+      fromRaw: event.target.value,
     });
   };
 
-  handleChangeTo = e => {
+  handleChangeTo = (event: ChangeEvent<HTMLInputElement>) => {
     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<TimePickerProps, TimePicke
         };
 
         if (rawRange.from.indexOf('now') === -1) {
-          rawRange.from = isUtc ? toUtc(rawRange.from, TIME_FORMAT) : dateTime(rawRange.from, TIME_FORMAT);
+          rawRange.from = timeZone === 'utc' ? toUtc(rawRange.from, TIME_FORMAT) : dateTime(rawRange.from, TIME_FORMAT);
         }
 
         if (rawRange.to.indexOf('now') === -1) {
-          rawRange.to = isUtc ? toUtc(rawRange.to, TIME_FORMAT) : dateTime(rawRange.to, TIME_FORMAT);
+          rawRange.to = timeZone === 'utc' ? toUtc(rawRange.to, TIME_FORMAT) : dateTime(rawRange.to, TIME_FORMAT);
         }
 
         const rangeString = rangeUtil.describeTimeRange(rawRange);

+ 1 - 2
public/app/features/profile/state/selectors.ts

@@ -1,4 +1,3 @@
 import { UserState } from 'app/types';
-import { parseTimeZone } from '@grafana/ui';
 
-export const getTimeZone = (state: UserState) => parseTimeZone(state.timeZone);
+export const getTimeZone = (state: UserState) => state.timeZone;

+ 2 - 0
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",

+ 2 - 2
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');
       },
     };

+ 2 - 0
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",

+ 2 - 2
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;

+ 3 - 1
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 {

+ 6 - 0
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;

+ 7 - 0
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;

+ 1 - 1
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;

+ 1 - 1
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;
   }

+ 1 - 1
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;
 }

+ 1 - 1
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);
         },
       };

+ 0 - 818
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("<div class='error'>Could not draw pie with labels contained inside canvas</div>");
-			}
-
-			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 = "<span class='pieLabel' id='pieLabel" + index + "' style='position:absolute;top:" + y + "px;left:" + x + "px;'>" + text + "</span>";
-						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;";
-							$("<div class='pieLabelBackground' style='position:absolute;width:" + label.width() + "px;height:" + label.height() + "px;" + pos + "background-color:" + c + ";'></div>")
-								.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 "<div style='font-size:x-small;text-align:center;padding:2px;color:" + slice.color + ";'>" + label + "<br/>" + Math.round(slice.percent) + "%</div>";
-					},	// 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);