Bladeren bron

Explore & Dashboard: New Refresh picker (#16505)

* Added RefreshButton

* Added RefreshSelect

* Added RefreshSelectButton

* Added RefreshPicker

* Removed the magic string Paused

* Minor style changes and using Off instead of Pause

* Added HeadlessSelect

* Added HeadlessSelect story

* Added SelectButton

* Removed RefreshSelectButton

* Added TimePicker and moved ClickOutsideWrapper to ui/components

* Added TimePickerPopOver

* Added react-calendar

* Missed yarn lock file

* Added inputs to popover

* Added TimePicker and RefreshPicker to DashNav

* Moved TimePicker and RefreshPicker to app/core

* Added react-calendar to app and removed from ui/components

* Fixed PopOver onClick

* Moved everything back to ui components because of typings problems

* Exporing RefreshPicker and TimePicker

* Added Apply and inputs

* Added typings

* Added TimePickerInput and logic

* Fixed parsing of string to Moments

* Fixed range string

* Styling and connecting the calendars and inputs

* Changed Calendar styling

* Added backward forward and zoom

* Fixed responsive styles

* Moved TimePicker and RefreshPicker into app core

* Renamed menuIsOpen to isOpen

* Changed from className={} to className=""

* Moved Popover to TimePickerOptionGroup

* Renamed all PopOver to Popover

* Renamed popOver to popover and some minor refactorings

* Renamed files with git mv

* Added ButtonSelect and refactored RefreshPicker

* Refactored TimePicker to use new ButtonSelect

* Removed HeadlessSelect as suggested

* fix: Fix typings and misc errors after rebase

* wip: Enable time picker on dashboard and add tooltip

* Merge branch 'master' into hugoh/new-timepicker-and-unified-component

# Conflicts:
#	packages/grafana-ui/package.json
#	packages/grafana-ui/src/components/Input/Input.test.tsx
#	packages/grafana-ui/src/components/Input/Input.tsx
#	packages/grafana-ui/src/utils/validate.ts
#	public/app/features/dashboard/panel_editor/QueryOptions.tsx
#	yarn.lock

* fix: Snapshot update

* Move TimePicker default options into the TimePicker as statics, pass the tooltipContent down the line when wanted and wrap the button in a tooltip element

* fix: Override internal state prop if we provide one in a prop

* Updated snapshots

* Let dashnav control refreshPicker state

* feat: Add a stringToMs function

* wip: RefreshPicker

* wip: Move RefreshPicker to @grafana/ui

* wip: Move TimePicker to @grafana/ui

* wip: Remove comments

* wip: Add refreshPicker to explore

* wip: Use default intervals if the prop is missing

* wip: Nicer way of setting defaults

* fix: Control the select component

* wip: Add onMoveForward/onMoveBack

* Remove code related to the new time picker and refresh picker from dashnav

* Fix: Typings after merge

* chore: Minor fix after merge

* chore: Remove _.map usage

* chore: Moved refresh-picker logic out of the refresh picker since it will work a little differently in explore and dashboards until we have replaced the TimeSrv

* feat: Add an Interval component to @grafana/ui

* chore: Remove intervalId from redux state and move setInterval logic from ExploreToolbar to its own Interval component

* feat: Add refreshInterval to Explore's URL state

* feat: Pick up refreshInterval from url on page load

* fix: Set default refreshInterval when no value can be retained from URL

* fix: Update test initial state with refreshInterval

* fix: Handle URLs before RefreshPicker

* fix: Move RefreshInterval to url position 3 since the segments can take multiple positions

* fix: A better way of detecting urls without RefreshInterval in Explore

* chore: Some Explore typings

* fix: Attach refresh picker to interval picker

* chore: Sass fix for refresh button border radius

* fix: Remove refreshInterval from URL

* fix: Intervals now start when previous interval is finished

* fix: Use clearTimeout instead of clearInterval

* fix: Make sure there's a delay set before adding a timeout when we have slow explore queries

* wip: Add refresh picker to dashboard

* feat: Add util for removing keys with empty values

* feat: RefreshPicker in dashboards and tmp rem out old RefreshPicker

* fix: Remove the jumpy:ness in the refreshpicker

* Changed placement and made it hide when your in dashboard settings

* chore: Move logic related to refresh picker out of DashNav to its own component

* feat: Add tooltip to refreshpicker

* fix: Fix bug with refreshpicker not updating when setting to 'off'

* fix: Make it possible to override refresh intervals using the dashboard intervals

* chore: Change name of Interval to SetInterval to align with ecmascripts naming since its basically the same but declarative and async

* fix: Use default intervals when auto refresh is empty in dashboard settings

* fix: Hide time/interval picker when hidden is true on the model, such as on the home dashboard

* fix: Interval picker will have to handle location changes since timeSrv wont

* RefreshPicker: Refactoring refresh picker

* RefreshPicker: minor refactoring
Johannes Schill 6 jaren geleden
bovenliggende
commit
406ef962fc
59 gewijzigde bestanden met toevoegingen van 2551 en 537 verwijderingen
  1. 1 0
      packages/grafana-ui/package.json
  2. 1 1
      packages/grafana-ui/src/components/ClickOutsideWrapper/ClickOutsideWrapper.tsx
  3. 32 0
      packages/grafana-ui/src/components/RefreshPicker/RefreshPicker.story.tsx
  4. 80 0
      packages/grafana-ui/src/components/RefreshPicker/RefreshPicker.tsx
  5. 28 0
      packages/grafana-ui/src/components/RefreshPicker/_RefreshPicker.scss
  6. 41 0
      packages/grafana-ui/src/components/Select/ButtonSelect.story.tsx
  7. 88 0
      packages/grafana-ui/src/components/Select/ButtonSelect.tsx
  8. 70 16
      packages/grafana-ui/src/components/Select/Select.tsx
  9. 1 1
      packages/grafana-ui/src/components/Select/SelectOption.tsx
  10. 4 0
      packages/grafana-ui/src/components/Select/_Select.scss
  11. 49 0
      packages/grafana-ui/src/components/SetInterval/SetInterval.tsx
  12. 220 0
      packages/grafana-ui/src/components/TimePicker/TimePicker.story.tsx
  13. 300 0
      packages/grafana-ui/src/components/TimePicker/TimePicker.tsx
  14. 29 0
      packages/grafana-ui/src/components/TimePicker/TimePickerCalendar.story.tsx
  15. 46 0
      packages/grafana-ui/src/components/TimePicker/TimePickerCalendar.tsx
  16. 60 0
      packages/grafana-ui/src/components/TimePicker/TimePickerInput.tsx
  17. 51 0
      packages/grafana-ui/src/components/TimePicker/TimePickerOptionGroup.story.tsx
  18. 66 0
      packages/grafana-ui/src/components/TimePicker/TimePickerOptionGroup.tsx
  19. 37 0
      packages/grafana-ui/src/components/TimePicker/TimePickerPopover.story.tsx
  20. 169 0
      packages/grafana-ui/src/components/TimePicker/TimePickerPopover.tsx
  21. 189 0
      packages/grafana-ui/src/components/TimePicker/_TimePicker.scss
  22. 44 0
      packages/grafana-ui/src/components/TimePicker/time.ts
  23. 8 1
      packages/grafana-ui/src/components/Tooltip/Tooltip.tsx
  24. 2 0
      packages/grafana-ui/src/components/index.scss
  25. 6 1
      packages/grafana-ui/src/components/index.ts
  26. 1 0
      packages/grafana-ui/src/themes/_variables.scss.tmpl.ts
  27. 16 0
      packages/grafana-ui/src/types/time.ts
  28. 1 0
      packages/grafana-ui/src/utils/index.ts
  29. 8 0
      packages/grafana-ui/src/utils/object.ts
  30. 20 0
      packages/grafana-ui/src/utils/storybook/withRightAlignedStory.tsx
  31. 39 1
      packages/grafana-ui/src/utils/string.test.ts
  32. 38 0
      packages/grafana-ui/src/utils/string.ts
  33. 65 63
      public/app/core/components/Select/__snapshots__/TeamPicker.test.tsx.snap
  34. 65 63
      public/app/core/components/Select/__snapshots__/UserPicker.test.tsx.snap
  35. 4 2
      public/app/core/utils/datemath.ts
  36. 1 1
      public/app/core/utils/explore.test.ts
  37. 7 2
      public/app/core/utils/explore.ts
  38. 8 7
      public/app/core/utils/rangeutil.ts
  39. 19 7
      public/app/features/dashboard/components/DashNav/DashNav.tsx
  40. 53 0
      public/app/features/dashboard/components/DashNav/DashNavTimeControls.tsx
  41. 1 1
      public/app/features/dashboard/components/TimePicker/TimePickerCtrl.ts
  42. 20 22
      public/app/features/dashboard/components/TimePicker/template.html
  43. 1 1
      public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx
  44. 12 9
      public/app/features/dashboard/services/TimeSrv.ts
  45. 0 1
      public/app/features/explore/Explore.tsx
  46. 47 11
      public/app/features/explore/ExploreToolbar.tsx
  47. 16 0
      public/app/features/explore/state/actionTypes.ts
  48. 10 1
      public/app/features/explore/state/actions.test.ts
  49. 59 41
      public/app/features/explore/state/actions.ts
  50. 38 0
      public/app/features/explore/state/reducers.test.ts
  51. 20 3
      public/app/features/explore/state/reducers.ts
  52. 20 0
      public/app/features/teams/__snapshots__/TeamMemberRow.test.tsx.snap
  53. 65 63
      public/app/plugins/datasource/stackdriver/components/__snapshots__/Aggregations.test.tsx.snap
  54. 226 218
      public/app/plugins/datasource/stackdriver/components/__snapshots__/QueryEditor.test.tsx.snap
  55. 5 0
      public/app/types/explore.ts
  56. 1 0
      public/sass/_variables.generated.scss
  57. 10 0
      public/sass/components/_buttons.scss
  58. 11 0
      public/sass/components/_navbar.scss
  59. 22 0
      yarn.lock

+ 1 - 0
packages/grafana-ui/package.json

@@ -28,6 +28,7 @@
     "moment": "^2.22.2",
     "papaparse": "^4.6.3",
     "react": "^16.8.4",
+    "react-calendar": "^2.18.1",
     "react-color": "^2.17.0",
     "react-custom-scrollbars": "^4.2.1",
     "react-dom": "^16.8.4",

+ 1 - 1
public/app/core/components/ClickOutsideWrapper/ClickOutsideWrapper.tsx → packages/grafana-ui/src/components/ClickOutsideWrapper/ClickOutsideWrapper.tsx

@@ -22,7 +22,7 @@ export class ClickOutsideWrapper extends PureComponent<Props, State> {
     window.removeEventListener('click', this.onOutsideClick, false);
   }
 
-  onOutsideClick = event => {
+  onOutsideClick = (event: any) => {
     const domNode = ReactDOM.findDOMNode(this) as Element;
 
     if (!domNode || !domNode.contains(event.target)) {

+ 32 - 0
packages/grafana-ui/src/components/RefreshPicker/RefreshPicker.story.tsx

@@ -0,0 +1,32 @@
+import React from 'react';
+import { storiesOf } from '@storybook/react';
+import { action } from '@storybook/addon-actions';
+import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
+import { UseState } from '../../utils/storybook/UseState';
+import { RefreshPicker } from './RefreshPicker';
+
+const RefreshSelectStories = storiesOf('UI/RefreshPicker', module);
+
+RefreshSelectStories.addDecorator(withCenteredStory);
+
+RefreshSelectStories.add('default', () => {
+  return (
+    <UseState initialState={'1h'}>
+      {(value, updateValue) => {
+        return (
+          <RefreshPicker
+            tooltip="Hello world"
+            value={value}
+            intervals={['5s', '10s', '30s', '1m', '5m', '15m', '30m', '1h', '2h', '1d']}
+            onIntervalChanged={interval => {
+              action('onIntervalChanged fired')(interval);
+            }}
+            onRefresh={() => {
+              action('onRefresh fired')();
+            }}
+          />
+        );
+      }}
+    </UseState>
+  );
+});

+ 80 - 0
packages/grafana-ui/src/components/RefreshPicker/RefreshPicker.tsx

@@ -0,0 +1,80 @@
+import React, { PureComponent } from 'react';
+import classNames from 'classnames';
+
+import { SelectOptionItem, ButtonSelect, Tooltip } from '@grafana/ui';
+
+export const offOption = { label: 'Off', value: '' };
+export const defaultIntervals = ['5s', '10s', '30s', '1m', '5m', '15m', '30m', '1h', '2h', '1d'];
+
+export interface Props {
+  intervals?: string[];
+  onRefresh: () => any;
+  onIntervalChanged: (interval: string) => void;
+  value?: string;
+  tooltip: string;
+}
+
+export class RefreshPicker extends PureComponent<Props> {
+  static defaultProps = {
+    intervals: defaultIntervals,
+  };
+
+  constructor(props: Props) {
+    super(props);
+  }
+
+  hasNoIntervals = () => {
+    const { intervals } = this.props;
+    // Current implementaion returns an array with length of 1 consisting of
+    // an empty string when auto-refresh is empty in dashboard settings
+    if (!intervals || intervals.length < 1 || (intervals.length === 1 && intervals[0] === '')) {
+      return true;
+    }
+    return false;
+  };
+
+  intervalsToOptions = (intervals: string[] = defaultIntervals): SelectOptionItem[] => {
+    const options = intervals.map(interval => ({ label: interval, value: interval }));
+    options.unshift(offOption);
+    return options;
+  };
+
+  onChangeSelect = (item: SelectOptionItem) => {
+    const { onIntervalChanged } = this.props;
+    if (onIntervalChanged) {
+      onIntervalChanged(item.value);
+    }
+  };
+
+  render() {
+    const { onRefresh, intervals, tooltip, value } = this.props;
+    const options = this.intervalsToOptions(this.hasNoIntervals() ? defaultIntervals : intervals);
+    const currentValue = value || '';
+    const selectedValue = options.find(item => item.value === currentValue) || offOption;
+
+    const cssClasses = classNames({
+      'refresh-picker': true,
+      'refresh-picker--refreshing': selectedValue.label !== offOption.label,
+    });
+
+    return (
+      <div className={cssClasses}>
+        <div className="refresh-picker-buttons">
+          <Tooltip placement="top" content={tooltip}>
+            <button className="btn btn--radius-right-0 navbar-button navbar-button--refresh" onClick={onRefresh}>
+              <i className="fa fa-refresh" />
+            </button>
+          </Tooltip>
+          <ButtonSelect
+            className="navbar-button--attached btn--radius-left-0"
+            value={selectedValue}
+            label={selectedValue.label}
+            options={options}
+            onChange={this.onChangeSelect}
+            maxMenuHeight={380}
+          />
+        </div>
+      </div>
+    );
+  }
+}

+ 28 - 0
packages/grafana-ui/src/components/RefreshPicker/_RefreshPicker.scss

@@ -0,0 +1,28 @@
+.refresh-picker {
+  position: relative;
+  display: none;
+
+  .refresh-picker-buttons {
+    display: flex;
+  }
+
+  .gf-form-input--form-dropdown {
+    position: static;
+  }
+
+  .gf-form-select-box__menu {
+    position: absolute;
+    left: 0;
+    width: 100%;
+  }
+
+  &--refreshing {
+    .select-button-value {
+      color: $orange;
+    }
+  }
+
+  @include media-breakpoint-up(md) {
+    display: block;
+  }
+}

+ 41 - 0
packages/grafana-ui/src/components/Select/ButtonSelect.story.tsx

@@ -0,0 +1,41 @@
+import React from 'react';
+import { storiesOf } from '@storybook/react';
+import { action } from '@storybook/addon-actions';
+import { withKnobs, object, text } from '@storybook/addon-knobs';
+import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
+import { UseState } from '../../utils/storybook/UseState';
+import { SelectOptionItem } from './Select';
+import { ButtonSelect } from './ButtonSelect';
+
+const ButtonSelectStories = storiesOf('UI/Select/ButtonSelect', module);
+
+ButtonSelectStories.addDecorator(withCenteredStory).addDecorator(withKnobs);
+
+ButtonSelectStories.add('default', () => {
+  const intialState: SelectOptionItem = { label: 'A label', value: 'A value' };
+  const value = object<SelectOptionItem>('Selected Value:', intialState);
+  const options = object<SelectOptionItem[]>('Options:', [
+    intialState,
+    { label: 'Another label', value: 'Another value' },
+  ]);
+
+  return (
+    <UseState initialState={value}>
+      {(value, updateValue) => {
+        return (
+          <ButtonSelect
+            value={value}
+            options={options}
+            onChange={value => {
+              action('onChanged fired')(value);
+              updateValue(value);
+            }}
+            label={value.label ? value.label : ''}
+            className="refresh-select"
+            iconClass={text('iconClass', 'fa fa-clock-o fa-fw')}
+          />
+        );
+      }}
+    </UseState>
+  );
+});

+ 88 - 0
packages/grafana-ui/src/components/Select/ButtonSelect.tsx

@@ -0,0 +1,88 @@
+import React, { PureComponent } from 'react';
+import Select, { SelectOptionItem } from './Select';
+import { PopperContent } from '@grafana/ui/src/components/Tooltip/PopperController';
+
+interface ButtonComponentProps {
+  label: string | undefined;
+  className: string | undefined;
+  iconClass?: string;
+}
+
+const ButtonComponent = (buttonProps: ButtonComponentProps) => (props: any) => {
+  const { label, className, iconClass } = buttonProps;
+
+  return (
+    <button
+      ref={props.innerRef}
+      className={`btn navbar-button navbar-button--tight ${className}`}
+      onClick={props.selectProps.menuIsOpen ? props.selectProps.onMenuClose : props.selectProps.onMenuOpen}
+      onBlur={props.selectProps.onMenuClose}
+    >
+      <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" />
+      </div>
+    </button>
+  );
+};
+
+export interface Props {
+  className: string | undefined;
+  options: SelectOptionItem[];
+  value: SelectOptionItem;
+  label?: string;
+  iconClass?: string;
+  components?: any;
+  maxMenuHeight?: number;
+  onChange: (item: SelectOptionItem) => void;
+  tooltipContent?: PopperContent<any>;
+  isMenuOpen?: boolean;
+  onOpenMenu?: () => void;
+  onCloseMenu?: () => void;
+}
+
+export class ButtonSelect extends PureComponent<Props> {
+  onChange = (item: SelectOptionItem) => {
+    const { onChange } = this.props;
+    onChange(item);
+  };
+
+  render() {
+    const {
+      className,
+      options,
+      value,
+      label,
+      iconClass,
+      components,
+      maxMenuHeight,
+      tooltipContent,
+      isMenuOpen,
+      onOpenMenu,
+      onCloseMenu,
+    } = this.props;
+    const combinedComponents = {
+      ...components,
+      Control: ButtonComponent({ label, className, iconClass }),
+    };
+    return (
+      <Select
+        autoFocus
+        backspaceRemovesValue={false}
+        isClearable={false}
+        isSearchable={false}
+        options={options}
+        onChange={this.onChange}
+        defaultValue={value}
+        maxMenuHeight={maxMenuHeight}
+        components={combinedComponents}
+        className="gf-form-select-box-button-select"
+        tooltipContent={tooltipContent}
+        isOpen={isMenuOpen}
+        onOpenMenu={onOpenMenu}
+        onCloseMenu={onCloseMenu}
+      />
+    );
+  }
+}

+ 70 - 16
packages/grafana-ui/src/components/Select/Select.tsx

@@ -17,6 +17,8 @@ import IndicatorsContainer from './IndicatorsContainer';
 import NoOptionsMessage from './NoOptionsMessage';
 import resetSelectStyles from './resetSelectStyles';
 import { CustomScrollbar } from '../CustomScrollbar/CustomScrollbar';
+import { PopperContent } from '@grafana/ui/src/components/Tooltip/PopperController';
+import { Tooltip } from '@grafana/ui';
 
 export interface SelectOptionItem {
   label?: string;
@@ -26,7 +28,7 @@ export interface SelectOptionItem {
   [key: string]: any;
 }
 
-interface CommonProps {
+export interface CommonProps {
   defaultValue?: any;
   getOptionLabel?: (item: SelectOptionItem) => string;
   getOptionValue?: (item: SelectOptionItem) => string;
@@ -42,13 +44,18 @@ interface CommonProps {
   openMenuOnFocus?: boolean;
   onBlur?: () => void;
   maxMenuHeight?: number;
-  isLoading: boolean;
+  isLoading?: boolean;
   noOptionsMessage?: () => string;
   isMulti?: boolean;
-  backspaceRemovesValue: boolean;
+  backspaceRemovesValue?: boolean;
+  isOpen?: boolean;
+  components?: any;
+  tooltipContent?: PopperContent<any>;
+  onOpenMenu?: () => void;
+  onCloseMenu?: () => void;
 }
 
-interface SelectProps {
+export interface SelectProps {
   options: SelectOptionItem[];
 }
 
@@ -58,6 +65,26 @@ interface AsyncProps {
   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}>
@@ -81,6 +108,28 @@ export class Select extends PureComponent<CommonProps & SelectProps> {
     isLoading: false,
     backspaceRemovesValue: true,
     maxMenuHeight: 300,
+    menuIsOpen: false,
+    components: {
+      Option: SelectOption,
+      SingleValue,
+      IndicatorsContainer,
+      MenuList,
+      Group: SelectOptionGroup,
+    },
+  };
+
+  onOpenMenu = () => {
+    const { onOpenMenu } = this.props;
+    if (onOpenMenu) {
+      onOpenMenu();
+    }
+  };
+
+  onCloseMenu = () => {
+    const { onCloseMenu } = this.props;
+    if (onCloseMenu) {
+      onCloseMenu();
+    }
   };
 
   render() {
@@ -105,6 +154,9 @@ export class Select extends PureComponent<CommonProps & SelectProps> {
       onBlur,
       maxMenuHeight,
       noOptionsMessage,
+      isOpen,
+      components,
+      tooltipContent,
     } = this.props;
 
     let widthClass = '';
@@ -113,18 +165,12 @@ export class Select extends PureComponent<CommonProps & SelectProps> {
     }
 
     const selectClassNames = classNames('gf-form-input', 'gf-form-input--form-dropdown', widthClass, className);
-
-    return (
+    const selectComponents = { ...Select.defaultProps.components, ...components };
+    return wrapInTooltip(
       <ReactSelect
         classNamePrefix="gf-form-select-box"
         className={selectClassNames}
-        components={{
-          Option: SelectOption,
-          SingleValue,
-          IndicatorsContainer,
-          MenuList,
-          Group: SelectOptionGroup,
-        }}
+        components={selectComponents}
         defaultValue={defaultValue}
         value={value}
         getOptionLabel={getOptionLabel}
@@ -145,7 +191,12 @@ export class Select extends PureComponent<CommonProps & SelectProps> {
         noOptionsMessage={noOptionsMessage}
         isMulti={isMulti}
         backspaceRemovesValue={backspaceRemovesValue}
-      />
+        menuIsOpen={isOpen}
+        onMenuOpen={this.onOpenMenu}
+        onMenuClose={this.onCloseMenu}
+      />,
+      tooltipContent,
+      isOpen
     );
   }
 }
@@ -190,6 +241,7 @@ export class AsyncSelect extends PureComponent<CommonProps & AsyncProps> {
       openMenuOnFocus,
       maxMenuHeight,
       isMulti,
+      tooltipContent,
     } = this.props;
 
     let widthClass = '';
@@ -199,7 +251,7 @@ export class AsyncSelect extends PureComponent<CommonProps & AsyncProps> {
 
     const selectClassNames = classNames('gf-form-input', 'gf-form-input--form-dropdown', widthClass, className);
 
-    return (
+    return wrapInTooltip(
       <ReactAsyncSelect
         classNamePrefix="gf-form-select-box"
         className={selectClassNames}
@@ -231,7 +283,9 @@ export class AsyncSelect extends PureComponent<CommonProps & AsyncProps> {
         maxMenuHeight={maxMenuHeight}
         isMulti={isMulti}
         backspaceRemovesValue={backspaceRemovesValue}
-      />
+      />,
+      tooltipContent,
+      false
     );
   }
 }

+ 1 - 1
packages/grafana-ui/src/components/Select/SelectOption.tsx

@@ -6,7 +6,7 @@ import { components } from '@torkelo/react-select';
 import { OptionProps } from 'react-select/lib/components/Option';
 
 // https://github.com/JedWatson/react-select/issues/3038
-interface ExtendedOptionProps extends OptionProps<any> {
+export interface ExtendedOptionProps extends OptionProps<any> {
   data: {
     description?: string;
     imgUrl?: string;

+ 4 - 0
packages/grafana-ui/src/components/Select/_Select.scss

@@ -189,3 +189,7 @@ $select-input-bg-disabled: $input-bg-disabled;
     padding-right: 2px;
   }
 }
+
+.gf-form-select-box-button-select {
+  height: auto;
+}

+ 49 - 0
packages/grafana-ui/src/components/SetInterval/SetInterval.tsx

@@ -0,0 +1,49 @@
+import { PureComponent } from 'react';
+import { stringToMs } from '../../utils/string';
+
+interface Props {
+  func: () => any; // TODO
+  interval: string;
+}
+
+export class SetInterval extends PureComponent<Props> {
+  private intervalId = 0;
+
+  componentDidMount() {
+    this.addInterval();
+  }
+
+  componentDidUpdate(prevProps: Props) {
+    const { interval } = this.props;
+    if (interval !== prevProps.interval) {
+      this.clearInterval();
+      this.addInterval();
+    }
+  }
+
+  componentWillUnmount() {
+    this.clearInterval();
+  }
+
+  addInterval = () => {
+    const { func, interval } = this.props;
+
+    if (interval) {
+      func().then(() => {
+        if (interval) {
+          this.intervalId = window.setTimeout(() => {
+            this.addInterval();
+          }, stringToMs(interval));
+        }
+      });
+    }
+  };
+
+  clearInterval = () => {
+    window.clearTimeout(this.intervalId);
+  };
+
+  render() {
+    return null;
+  }
+}

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

@@ -0,0 +1,220 @@
+import React from 'react';
+import moment, { Moment } from 'moment';
+import { storiesOf } from '@storybook/react';
+import { action } from '@storybook/addon-actions';
+
+import { TimePicker } from './TimePicker';
+import { UseState } from '../../utils/storybook/UseState';
+import { withRighAlignedStory } from '../../utils/storybook/withRightAlignedStory';
+
+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);
+
+TimePickerStories.add('default', () => {
+  return (
+    <UseState
+      initialState={{
+        from: moment(),
+        to: moment(),
+        raw: { from: 'now-6h' as string | Moment, to: 'now' as string | Moment },
+      }}
+    >
+      {(value, updateValue) => {
+        return (
+          <TimePicker
+            isTimezoneUtc={false}
+            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 },
+            ]}
+            popoverOptions={popoverOptions}
+            onChange={timeRange => {
+              action('onChange fired')(timeRange);
+              updateValue(timeRange);
+            }}
+            onMoveBackward={() => {
+              action('onMoveBackward fired')();
+            }}
+            onMoveForward={() => {
+              action('onMoveForward fired')();
+            }}
+            onZoom={() => {
+              action('onZoom fired')();
+            }}
+          />
+        );
+      }}
+    </UseState>
+  );
+});

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

@@ -0,0 +1,300 @@
+import React, { PureComponent } from 'react';
+import moment from 'moment';
+import { TimeRange, TimeOptions, TimeOption, SelectOptionItem } from '@grafana/ui';
+import { ButtonSelect } from '@grafana/ui/src/components/Select/ButtonSelect';
+import { mapTimeOptionToTimeRange, mapTimeRangeToRangeString } from './time';
+import { Props as TimePickerPopoverProps } from './TimePickerPopover';
+import { TimePickerOptionGroup } from './TimePickerOptionGroup';
+import { PopperContent } from '@grafana/ui/src/components/Tooltip/PopperController';
+import { Timezone } from '../../../../../public/app/core/utils/datemath';
+
+export interface Props {
+  value: TimeRange;
+  isTimezoneUtc: boolean;
+  popoverOptions: TimeOptions;
+  selectOptions: TimeOption[];
+  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 },
+];
+
+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,
+    },
+  ],
+};
+
+export interface State {
+  isMenuOpen: boolean;
+}
+
+export class TimePicker extends PureComponent<Props, State> {
+  static defaultSelectOptions = defaultSelectOptions;
+  static defaultPopoverOptions = defaultPopoverOptions;
+  state: State = {
+    isMenuOpen: false,
+  };
+
+  mapTimeOptionsToSelectOptionItems = (selectOptions: TimeOption[]) => {
+    const { value, popoverOptions, isTimezoneUtc, timezone } = this.props;
+    const options = selectOptions.map(timeOption => {
+      return { label: timeOption.display, value: timeOption };
+    });
+
+    const popoverProps: TimePickerPopoverProps = {
+      value,
+      options: popoverOptions,
+      isTimezoneUtc,
+      timezone,
+    };
+
+    return [
+      {
+        label: 'Custom',
+        expanded: true,
+        options,
+        onPopoverOpen: () => undefined,
+        onPopoverClose: (timeRange: TimeRange) => this.onPopoverClose(timeRange),
+        popoverProps,
+      },
+    ];
+  };
+
+  onSelectChanged = (item: SelectOptionItem) => {
+    const { isTimezoneUtc, onChange, timezone } = this.props;
+    onChange(mapTimeOptionToTimeRange(item.value, isTimezoneUtc, timezone));
+  };
+
+  onChangeMenuOpenState = (isOpen: boolean) => {
+    this.setState({
+      isMenuOpen: isOpen,
+    });
+  };
+  onOpenMenu = () => this.onChangeMenuOpenState(true);
+  onCloseMenu = () => this.onChangeMenuOpenState(false);
+
+  onPopoverClose = (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();
+  };
+
+  render() {
+    const {
+      selectOptions: selectTimeOptions,
+      value,
+      onMoveBackward,
+      onMoveForward,
+      onZoom,
+      tooltipContent,
+    } = this.props;
+    const options = this.mapTimeOptionsToSelectOptionItems(selectTimeOptions);
+    const rangeString = mapTimeRangeToRangeString(value);
+    const isAbsolute = moment.isMoment(value.raw.to);
+    return (
+      <div className="time-picker">
+        <div className="time-picker-buttons">
+          {isAbsolute && (
+            <button className="btn navbar-button navbar-button--tight" onClick={onMoveBackward}>
+              <i className="fa fa-chevron-left" />
+            </button>
+          )}
+          <ButtonSelect
+            className="time-picker-button-select"
+            value={value}
+            label={rangeString}
+            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}
+          />
+          {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>
+        </div>
+      </div>
+    );
+  }
+}

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

@@ -0,0 +1,29 @@
+import React from 'react';
+import { storiesOf } from '@storybook/react';
+import { action } from '@storybook/addon-actions';
+import { Moment } from 'moment';
+
+import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
+import { TimePickerCalendar } from './TimePickerCalendar';
+import { UseState } from '../../utils/storybook/UseState';
+
+const TimePickerCalendarStories = storiesOf('UI/TimePicker/TimePickerCalendar', module);
+
+TimePickerCalendarStories.addDecorator(withCenteredStory);
+
+TimePickerCalendarStories.add('default', () => (
+  <UseState initialState={'now-6h' as string | Moment}>
+    {(value, updateValue) => {
+      return (
+        <TimePickerCalendar
+          isTimezoneUtc={false}
+          value={value}
+          onChange={timeRange => {
+            action('onChange fired')(timeRange);
+            updateValue(timeRange);
+          }}
+        />
+      );
+    }}
+  </UseState>
+));

+ 46 - 0
packages/grafana-ui/src/components/TimePicker/TimePickerCalendar.tsx

@@ -0,0 +1,46 @@
+import React, { PureComponent } from 'react';
+import Calendar from 'react-calendar/dist/entry.nostyle';
+import moment, { Moment } from 'moment';
+import { TimeFragment } from '@grafana/ui';
+import { Timezone } from '../../../../../public/app/core/utils/datemath';
+
+import { stringToMoment } from './time';
+
+export interface Props {
+  value: TimeFragment;
+  isTimezoneUtc: boolean;
+  roundup?: boolean;
+  timezone?: Timezone;
+  onChange: (value: Moment) => void;
+}
+
+export class TimePickerCalendar extends PureComponent<Props> {
+  onCalendarChange = (date: Date | Date[]) => {
+    const { onChange } = this.props;
+
+    if (Array.isArray(date)) {
+      return;
+    }
+
+    onChange(moment(date));
+  };
+
+  render() {
+    const { value, isTimezoneUtc, roundup, timezone } = this.props;
+    const dateValue = moment.isMoment(value)
+      ? value.toDate()
+      : stringToMoment(value, isTimezoneUtc, roundup, timezone).toDate();
+    const calendarValue = dateValue instanceof Date && !isNaN(dateValue.getTime()) ? dateValue : moment().toDate();
+
+    return (
+      <Calendar
+        value={calendarValue}
+        next2Label={null}
+        prev2Label={null}
+        className="time-picker-calendar"
+        tileClassName="time-picker-calendar-tile"
+        onChange={this.onCalendarChange}
+      />
+    );
+  }
+}

+ 60 - 0
packages/grafana-ui/src/components/TimePicker/TimePickerInput.tsx

@@ -0,0 +1,60 @@
+import React, { PureComponent, ChangeEvent } from 'react';
+import moment from 'moment';
+import { TimeFragment, TIME_FORMAT, Input } from '@grafana/ui';
+
+import { stringToMoment, isValidTimeString } from './time';
+
+export interface Props {
+  value: TimeFragment;
+  isTimezoneUtc: boolean;
+  roundup?: boolean;
+  timezone?: string;
+  onChange: (value: string, isValid: boolean) => void;
+}
+
+export class TimePickerInput extends PureComponent<Props> {
+  isValid = (value: string) => {
+    const { isTimezoneUtc } = this.props;
+
+    if (value.indexOf('now') !== -1) {
+      const isValid = isValidTimeString(value);
+      return isValid;
+    }
+
+    const parsed = stringToMoment(value, isTimezoneUtc);
+    const isValid = parsed.isValid();
+    return isValid;
+  };
+
+  onChange = (event: ChangeEvent<HTMLInputElement>) => {
+    const { onChange } = this.props;
+    const value = event.target.value;
+
+    onChange(value, this.isValid(value));
+  };
+
+  valueToString = (value: TimeFragment) => {
+    if (moment.isMoment(value)) {
+      return value.format(TIME_FORMAT);
+    } else {
+      return value;
+    }
+  };
+
+  render() {
+    const { value } = this.props;
+    const valueString = this.valueToString(value);
+    const error = !this.isValid(valueString);
+
+    return (
+      <Input
+        type="text"
+        onChange={this.onChange}
+        onBlur={this.onChange}
+        hideErrorMessage={true}
+        value={valueString}
+        className={`time-picker-input${error ? '-error' : ''}`}
+      />
+    );
+  }
+}

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

@@ -0,0 +1,51 @@
+import React, { ComponentType } from 'react';
+import { storiesOf } from '@storybook/react';
+import moment from 'moment';
+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';
+
+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: moment(), to: moment(), 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}
+  />
+));

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

@@ -0,0 +1,66 @@
+import React, { PureComponent, createRef } from 'react';
+import { GroupProps } from 'react-select/lib/components/Group';
+import { Popper } from '@grafana/ui/src/components/Tooltip/Popper';
+import { Props as TimePickerProps, TimePickerPopover } from './TimePickerPopover';
+import { TimeRange } from '@grafana/ui';
+
+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>
+      </>
+    );
+  }
+}

+ 37 - 0
packages/grafana-ui/src/components/TimePicker/TimePickerPopover.story.tsx

@@ -0,0 +1,37 @@
+import React from 'react';
+import { action } from '@storybook/addon-actions';
+import moment, { Moment } from 'moment';
+
+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';
+
+const TimePickerPopoverStories = storiesOf('UI/TimePicker/TimePickerPopover', module);
+
+TimePickerPopoverStories.addDecorator(withCenteredStory);
+
+TimePickerPopoverStories.add('default', () => (
+  <UseState
+    initialState={{
+      from: moment(),
+      to: moment(),
+      raw: { from: 'now-6h' as string | Moment, to: 'now' as string | Moment },
+    }}
+  >
+    {(value, updateValue) => {
+      return (
+        <TimePickerPopover
+          value={value}
+          isTimezoneUtc={false}
+          onChange={timeRange => {
+            action('onChange fired')(timeRange);
+            updateValue(timeRange);
+          }}
+          options={popoverOptions}
+        />
+      );
+    }}
+  </UseState>
+));

+ 169 - 0
packages/grafana-ui/src/components/TimePicker/TimePickerPopover.tsx

@@ -0,0 +1,169 @@
+import React, { Component, SyntheticEvent } from 'react';
+import { TimeRange, TimeOptions, TimeOption } from '@grafana/ui';
+import { Moment } from 'moment';
+
+import { TimePickerCalendar } from './TimePickerCalendar';
+import { TimePickerInput } from './TimePickerInput';
+import { mapTimeOptionToTimeRange } from './time';
+import { Timezone } from '../../../../../public/app/core/utils/datemath';
+
+export interface Props {
+  value: TimeRange;
+  options: TimeOptions;
+  isTimezoneUtc: boolean;
+  timezone?: Timezone;
+  onChange?: (timeRange: TimeRange) => void;
+}
+
+export interface State {
+  value: TimeRange;
+  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 };
+  }
+
+  onFromInputChanged = (value: string, valid: boolean) => {
+    this.setState({
+      value: { ...this.state.value, raw: { ...this.state.value.raw, from: value } },
+      isFromInputValid: valid,
+    });
+  };
+
+  onToInputChanged = (value: string, valid: boolean) => {
+    this.setState({
+      value: { ...this.state.value, raw: { ...this.state.value.raw, to: value } },
+      isToInputValid: valid,
+    });
+  };
+
+  onFromCalendarChanged = (value: Moment) => {
+    this.setState({
+      value: { ...this.state.value, raw: { ...this.state.value.raw, from: value } },
+    });
+  };
+
+  onToCalendarChanged = (value: Moment) => {
+    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));
+    }
+  };
+
+  onApplyClick = () => {
+    const { onChange } = this.props;
+    if (onChange) {
+      onChange(this.state.value);
+    }
+  };
+
+  render() {
+    const { options, isTimezoneUtc, timezone } = this.props;
+    const { isFromInputValid, isToInputValid, value } = 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>
+                <TimePickerInput
+                  isTimezoneUtc={isTimezoneUtc}
+                  roundup={false}
+                  timezone={timezone}
+                  value={value.raw.from}
+                  onChange={this.onFromInputChanged}
+                />
+              </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>
+                <TimePickerInput
+                  isTimezoneUtc={isTimezoneUtc}
+                  roundup={true}
+                  timezone={timezone}
+                  value={value.raw.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}
+                />
+              </div>
+            </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>
+    );
+  }
+}

+ 189 - 0
packages/grafana-ui/src/components/TimePicker/_TimePicker.scss

@@ -0,0 +1,189 @@
+.time-picker {
+  display: flex;
+  flex-flow: column nowrap;
+
+  .time-picker-buttons {
+    display: flex;
+  }
+}
+.time-picker-popover-popper {
+  z-index: $zindex-timepicker-popover;
+}
+
+.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;
+  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;
+    }
+  }
+
+  .time-picker-popover-box-title {
+    font-size: $font-size-lg;
+    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-box-body-custom-ranges-input {
+    display: flex;
+    flex-flow: row nowrap;
+    align-items: center;
+    margin: $spacer 0;
+
+    .our-custom-wrapper-class {
+      margin-left: $spacer;
+      width: 100%;
+
+      .time-picker-input-error {
+        box-shadow: inset 0 0px 5px $red;
+      }
+    }
+  }
+
+  .time-picker-popover-box-footer {
+    display: flex;
+    flex-flow: row nowrap;
+    justify-content: flex-end;
+    margin-top: $spacer;
+  }
+}
+
+.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;
+    border: 0;
+  }
+
+  .react-calendar__month-view__weekdays {
+    background-color: $popover-border-color;
+    text-align: center;
+
+    abbr {
+      border: 0;
+      text-decoration: none;
+      cursor: default;
+      color: $popover-color;
+      font-weight: $font-weight-semi-bold;
+    }
+  }
+
+  .time-picker-calendar-tile {
+    color: $input-color;
+    background-color: $input-bg;
+    border: 0;
+    line-height: 22px;
+  }
+
+  button.time-picker-calendar-tile:hover {
+    font-weight: $font-weight-semi-bold;
+  }
+
+  .react-calendar__navigation__label,
+  .react-calendar__navigation > button:focus,
+  .time-picker-calendar-tile:focus {
+    outline: 0;
+  }
+
+  .react-calendar__tile--now {
+    color: $orange;
+  }
+
+  .react-calendar__tile--active {
+    color: $blue;
+    font-weight: $font-weight-semi-bold;
+  }
+}
+
+@media only screen and (max-width: 1116px) {
+  .time-picker-popover {
+    margin-left: $spacer;
+    display: flex;
+    flex-flow: column nowrap;
+
+    .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-box:first-child {
+      border-right: none;
+      border-bottom: 1px ridge;
+    }
+
+    .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-box-footer {
+      display: flex;
+      flex-flow: row nowrap;
+      justify-content: flex-end;
+      margin-top: $spacer;
+    }
+  }
+
+  .time-picker-calendar {
+    max-width: 500px;
+    width: 100%;
+  }
+}
+
+@media only screen and (max-width: 746px) {
+  .time-picker-popover {
+    margin-top: 48px;
+  }
+}

+ 44 - 0
packages/grafana-ui/src/components/TimePicker/time.ts

@@ -0,0 +1,44 @@
+import moment, { Moment } from 'moment';
+import { TimeOption, TimeRange, TIME_FORMAT } from '@grafana/ui';
+
+import * as dateMath from '../../../../../public/app/core/utils/datemath';
+import { describeTimeRange } from '../../../../../public/app/core/utils/rangeutil';
+
+export const mapTimeOptionToTimeRange = (
+  timeOption: TimeOption,
+  isTimezoneUtc: boolean,
+  timezone?: dateMath.Timezone
+): TimeRange => {
+  const fromMoment = stringToMoment(timeOption.from, isTimezoneUtc, false, timezone);
+  const toMoment = stringToMoment(timeOption.to, isTimezoneUtc, true, timezone);
+
+  return { from: fromMoment, to: toMoment, raw: { from: timeOption.from, to: timeOption.to } };
+};
+
+export const stringToMoment = (
+  value: string,
+  isTimezoneUtc: boolean,
+  roundUp?: boolean,
+  timezone?: dateMath.Timezone
+): Moment => {
+  if (value.indexOf('now') !== -1) {
+    if (!dateMath.isValid(value)) {
+      return moment();
+    }
+
+    const parsed = dateMath.parse(value, roundUp, timezone);
+    return parsed || moment();
+  }
+
+  if (isTimezoneUtc) {
+    return moment.utc(value, TIME_FORMAT);
+  }
+
+  return moment(value, TIME_FORMAT);
+};
+
+export const mapTimeRangeToRangeString = (timeRange: TimeRange): string => {
+  return describeTimeRange(timeRange.raw);
+};
+
+export const isValidTimeString = (text: string) => dateMath.isValid(text);

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

@@ -13,11 +13,18 @@ export const Tooltip = ({ children, theme, ...controllerProps }: TooltipProps) =
   return (
     <PopperController {...controllerProps}>
       {(showPopper, hidePopper, popperProps) => {
+        {
+          /* Override internal 'show' state if passed in as prop */
+        }
+        const payloadProps = {
+          ...popperProps,
+          show: controllerProps.show !== undefined ? controllerProps.show : popperProps.show,
+        };
         return (
           <>
             {tooltipTriggerRef.current && (
               <Popper
-                {...popperProps}
+                {...payloadProps}
                 onMouseEnter={showPopper}
                 onMouseLeave={hidePopper}
                 referenceElement={tooltipTriggerRef.current}

+ 2 - 0
packages/grafana-ui/src/components/index.scss

@@ -12,3 +12,5 @@
 @import 'EmptySearchResult/EmptySearchResult';
 @import 'FormField/FormField';
 @import 'BarGauge/BarGauge';
+@import 'RefreshPicker/RefreshPicker';
+@import 'TimePicker/TimePicker';

+ 6 - 1
packages/grafana-ui/src/components/index.ts

@@ -12,6 +12,7 @@ export { Select, AsyncSelect, SelectOptionItem } from './Select/Select';
 export { IndicatorsContainer } from './Select/IndicatorsContainer';
 export { NoOptionsMessage } from './Select/NoOptionsMessage';
 export { default as resetSelectStyles } from './Select/resetSelectStyles';
+export { ButtonSelect } from './Select/ButtonSelect';
 
 // Forms
 export { FormLabel } from './FormLabel/FormLabel';
@@ -31,6 +32,10 @@ export { PieChart, PieChartType } from './PieChart/PieChart';
 export { UnitPicker } from './UnitPicker/UnitPicker';
 export { StatsPicker } from './StatsPicker/StatsPicker';
 export { Input, InputStatus } from './Input/Input';
+export { RefreshPicker } from './RefreshPicker/RefreshPicker';
+
+// Renderless
+export { SetInterval } from './SetInterval/SetInterval';
 
 export { Table } from './Table/Table';
 export { TableInputCSV } from './Table/TableInputCSV';
@@ -41,6 +46,6 @@ export { Gauge } from './Gauge/Gauge';
 export { Graph } from './Graph/Graph';
 export { BarGauge } from './BarGauge/BarGauge';
 export { VizRepeater } from './VizRepeater/VizRepeater';
+export { ClickOutsideWrapper } from './ClickOutsideWrapper/ClickOutsideWrapper';
 export * from './SingleStatShared/shared';
-
 export { CallToActionCard } from './CallToActionCard/CallToActionCard';

+ 1 - 0
packages/grafana-ui/src/themes/_variables.scss.tmpl.ts

@@ -171,6 +171,7 @@ $zindex-tooltip: ${theme.zIndex.tooltip};
 $zindex-modal-backdrop: ${theme.zIndex.modalBackdrop};
 $zindex-modal: ${theme.zIndex.modal};
 $zindex-typeahead: ${theme.zIndex.typeahead};
+$zindex-timepicker-popover: 1070;
 
 // Buttons
 //

+ 16 - 0
packages/grafana-ui/src/types/time.ts

@@ -15,3 +15,19 @@ export interface IntervalValues {
   interval: string; // 10s,5m
   intervalMs: number;
 }
+
+export interface TimeOption {
+  from: string;
+  to: string;
+  display: string;
+  section: number;
+  active: boolean;
+}
+
+export interface TimeOptions {
+  [key: string]: TimeOption[];
+}
+
+export type TimeFragment = string | Moment;
+
+export const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss';

+ 1 - 0
packages/grafana-ui/src/utils/index.ts

@@ -12,3 +12,4 @@ export * from './logs';
 export * from './labels';
 export { getMappedValue } from './valueMappings';
 export * from './validate';
+export * from './object';

+ 8 - 0
packages/grafana-ui/src/utils/object.ts

@@ -0,0 +1,8 @@
+export const objRemoveUndefined = (obj: any) => {
+  return Object.keys(obj).reduce((acc: any, key) => {
+    if (obj[key] !== undefined) {
+      acc[key] = obj[key];
+    }
+    return acc;
+  }, {});
+};

+ 20 - 0
packages/grafana-ui/src/utils/storybook/withRightAlignedStory.tsx

@@ -0,0 +1,20 @@
+import React from 'react';
+import { RenderFunction } from '@storybook/react';
+
+const RightAlignedStory: React.FunctionComponent<{}> = ({ children }) => {
+  return (
+    <div
+      style={{
+        height: '100vh  ',
+        display: 'flex',
+        alignItems: 'flex-start',
+        justifyContent: 'flex-end',
+        marginRight: '20px',
+      }}
+    >
+      {children}
+    </div>
+  );
+};
+
+export const withRighAlignedStory = (story: RenderFunction) => <RightAlignedStory>{story()}</RightAlignedStory>;

+ 39 - 1
packages/grafana-ui/src/utils/string.test.ts

@@ -1,4 +1,4 @@
-import { stringToJsRegex } from '@grafana/ui';
+import { stringToJsRegex, stringToMs } from '@grafana/ui';
 
 describe('stringToJsRegex', () => {
   it('should parse the valid regex value', () => {
@@ -13,3 +13,41 @@ describe('stringToJsRegex', () => {
     }).toThrow();
   });
 });
+
+describe('stringToMs', () => {
+  it('should return zero if no input', () => {
+    const output = stringToMs('');
+    expect(output).toBe(0);
+  });
+
+  it('should return its input, as int, if no unit is supplied', () => {
+    const output = stringToMs('1000');
+    expect(output).toBe(1000);
+  });
+
+  it('should convert 3s to 3000', () => {
+    const output = stringToMs('3s');
+    expect(output).toBe(3000);
+  });
+
+  it('should convert 2m to 120000', () => {
+    const output = stringToMs('2m');
+    expect(output).toBe(120000);
+  });
+
+  it('should convert 2h to 7200000', () => {
+    const output = stringToMs('2h');
+    expect(output).toBe(7200000);
+  });
+
+  it('should convert 2d to 172800000', () => {
+    const output = stringToMs('2d');
+    expect(output).toBe(172800000);
+  });
+
+  it('should throw on unsupported unit', () => {
+    expect(() => {
+      stringToMs('1y');
+    }).toThrow();
+  });
+});

+ 38 - 0
packages/grafana-ui/src/utils/string.ts

@@ -1,3 +1,5 @@
+import { SelectOptionItem } from './../components/Select/Select';
+
 export function stringToJsRegex(str: string): RegExp {
   if (str[0] !== '/') {
     return new RegExp('^' + str + '$');
@@ -11,3 +13,39 @@ export function stringToJsRegex(str: string): RegExp {
 
   return new RegExp(match[1], match[2]);
 }
+
+export function stringToMs(str: string): number {
+  if (!str) {
+    return 0;
+  }
+
+  const nr = parseInt(str, 10);
+  const unit = str.substr(String(nr).length);
+  const s = 1000;
+  const m = s * 60;
+  const h = m * 60;
+  const d = h * 24;
+
+  switch (unit) {
+    case 's':
+      return nr * s;
+    case 'm':
+      return nr * m;
+    case 'h':
+      return nr * h;
+    case 'd':
+      return nr * d;
+    default:
+      if (!unit) {
+        return isNaN(nr) ? 0 : nr;
+      }
+      throw new Error('Not supported unit: ' + unit);
+  }
+}
+
+export function getIntervalFromString(strInterval: string): SelectOptionItem {
+  return {
+    label: strInterval,
+    value: stringToMs(strInterval),
+  };
+}

+ 65 - 63
public/app/core/components/Select/__snapshots__/TeamPicker.test.tsx.snap

@@ -4,86 +4,88 @@ exports[`TeamPicker renders correctly 1`] = `
 <div
   className="user-picker"
 >
-  <div
-    className="css-0 gf-form-input gf-form-input--form-dropdown"
-    onKeyDown={[Function]}
-  >
+  <div>
     <div
-      className="css-0 gf-form-select-box__control"
-      onMouseDown={[Function]}
-      onTouchEnd={[Function]}
+      className="css-0 gf-form-input gf-form-input--form-dropdown"
+      onKeyDown={[Function]}
     >
       <div
-        className="css-0 gf-form-select-box__value-container"
+        className="css-0 gf-form-select-box__control"
+        onMouseDown={[Function]}
+        onTouchEnd={[Function]}
       >
         <div
-          className="css-0 gf-form-select-box__placeholder"
-        >
-          Select a team
-        </div>
-        <div
-          className="css-0"
+          className="css-0 gf-form-select-box__value-container"
         >
           <div
-            className="gf-form-select-box__input"
-            style={
-              Object {
-                "display": "inline-block",
-              }
-            }
+            className="css-0 gf-form-select-box__placeholder"
+          >
+            Select a team
+          </div>
+          <div
+            className="css-0"
           >
-            <input
-              aria-autocomplete="list"
-              autoCapitalize="none"
-              autoComplete="off"
-              autoCorrect="off"
-              disabled={false}
-              id="react-select-2-input"
-              onBlur={[Function]}
-              onChange={[Function]}
-              onFocus={[Function]}
-              spellCheck="false"
-              style={
-                Object {
-                  "background": 0,
-                  "border": 0,
-                  "boxSizing": "content-box",
-                  "color": "inherit",
-                  "fontSize": "inherit",
-                  "opacity": 1,
-                  "outline": 0,
-                  "padding": 0,
-                  "width": "1px",
-                }
-              }
-              tabIndex="0"
-              type="text"
-              value=""
-            />
             <div
+              className="gf-form-select-box__input"
               style={
                 Object {
-                  "height": 0,
-                  "left": 0,
-                  "overflow": "scroll",
-                  "position": "absolute",
-                  "top": 0,
-                  "visibility": "hidden",
-                  "whiteSpace": "pre",
+                  "display": "inline-block",
                 }
               }
             >
-              
+              <input
+                aria-autocomplete="list"
+                autoCapitalize="none"
+                autoComplete="off"
+                autoCorrect="off"
+                disabled={false}
+                id="react-select-2-input"
+                onBlur={[Function]}
+                onChange={[Function]}
+                onFocus={[Function]}
+                spellCheck="false"
+                style={
+                  Object {
+                    "background": 0,
+                    "border": 0,
+                    "boxSizing": "content-box",
+                    "color": "inherit",
+                    "fontSize": "inherit",
+                    "opacity": 1,
+                    "outline": 0,
+                    "padding": 0,
+                    "width": "1px",
+                  }
+                }
+                tabIndex="0"
+                type="text"
+                value=""
+              />
+              <div
+                style={
+                  Object {
+                    "height": 0,
+                    "left": 0,
+                    "overflow": "scroll",
+                    "position": "absolute",
+                    "top": 0,
+                    "visibility": "hidden",
+                    "whiteSpace": "pre",
+                  }
+                }
+              >
+                
+              </div>
             </div>
           </div>
         </div>
-      </div>
-      <div
-        className="css-0 gf-form-select-box__indicators"
-      >
-        <span
-          className="gf-form-select-box__select-arrow "
-        />
+        <div
+          className="css-0 gf-form-select-box__indicators"
+        >
+          <span
+            className="gf-form-select-box__select-arrow "
+          />
+        </div>
       </div>
     </div>
   </div>

+ 65 - 63
public/app/core/components/Select/__snapshots__/UserPicker.test.tsx.snap

@@ -4,86 +4,88 @@ exports[`UserPicker renders correctly 1`] = `
 <div
   className="user-picker"
 >
-  <div
-    className="css-0 gf-form-input gf-form-input--form-dropdown"
-    onKeyDown={[Function]}
-  >
+  <div>
     <div
-      className="css-0 gf-form-select-box__control"
-      onMouseDown={[Function]}
-      onTouchEnd={[Function]}
+      className="css-0 gf-form-input gf-form-input--form-dropdown"
+      onKeyDown={[Function]}
     >
       <div
-        className="css-0 gf-form-select-box__value-container"
+        className="css-0 gf-form-select-box__control"
+        onMouseDown={[Function]}
+        onTouchEnd={[Function]}
       >
         <div
-          className="css-0 gf-form-select-box__placeholder"
-        >
-          Select user
-        </div>
-        <div
-          className="css-0"
+          className="css-0 gf-form-select-box__value-container"
         >
           <div
-            className="gf-form-select-box__input"
-            style={
-              Object {
-                "display": "inline-block",
-              }
-            }
+            className="css-0 gf-form-select-box__placeholder"
+          >
+            Select user
+          </div>
+          <div
+            className="css-0"
           >
-            <input
-              aria-autocomplete="list"
-              autoCapitalize="none"
-              autoComplete="off"
-              autoCorrect="off"
-              disabled={false}
-              id="react-select-2-input"
-              onBlur={[Function]}
-              onChange={[Function]}
-              onFocus={[Function]}
-              spellCheck="false"
-              style={
-                Object {
-                  "background": 0,
-                  "border": 0,
-                  "boxSizing": "content-box",
-                  "color": "inherit",
-                  "fontSize": "inherit",
-                  "opacity": 1,
-                  "outline": 0,
-                  "padding": 0,
-                  "width": "1px",
-                }
-              }
-              tabIndex="0"
-              type="text"
-              value=""
-            />
             <div
+              className="gf-form-select-box__input"
               style={
                 Object {
-                  "height": 0,
-                  "left": 0,
-                  "overflow": "scroll",
-                  "position": "absolute",
-                  "top": 0,
-                  "visibility": "hidden",
-                  "whiteSpace": "pre",
+                  "display": "inline-block",
                 }
               }
             >
-              
+              <input
+                aria-autocomplete="list"
+                autoCapitalize="none"
+                autoComplete="off"
+                autoCorrect="off"
+                disabled={false}
+                id="react-select-2-input"
+                onBlur={[Function]}
+                onChange={[Function]}
+                onFocus={[Function]}
+                spellCheck="false"
+                style={
+                  Object {
+                    "background": 0,
+                    "border": 0,
+                    "boxSizing": "content-box",
+                    "color": "inherit",
+                    "fontSize": "inherit",
+                    "opacity": 1,
+                    "outline": 0,
+                    "padding": 0,
+                    "width": "1px",
+                  }
+                }
+                tabIndex="0"
+                type="text"
+                value=""
+              />
+              <div
+                style={
+                  Object {
+                    "height": 0,
+                    "left": 0,
+                    "overflow": "scroll",
+                    "position": "absolute",
+                    "top": 0,
+                    "visibility": "hidden",
+                    "whiteSpace": "pre",
+                  }
+                }
+              >
+                
+              </div>
             </div>
           </div>
         </div>
-      </div>
-      <div
-        className="css-0 gf-form-select-box__indicators"
-      >
-        <span
-          className="gf-form-select-box__select-arrow "
-        />
+        <div
+          className="css-0 gf-form-select-box__indicators"
+        >
+          <span
+            className="gf-form-select-box__select-arrow "
+          />
+        </div>
       </div>
     </div>
   </div>

+ 4 - 2
public/app/core/utils/datemath.ts

@@ -1,9 +1,10 @@
+// @ts-ignore
 import _ from 'lodash';
 import moment from 'moment';
 
 const units = ['y', 'M', 'w', 'd', 'h', 'm', 's'];
 
-type Timezone = 'utc';
+export type Timezone = 'utc';
 
 /**
  * Parses different types input to a moment instance. There is a specific formatting language that can be used
@@ -88,7 +89,8 @@ export function isValid(text: string | moment.Moment): boolean {
  * @param time
  * @param roundUp If true it will round the time to endOf time unit, otherwise to startOf time unit.
  */
-export function parseDateMath(mathString: string, time: moment.Moment, roundUp?: boolean): moment.Moment | undefined {
+// TODO: Had to revert Andrejs `time: moment.Moment` to `time: any`
+export function parseDateMath(mathString: string, time: any, roundUp?: boolean): moment.Moment | undefined {
   const dateTime = time;
   let i = 0;
   const len = mathString.length;

+ 1 - 1
public/app/core/utils/explore.test.ts

@@ -46,7 +46,7 @@ describe('state functions', () => {
     });
 
     it('returns a valid Explore state from a compact URL parameter', () => {
-      const paramValue = '%5B"now-1h","now","Local",%7B"expr":"metric"%7D%5D';
+      const paramValue = '%5B"now-1h","now","Local","5m",%7B"expr":"metric"%7D,"ui"%5D';
       expect(parseUrlState(paramValue)).toMatchObject({
         datasource: 'Local',
         queries: [{ expr: 'metric' }],

+ 7 - 2
public/app/core/utils/explore.ts

@@ -59,7 +59,7 @@ export async function getExploreUrl(
 ) {
   let exploreDatasource = panelDatasource;
   let exploreTargets: DataQuery[] = panelTargets;
-  let url;
+  let url: string;
 
   // Mixed datasources need to choose only one datasource
   if (panelDatasource.meta.id === 'mixed' && panelTargets) {
@@ -191,7 +191,12 @@ export const safeParseJson = (text: string) => {
 
 export function parseUrlState(initial: string | undefined): ExploreUrlState {
   const parsed = safeParseJson(initial);
-  const errorResult = { datasource: null, queries: [], range: DEFAULT_RANGE, ui: DEFAULT_UI_STATE };
+  const errorResult = {
+    datasource: null,
+    queries: [],
+    range: DEFAULT_RANGE,
+    ui: DEFAULT_UI_STATE,
+  };
 
   if (!parsed) {
     return errorResult;

+ 8 - 7
public/app/core/utils/rangeutil.ts

@@ -1,3 +1,4 @@
+// @ts-ignore
 import _ from 'lodash';
 import moment from 'moment';
 
@@ -5,7 +6,7 @@ import { RawTimeRange } from '@grafana/ui';
 
 import * as dateMath from './datemath';
 
-const spans = {
+const spans: { [key: string]: { display: string; section?: number } } = {
   s: { display: 'second' },
   m: { display: 'minute' },
   h: { display: 'hour' },
@@ -63,12 +64,12 @@ const rangeOptions = [
 
 const absoluteFormat = 'MMM D, YYYY HH:mm:ss';
 
-const rangeIndex = {};
-_.each(rangeOptions, frame => {
+const rangeIndex: any = {};
+_.each(rangeOptions, (frame: any) => {
   rangeIndex[frame.from + ' to ' + frame.to] = frame;
 });
 
-export function getRelativeTimesList(timepickerSettings, currentDisplay) {
+export function getRelativeTimesList(timepickerSettings: any, currentDisplay: any) {
   const groups = _.groupBy(rangeOptions, (option: any) => {
     option.active = option.display === currentDisplay;
     return option.section;
@@ -84,7 +85,7 @@ export function getRelativeTimesList(timepickerSettings, currentDisplay) {
   return groups;
 }
 
-function formatDate(date) {
+function formatDate(date: any) {
   return date.format(absoluteFormat);
 }
 
@@ -144,12 +145,12 @@ export function describeTimeRange(range: RawTimeRange): string {
 
   if (moment.isMoment(range.from)) {
     const toMoment = dateMath.parse(range.to, true);
-    return formatDate(range.from) + ' to ' + toMoment.fromNow();
+    return toMoment ? formatDate(range.from) + ' to ' + toMoment.fromNow() : '';
   }
 
   if (moment.isMoment(range.to)) {
     const from = dateMath.parse(range.from, false);
-    return from.fromNow() + ' to ' + formatDate(range.to);
+    return from ? from.fromNow() + ' to ' + formatDate(range.to) : '';
   }
 
   if (range.to.toString() === 'now') {

+ 19 - 7
public/app/features/dashboard/components/DashNav/DashNav.tsx

@@ -9,6 +9,7 @@ import { PlaylistSrv } from 'app/features/playlist/playlist_srv';
 
 // Components
 import { DashNavButton } from './DashNavButton';
+import { DashNavTimeControls } from './DashNavTimeControls';
 import { Tooltip } from '@grafana/ui';
 
 // State
@@ -16,8 +17,9 @@ import { updateLocation } from 'app/core/actions';
 
 // Types
 import { DashboardModel } from '../../state';
+import { StoreState } from 'app/types';
 
-export interface Props {
+export interface OwnProps {
   dashboard: DashboardModel;
   editview: string;
   isEditing: boolean;
@@ -27,6 +29,12 @@ export interface Props {
   onAddPanel: () => void;
 }
 
+export interface StateProps {
+  location: any;
+}
+
+type Props = StateProps & OwnProps;
+
 export class DashNav extends PureComponent<Props> {
   timePickerEl: HTMLElement;
   timepickerCmp: AngularComponent;
@@ -39,7 +47,6 @@ export class DashNav extends PureComponent<Props> {
 
   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 };
@@ -161,12 +168,10 @@ export class DashNav extends PureComponent<Props> {
   }
 
   render() {
-    const { dashboard, onAddPanel } = this.props;
+    const { dashboard, onAddPanel, location } = this.props;
     const { canStar, canSave, canShare, showSettings, isStarred } = dashboard.meta;
     const { snapshot } = dashboard;
-
     const snapshotUrl = snapshot && snapshot.originalUrl;
-
     return (
       <div className="navbar">
         {this.isInFullscreenOrSettings && this.renderBackButton()}
@@ -255,13 +260,20 @@ export class DashNav extends PureComponent<Props> {
           />
         </div>
 
-        <div className="gf-timepicker-nav" ref={element => (this.timePickerEl = element)} />
+        {!dashboard.timepicker.hidden && (
+          <div className="navbar-buttons">
+            <DashNavTimeControls dashboard={dashboard} location={location} updateLocation={updateLocation} />
+            <div className="gf-timepicker-nav" ref={element => (this.timePickerEl = element)} />
+          </div>
+        )}
       </div>
     );
   }
 }
 
-const mapStateToProps = () => ({});
+const mapStateToProps = (state: StoreState) => ({
+  location: state.location,
+});
 
 const mapDispatchToProps = {
   updateLocation,

+ 53 - 0
public/app/features/dashboard/components/DashNav/DashNavTimeControls.tsx

@@ -0,0 +1,53 @@
+// Libaries
+import React, { Component } from 'react';
+
+// Types
+import { DashboardModel } from '../../state';
+import { LocationState } from 'app/types';
+
+// State
+import { updateLocation } from 'app/core/actions';
+
+// Components
+import { RefreshPicker } from '@grafana/ui';
+
+// Utils & Services
+import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
+
+export interface Props {
+  dashboard: DashboardModel;
+  updateLocation: typeof updateLocation;
+  location: LocationState;
+}
+
+export class DashNavTimeControls extends Component<Props> {
+  timeSrv: TimeSrv = getTimeSrv();
+
+  get refreshParamInUrl(): string {
+    return this.props.location.query.refresh as string;
+  }
+
+  onChangeRefreshInterval = (interval: string) => {
+    this.timeSrv.setAutoRefresh(interval);
+    this.forceUpdate();
+  };
+
+  onRefresh = () => {
+    this.timeSrv.refreshDashboard();
+    return Promise.resolve();
+  };
+
+  render() {
+    const { dashboard } = this.props;
+    const intervals = dashboard.timepicker.refresh_intervals;
+    return (
+      <RefreshPicker
+        onIntervalChanged={this.onChangeRefreshInterval}
+        onRefresh={this.onRefresh}
+        value={dashboard.refresh}
+        intervals={intervals}
+        tooltip="Refresh dashboard"
+      />
+    );
+  }
+}

+ 1 - 1
public/app/features/dashboard/components/TimePicker/TimePickerCtrl.ts

@@ -108,7 +108,7 @@ export class TimePickerCtrl {
     this.timeOptions = rangeUtil.getRelativeTimesList(this.panel, this.rangeString);
     this.refresh = {
       value: this.dashboard.refresh,
-      options: _.map(this.panel.refresh_intervals, (interval: any) => {
+      options: this.panel.refresh_intervals.map((interval: any) => {
         return { text: interval, value: interval };
       }),
     };

+ 20 - 22
public/app/features/dashboard/components/TimePicker/template.html

@@ -1,27 +1,25 @@
-<div class="navbar-buttons">
-  <button class="btn navbar-button navbar-button--tight" ng-click='ctrl.move(-1)' ng-if="ctrl.isAbsolute">
-		<i class="fa fa-chevron-left"></i>
-	</button>
+<button class="btn navbar-button navbar-button--tight" ng-click='ctrl.move(-1)' ng-if="ctrl.isAbsolute">
+  <i class="fa fa-chevron-left"></i>
+</button>
 
-	<button bs-tooltip="ctrl.tooltip" data-placement="bottom" ng-click="ctrl.openDropdown()" class="btn navbar-button gf-timepicker-nav-btn">
-		<i class="fa fa-clock-o"></i>
-		<span ng-bind="ctrl.rangeString"></span>
-		<span ng-show="ctrl.isUtc" class="gf-timepicker-utc">UTC</span>
-		<span ng-show="ctrl.dashboard.refresh" class="text-warning">&nbsp; Refresh every {{ctrl.dashboard.refresh}}</span>
-	</button>
+<button bs-tooltip="ctrl.tooltip" data-placement="bottom" ng-click="ctrl.openDropdown()" class="btn navbar-button gf-timepicker-nav-btn">
+  <i class="fa fa-clock-o"></i>
+  <span ng-bind="ctrl.rangeString"></span>
+  <span ng-show="ctrl.isUtc" class="gf-timepicker-utc">UTC</span>
+  <!-- <span ng-show="ctrl.dashboard.refresh" class="text-warning">&nbsp; Refresh every {{ctrl.dashboard.refresh}}</span> -->
+</button>
 
-  <button class="btn navbar-button navbar-button--tight" ng-click='ctrl.move(1)' ng-if="ctrl.isAbsolute">
-		<i class="fa fa-chevron-right"></i>
-	</button>
+<button class="btn navbar-button navbar-button--tight" ng-click='ctrl.move(1)' ng-if="ctrl.isAbsolute">
+  <i class="fa fa-chevron-right"></i>
+</button>
 
-  <button class="btn navbar-button navbar-button--zoom" bs-tooltip="'Time range zoom out <br> CTRL+Z'" data-placement="bottom" ng-click='ctrl.zoom(2)'>
-		<i class="fa fa-search-minus"></i>
-	</button>
+<button class="btn navbar-button navbar-button--zoom" bs-tooltip="'Time range zoom out <br> CTRL+Z'" data-placement="bottom" ng-click='ctrl.zoom(2)'>
+  <i class="fa fa-search-minus"></i>
+</button>
 
-  <button class="btn navbar-button navbar-button--refresh" ng-click="ctrl.timeSrv.refreshDashboard()">
-		<i class="fa fa-refresh"></i>
-	</button>
-</div>
+<!-- <button class="btn navbar-button navbar-button--refresh" ng-click="ctrl.timeSrv.refreshDashboard()">
+  <i class="fa fa-refresh"></i>
+</button> -->
 
 <div ng-if="ctrl.isOpen" class="gf-timepicker-dropdown">
   <div class="popover-box">
@@ -75,7 +73,7 @@
         <datepicker ng-model="ctrl.absolute.toJs" class="gf-timepicker-component" show-weeks="false" starting-day="ctrl.firstDayOfWeek" ng-change="ctrl.absoluteToChanged()"></datepicker>
       </div>
 
-      <label class="small">Refreshing every:</label>
+      <!-- <label class="small">Refreshing every:</label>
       <div class="gf-form-inline">
         <div class="gf-form max-width-28">
           <select ng-model="ctrl.refresh.value" class="gf-form-input input-medium" ng-options="f.value as f.text for f in ctrl.refresh.options"></select>
@@ -83,7 +81,7 @@
         <div class="gf-form">
           <button type="submit" class="btn gf-form-btn btn-primary" ng-click="ctrl.applyCustom();" ng-disabled="!timeForm.$valid">Apply</button>
         </div>
-      </div>
+      </div> -->
     </form>
   </div>
 </div>

+ 1 - 1
public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx

@@ -9,7 +9,7 @@ import templateSrv from 'app/features/templating/template_srv';
 
 import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
 import { PanelModel } from 'app/features/dashboard/state/PanelModel';
-import { ClickOutsideWrapper } from 'app/core/components/ClickOutsideWrapper/ClickOutsideWrapper';
+import { ClickOutsideWrapper } from '@grafana/ui';
 
 export interface Props {
   panel: PanelModel;

+ 12 - 9
public/app/features/dashboard/services/TimeSrv.ts

@@ -124,6 +124,7 @@ export class TimeSrv {
   setAutoRefresh(interval) {
     this.dashboard.refresh = interval;
     this.cancelNextRefresh();
+
     if (interval) {
       const intervalMs = kbn.interval_to_ms(interval);
 
@@ -135,15 +136,17 @@ export class TimeSrv {
       );
     }
 
-    // update url
-    const params = this.$location.search();
-    if (interval) {
-      params.refresh = interval;
-      this.$location.search(params);
-    } else if (params.refresh) {
-      delete params.refresh;
-      this.$location.search(params);
-    }
+    // update url inside timeout to so that a digest happens after (called from react)
+    this.$timeout(() => {
+      const params = this.$location.search();
+      if (interval) {
+        params.refresh = interval;
+        this.$location.search(params);
+      } else if (params.refresh) {
+        delete params.refresh;
+        this.$location.search(params);
+      }
+    });
   }
 
   refreshDashboard() {

+ 0 - 1
public/app/features/explore/Explore.tsx

@@ -117,7 +117,6 @@ export class Explore extends React.PureComponent<ExploreProps> {
     const initialQueries: DataQuery[] = ensureQueries(queries);
     const initialRange = { from: parseTime(range.from), to: parseTime(range.to) };
     const width = this.el ? this.el.offsetWidth : 0;
-
     // initialize the whole explore first time we mount and if browser history contains a change in datasource
     if (!initialized) {
       this.props.initializeExplore(

+ 47 - 11
public/app/features/explore/ExploreToolbar.tsx

@@ -3,12 +3,19 @@ import { connect } from 'react-redux';
 import { hot } from 'react-hot-loader';
 
 import { ExploreId } from 'app/types/explore';
-import { DataSourceSelectItem, RawTimeRange, TimeRange } from '@grafana/ui';
+import { DataSourceSelectItem, RawTimeRange, TimeRange, ClickOutsideWrapper } from '@grafana/ui';
 import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
 import { StoreState } from 'app/types/store';
-import { changeDatasource, clearQueries, splitClose, runQueries, splitOpen } from './state/actions';
+import {
+  changeDatasource,
+  clearQueries,
+  splitClose,
+  runQueries,
+  splitOpen,
+  changeRefreshInterval,
+} from './state/actions';
 import TimePicker from './TimePicker';
-import { ClickOutsideWrapper } from 'app/core/components/ClickOutsideWrapper/ClickOutsideWrapper';
+import { RefreshPicker, SetInterval } from '@grafana/ui';
 
 enum IconSide {
   left = 'left',
@@ -51,20 +58,22 @@ interface StateProps {
   range: RawTimeRange;
   selectedDatasource: DataSourceSelectItem;
   splitted: boolean;
+  refreshInterval: string;
 }
 
 interface DispatchProps {
   changeDatasource: typeof changeDatasource;
   clearAll: typeof clearQueries;
-  runQuery: typeof runQueries;
+  runQueries: typeof runQueries;
   closeSplit: typeof splitClose;
   split: typeof splitOpen;
+  changeRefreshInterval: typeof changeRefreshInterval;
 }
 
 type Props = StateProps & DispatchProps & OwnProps;
 
 export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
-  constructor(props) {
+  constructor(props: Props) {
     super(props);
   }
 
@@ -77,23 +86,32 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
   };
 
   onRunQuery = () => {
-    this.props.runQuery(this.props.exploreId);
+    return this.props.runQueries(this.props.exploreId);
   };
 
   onCloseTimePicker = () => {
     this.props.timepickerRef.current.setState({ isOpen: false });
   };
 
+  onChangeRefreshInterval = (item: string) => {
+    const { changeRefreshInterval, exploreId } = this.props;
+    changeRefreshInterval(exploreId, item);
+  };
+
   render() {
     const {
       datasourceMissing,
       exploreDatasources,
+      closeSplit,
       exploreId,
       loading,
       range,
       selectedDatasource,
       splitted,
       timepickerRef,
+      refreshInterval,
+      onChangeTime,
+      split,
     } = this.props;
 
     return (
@@ -109,7 +127,7 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
               )}
             </div>
             {splitted && (
-              <a className="explore-toolbar-header-close" onClick={() => this.props.closeSplit(exploreId)}>
+              <a className="explore-toolbar-header-close" onClick={() => closeSplit(exploreId)}>
                 <i className="fa fa-times fa-fw" />
               </a>
             )}
@@ -133,7 +151,7 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
                 {createResponsiveButton({
                   splitted,
                   title: 'Split',
-                  onClick: this.props.split,
+                  onClick: split,
                   iconClassName: 'fa fa-fw fa-columns icon-margin-right',
                   iconSide: IconSide.left,
                 })}
@@ -141,9 +159,18 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
             ) : null}
             <div className="explore-toolbar-content-item timepicker">
               <ClickOutsideWrapper onClick={this.onCloseTimePicker}>
-                <TimePicker ref={timepickerRef} range={range} onChangeTime={this.props.onChangeTime} />
+                <TimePicker ref={timepickerRef} range={range} onChangeTime={onChangeTime} />
               </ClickOutsideWrapper>
+
+              <RefreshPicker
+                onIntervalChanged={this.onChangeRefreshInterval}
+                onRefresh={this.onRunQuery}
+                value={refreshInterval}
+                tooltip="Refresh"
+              />
+              {refreshInterval && <SetInterval func={this.onRunQuery} interval={refreshInterval} />}
             </div>
+
             <div className="explore-toolbar-content-item">
               <button className="btn navbar-button navbar-button--no-icon" onClick={this.onClearAll}>
                 Clear All
@@ -169,7 +196,14 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
 const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps => {
   const splitted = state.explore.split;
   const exploreItem = state.explore[exploreId];
-  const { datasourceInstance, datasourceMissing, exploreDatasources, queryTransactions, range } = exploreItem;
+  const {
+    datasourceInstance,
+    datasourceMissing,
+    exploreDatasources,
+    queryTransactions,
+    range,
+    refreshInterval,
+  } = exploreItem;
   const selectedDatasource = datasourceInstance
     ? exploreDatasources.find(datasource => datasource.name === datasourceInstance.name)
     : undefined;
@@ -182,13 +216,15 @@ const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps
     range,
     selectedDatasource,
     splitted,
+    refreshInterval,
   };
 };
 
 const mapDispatchToProps: DispatchProps = {
   changeDatasource,
+  changeRefreshInterval,
   clearAll: clearQueries,
-  runQuery: runQueries,
+  runQueries,
   closeSplit: splitClose,
   split: splitOpen,
 };

+ 16 - 0
public/app/features/explore/state/actionTypes.ts

@@ -66,10 +66,19 @@ export interface ChangeTimePayload {
   range: TimeRange;
 }
 
+export interface ChangeRefreshIntervalPayload {
+  exploreId: ExploreId;
+  refreshInterval: string;
+}
+
 export interface ClearQueriesPayload {
   exploreId: ExploreId;
 }
 
+export interface ClearRefreshIntervalPayload {
+  exploreId: ExploreId;
+}
+
 export interface HighlightLogsExpressionPayload {
   exploreId: ExploreId;
   expressions: string[];
@@ -240,6 +249,13 @@ export const changeSizeAction = actionCreatorFactory<ChangeSizePayload>('explore
  */
 export const changeTimeAction = actionCreatorFactory<ChangeTimePayload>('explore/CHANGE_TIME').create();
 
+/**
+ * Change the time range of Explore. Usually called from the Timepicker or a graph interaction.
+ */
+export const changeRefreshIntervalAction = actionCreatorFactory<ChangeRefreshIntervalPayload>(
+  'explore/CHANGE_REFRESH_INTERVAL'
+).create();
+
 /**
  * Clear all queries and results.
  */

+ 10 - 1
public/app/features/explore/state/actions.test.ts

@@ -35,7 +35,12 @@ const setup = (updateOverides?: Partial<ExploreUpdateState>) => {
   const eventBridge = {} as Emitter;
   const ui = { dedupStrategy: LogsDedupStrategy.none, showingGraph: false, showingLogs: false, showingTable: false };
   const range = { from: 'now', to: 'now' };
-  const urlState: ExploreUrlState = { datasource: 'some-datasource', queries: [], range, ui };
+  const urlState: ExploreUrlState = {
+    datasource: 'some-datasource',
+    queries: [],
+    range,
+    ui,
+  };
   const updateDefaults = makeInitialUpdateState();
   const update = { ...updateDefaults, ...updateOverides };
   const initialState = {
@@ -50,6 +55,10 @@ const setup = (updateOverides?: Partial<ExploreUpdateState>) => {
         queries: [] as DataQuery[],
         range,
         ui,
+        refreshInterval: {
+          label: 'Off',
+          value: 0,
+        },
       },
     },
   };

+ 59 - 41
public/app/features/explore/state/actions.ts

@@ -22,6 +22,7 @@ import {
 import { updateLocation } from 'app/core/actions';
 
 // Types
+import { ResultGetter } from 'app/types/explore';
 import { ThunkResult } from 'app/types';
 import {
   RawTimeRange,
@@ -44,6 +45,8 @@ import {
 import {
   updateDatasourceInstanceAction,
   changeQueryAction,
+  changeRefreshIntervalAction,
+  ChangeRefreshIntervalPayload,
   changeSizeAction,
   ChangeSizePayload,
   changeTimeAction,
@@ -164,7 +167,7 @@ export function changeSize(
 }
 
 /**
- * Change the time range of Explore. Usually called from the Timepicker or a graph interaction.
+ * Change the time range of Explore. Usually called from the Time picker or a graph interaction.
  */
 export function changeTime(exploreId: ExploreId, range: TimeRange): ThunkResult<void> {
   return dispatch => {
@@ -173,6 +176,16 @@ export function changeTime(exploreId: ExploreId, range: TimeRange): ThunkResult<
   };
 }
 
+/**
+ * Change the refresh interval of Explore. Called from the Refresh picker.
+ */
+export function changeRefreshInterval(
+  exploreId: ExploreId,
+  refreshInterval: string
+): ActionOf<ChangeRefreshIntervalPayload> {
+  return changeRefreshIntervalAction({ exploreId, refreshInterval });
+}
+
 /**
  * Clear all queries and results.
  */
@@ -526,7 +539,7 @@ export function queryTransactionSuccess(
 /**
  * Main action to run queries and dispatches sub-actions based on which result viewers are active
  */
-export function runQueries(exploreId: ExploreId, ignoreUIState = false): ThunkResult<void> {
+export function runQueries(exploreId: ExploreId, ignoreUIState = false): ThunkResult<Promise<any>> {
   return (dispatch, getState) => {
     const {
       datasourceInstance,
@@ -543,13 +556,13 @@ export function runQueries(exploreId: ExploreId, ignoreUIState = false): ThunkRe
 
     if (datasourceError) {
       // let's not run any queries if data source is in a faulty state
-      return;
+      return Promise.resolve();
     }
 
     if (!hasNonEmptyQuery(queries)) {
       dispatch(clearQueriesAction({ exploreId }));
       dispatch(stateSave()); // Remember to saves to state and update location
-      return;
+      return Promise.resolve();
     }
 
     // Some datasource's query builders allow per-query interval limits,
@@ -558,41 +571,46 @@ export function runQueries(exploreId: ExploreId, ignoreUIState = false): ThunkRe
 
     dispatch(runQueriesAction({ exploreId }));
     // Keep table queries first since they need to return quickly
-    if ((ignoreUIState || showingTable) && supportsTable) {
-      dispatch(
-        runQueriesForType(
-          exploreId,
-          'Table',
-          {
-            interval,
-            format: 'table',
-            instant: true,
-            valueWithRefId: true,
-          },
-          (data: any) => data[0]
-        )
-      );
-    }
-    if ((ignoreUIState || showingGraph) && supportsGraph) {
-      dispatch(
-        runQueriesForType(
-          exploreId,
-          'Graph',
-          {
-            interval,
-            format: 'time_series',
-            instant: false,
-            maxDataPoints: containerWidth,
-          },
-          makeTimeSeriesList
-        )
-      );
-    }
-    if ((ignoreUIState || showingLogs) && supportsLogs) {
-      dispatch(runQueriesForType(exploreId, 'Logs', { interval, format: 'logs' }));
-    }
+    const tableQueriesPromise =
+      (ignoreUIState || showingTable) && supportsTable
+        ? dispatch(
+            runQueriesForType(
+              exploreId,
+              'Table',
+              {
+                interval,
+                format: 'table',
+                instant: true,
+                valueWithRefId: true,
+              },
+              (data: any[]) => data[0]
+            )
+          )
+        : undefined;
+    const typeQueriesPromise =
+      (ignoreUIState || showingGraph) && supportsGraph
+        ? dispatch(
+            runQueriesForType(
+              exploreId,
+              'Graph',
+              {
+                interval,
+                format: 'time_series',
+                instant: false,
+                maxDataPoints: containerWidth,
+              },
+              makeTimeSeriesList
+            )
+          )
+        : undefined;
+    const logsQueriesPromise =
+      (ignoreUIState || showingLogs) && supportsLogs
+        ? dispatch(runQueriesForType(exploreId, 'Logs', { interval, format: 'logs' }))
+        : undefined;
 
     dispatch(stateSave());
+
+    return Promise.all([tableQueriesPromise, typeQueriesPromise, logsQueriesPromise]);
   };
 }
 
@@ -607,14 +625,13 @@ function runQueriesForType(
   exploreId: ExploreId,
   resultType: ResultType,
   queryOptions: QueryOptions,
-  resultGetter?: any
+  resultGetter?: ResultGetter
 ): ThunkResult<void> {
   return async (dispatch, getState) => {
     const { datasourceInstance, eventBridge, queries, queryIntervals, range, scanning } = getState().explore[exploreId];
     const datasourceId = datasourceInstance.meta.id;
-
     // Run all queries concurrently
-    queries.forEach(async (query, rowIndex) => {
+    const queryPromises = queries.map(async (query, rowIndex) => {
       const transaction = buildQueryTransaction(
         query,
         rowIndex,
@@ -638,6 +655,8 @@ function runQueriesForType(
         dispatch(queryTransactionFailure(exploreId, transaction.id, response, datasourceId));
       }
     });
+
+    return Promise.all(queryPromises);
   };
 }
 
@@ -814,7 +833,6 @@ export function refreshExplore(exploreId: ExploreId): ThunkResult<void> {
     const { datasource, queries, range, ui } = urlState;
     const refreshQueries = queries.map(q => ({ ...q, ...generateEmptyQuery(itemState.queries) }));
     const refreshRange = { from: parseTime(range.from), to: parseTime(range.to) };
-
     // need to refresh datasource
     if (update.datasource) {
       const initialQueries = ensureQueries(queries);

+ 38 - 0
public/app/features/explore/state/reducers.test.ts

@@ -515,6 +515,44 @@ describe('Explore reducer', () => {
             });
           });
 
+          describe('and refreshInterval differs', () => {
+            it('then it should return update refreshInterval', () => {
+              const { initalState, serializedUrlState } = setup();
+              const expectedState = {
+                ...initalState,
+                left: {
+                  ...initalState.left,
+                  update: {
+                    ...initalState.left.update,
+                    refreshInterval: true,
+                  },
+                },
+              };
+              const stateWithDifferentDataSource = {
+                ...initalState,
+                left: {
+                  ...initalState.left,
+                  urlState: {
+                    ...initalState.left.urlState,
+                    refreshInterval: '5s',
+                  },
+                },
+              };
+
+              reducerTester()
+                .givenReducer(exploreReducer, stateWithDifferentDataSource)
+                .whenActionIsDispatched(
+                  updateLocation({
+                    query: {
+                      left: serializedUrlState,
+                    },
+                    path: '/explore',
+                  })
+                )
+                .thenStateShouldEqual(expectedState);
+            });
+          });
+
           describe('and nothing differs', () => {
             fit('then it should return update ui', () => {
               const { initalState, serializedUrlState } = setup();

+ 20 - 3
public/app/features/explore/state/reducers.ts

@@ -27,6 +27,7 @@ import {
   changeQueryAction,
   changeSizeAction,
   changeTimeAction,
+  changeRefreshIntervalAction,
   clearQueriesAction,
   highlightLogsExpressionAction,
   initializeExploreAction,
@@ -67,6 +68,7 @@ export const makeInitialUpdateState = (): ExploreUpdateState => ({
   range: false,
   ui: false,
 });
+
 /**
  * Returns a fresh Explore area state
  */
@@ -101,10 +103,11 @@ export const makeExploreItemState = (): ExploreItemState => ({
 /**
  * Global Explore state that handles multiple Explore areas and the split state
  */
+export const initialExploreItemState = makeExploreItemState();
 export const initialExploreState: ExploreState = {
   split: null,
-  left: makeExploreItemState(),
-  right: makeExploreItemState(),
+  left: initialExploreItemState,
+  right: initialExploreItemState,
 };
 
 /**
@@ -175,6 +178,16 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
       return { ...state, range: action.payload.range };
     },
   })
+  .addMapper({
+    filter: changeRefreshIntervalAction,
+    mapper: (state, action): ExploreItemState => {
+      const { refreshInterval } = action.payload;
+      return {
+        ...state,
+        refreshInterval: refreshInterval,
+      };
+    },
+  })
   .addMapper({
     filter: clearQueriesAction,
     mapper: (state): ExploreItemState => {
@@ -580,7 +593,11 @@ export const updateChildRefreshState = (
   const urlState = parseUrlState(queryState);
   if (!state.urlState || path !== '/explore') {
     // we only want to refresh when browser back/forward
-    return { ...state, urlState, update: { datasource: false, queries: false, range: false, ui: false } };
+    return {
+      ...state,
+      urlState,
+      update: { datasource: false, queries: false, range: false, ui: false },
+    };
   }
 
   const datasource = _.isEqual(urlState ? urlState.datasource : '', state.urlState.datasource) === false;

+ 20 - 0
public/app/features/teams/__snapshots__/TeamMemberRow.test.tsx.snap

@@ -84,12 +84,22 @@ exports[`Render when feature toggle editorsCanAdmin is turned off should not ren
           autoFocus={false}
           backspaceRemovesValue={true}
           className="gf-form-select-box__control--menu-right"
+          components={
+            Object {
+              "Group": [Function],
+              "IndicatorsContainer": [Function],
+              "MenuList": [Function],
+              "Option": [Function],
+              "SingleValue": [Function],
+            }
+          }
           isClearable={false}
           isDisabled={false}
           isLoading={false}
           isMulti={false}
           isSearchable={false}
           maxMenuHeight={300}
+          menuIsOpen={false}
           onChange={[Function]}
           openMenuOnFocus={false}
           options={
@@ -160,12 +170,22 @@ exports[`Render when feature toggle editorsCanAdmin is turned on should render p
           autoFocus={false}
           backspaceRemovesValue={true}
           className="gf-form-select-box__control--menu-right"
+          components={
+            Object {
+              "Group": [Function],
+              "IndicatorsContainer": [Function],
+              "MenuList": [Function],
+              "Option": [Function],
+              "SingleValue": [Function],
+            }
+          }
           isClearable={false}
           isDisabled={false}
           isLoading={false}
           isMulti={false}
           isSearchable={false}
           maxMenuHeight={300}
+          menuIsOpen={false}
           onChange={[Function]}
           openMenuOnFocus={false}
           options={

+ 65 - 63
public/app/plugins/datasource/stackdriver/components/__snapshots__/Aggregations.test.tsx.snap

@@ -13,86 +13,88 @@ Array [
       >
         Aggregation
       </label>
-      <div
-        className="css-0 gf-form-input gf-form-input--form-dropdown width-15"
-        onKeyDown={[Function]}
-      >
+      <div>
         <div
-          className="css-0 gf-form-select-box__control"
-          onMouseDown={[Function]}
-          onTouchEnd={[Function]}
+          className="css-0 gf-form-input gf-form-input--form-dropdown width-15"
+          onKeyDown={[Function]}
         >
           <div
-            className="css-0 gf-form-select-box__value-container"
+            className="css-0 gf-form-select-box__control"
+            onMouseDown={[Function]}
+            onTouchEnd={[Function]}
           >
             <div
-              className="css-0 gf-form-select-box__placeholder"
-            >
-              Select Reducer
-            </div>
-            <div
-              className="css-0"
+              className="css-0 gf-form-select-box__value-container"
             >
               <div
-                className="gf-form-select-box__input"
-                style={
-                  Object {
-                    "display": "inline-block",
-                  }
-                }
+                className="css-0 gf-form-select-box__placeholder"
+              >
+                Select Reducer
+              </div>
+              <div
+                className="css-0"
               >
-                <input
-                  aria-autocomplete="list"
-                  autoCapitalize="none"
-                  autoComplete="off"
-                  autoCorrect="off"
-                  disabled={false}
-                  id="react-select-2-input"
-                  onBlur={[Function]}
-                  onChange={[Function]}
-                  onFocus={[Function]}
-                  spellCheck="false"
-                  style={
-                    Object {
-                      "background": 0,
-                      "border": 0,
-                      "boxSizing": "content-box",
-                      "color": "inherit",
-                      "fontSize": "inherit",
-                      "opacity": 1,
-                      "outline": 0,
-                      "padding": 0,
-                      "width": "1px",
-                    }
-                  }
-                  tabIndex="0"
-                  type="text"
-                  value=""
-                />
                 <div
+                  className="gf-form-select-box__input"
                   style={
                     Object {
-                      "height": 0,
-                      "left": 0,
-                      "overflow": "scroll",
-                      "position": "absolute",
-                      "top": 0,
-                      "visibility": "hidden",
-                      "whiteSpace": "pre",
+                      "display": "inline-block",
                     }
                   }
                 >
-                  
+                  <input
+                    aria-autocomplete="list"
+                    autoCapitalize="none"
+                    autoComplete="off"
+                    autoCorrect="off"
+                    disabled={false}
+                    id="react-select-2-input"
+                    onBlur={[Function]}
+                    onChange={[Function]}
+                    onFocus={[Function]}
+                    spellCheck="false"
+                    style={
+                      Object {
+                        "background": 0,
+                        "border": 0,
+                        "boxSizing": "content-box",
+                        "color": "inherit",
+                        "fontSize": "inherit",
+                        "opacity": 1,
+                        "outline": 0,
+                        "padding": 0,
+                        "width": "1px",
+                      }
+                    }
+                    tabIndex="0"
+                    type="text"
+                    value=""
+                  />
+                  <div
+                    style={
+                      Object {
+                        "height": 0,
+                        "left": 0,
+                        "overflow": "scroll",
+                        "position": "absolute",
+                        "top": 0,
+                        "visibility": "hidden",
+                        "whiteSpace": "pre",
+                      }
+                    }
+                  >
+                    
+                  </div>
                 </div>
               </div>
             </div>
-          </div>
-          <div
-            className="css-0 gf-form-select-box__indicators"
-          >
-            <span
-              className="gf-form-select-box__select-arrow "
-            />
+            <div
+              className="css-0 gf-form-select-box__indicators"
+            >
+              <span
+                className="gf-form-select-box__select-arrow "
+              />
+            </div>
           </div>
         </div>
       </div>

+ 226 - 218
public/app/plugins/datasource/stackdriver/components/__snapshots__/QueryEditor.test.tsx.snap

@@ -13,41 +13,43 @@ Array [
       >
         Service
       </span>
-      <div
-        className="css-0 gf-form-input gf-form-input--form-dropdown width-15"
-        onKeyDown={[Function]}
-      >
+      <div>
         <div
-          className="css-0 gf-form-select-box__control"
-          onMouseDown={[Function]}
-          onTouchEnd={[Function]}
+          className="css-0 gf-form-input gf-form-input--form-dropdown width-15"
+          onKeyDown={[Function]}
         >
           <div
-            className="css-0 gf-form-select-box__value-container"
+            className="css-0 gf-form-select-box__control"
+            onMouseDown={[Function]}
+            onTouchEnd={[Function]}
           >
             <div
-              className="css-0 gf-form-select-box__placeholder"
+              className="css-0 gf-form-select-box__value-container"
             >
-              Select Services
+              <div
+                className="css-0 gf-form-select-box__placeholder"
+              >
+                Select Services
+              </div>
+              <input
+                className="css-14uuagi"
+                disabled={false}
+                id="react-select-2-input"
+                onBlur={[Function]}
+                onChange={[Function]}
+                onFocus={[Function]}
+                readOnly={true}
+                tabIndex="0"
+                value=""
+              />
+            </div>
+            <div
+              className="css-0 gf-form-select-box__indicators"
+            >
+              <span
+                className="gf-form-select-box__select-arrow "
+              />
             </div>
-            <input
-              className="css-14uuagi"
-              disabled={false}
-              id="react-select-2-input"
-              onBlur={[Function]}
-              onChange={[Function]}
-              onFocus={[Function]}
-              readOnly={true}
-              tabIndex="0"
-              value=""
-            />
-          </div>
-          <div
-            className="css-0 gf-form-select-box__indicators"
-          >
-            <span
-              className="gf-form-select-box__select-arrow "
-            />
           </div>
         </div>
       </div>
@@ -71,86 +73,88 @@ Array [
       >
         Metric
       </span>
-      <div
-        className="css-0 gf-form-input gf-form-input--form-dropdown width-26"
-        onKeyDown={[Function]}
-      >
+      <div>
         <div
-          className="css-0 gf-form-select-box__control"
-          onMouseDown={[Function]}
-          onTouchEnd={[Function]}
+          className="css-0 gf-form-input gf-form-input--form-dropdown width-26"
+          onKeyDown={[Function]}
         >
           <div
-            className="css-0 gf-form-select-box__value-container"
+            className="css-0 gf-form-select-box__control"
+            onMouseDown={[Function]}
+            onTouchEnd={[Function]}
           >
             <div
-              className="css-0 gf-form-select-box__placeholder"
-            >
-              Select Metric
-            </div>
-            <div
-              className="css-0"
+              className="css-0 gf-form-select-box__value-container"
             >
               <div
-                className="gf-form-select-box__input"
-                style={
-                  Object {
-                    "display": "inline-block",
-                  }
-                }
+                className="css-0 gf-form-select-box__placeholder"
+              >
+                Select Metric
+              </div>
+              <div
+                className="css-0"
               >
-                <input
-                  aria-autocomplete="list"
-                  autoCapitalize="none"
-                  autoComplete="off"
-                  autoCorrect="off"
-                  disabled={false}
-                  id="react-select-3-input"
-                  onBlur={[Function]}
-                  onChange={[Function]}
-                  onFocus={[Function]}
-                  spellCheck="false"
-                  style={
-                    Object {
-                      "background": 0,
-                      "border": 0,
-                      "boxSizing": "content-box",
-                      "color": "inherit",
-                      "fontSize": "inherit",
-                      "opacity": 1,
-                      "outline": 0,
-                      "padding": 0,
-                      "width": "1px",
-                    }
-                  }
-                  tabIndex="0"
-                  type="text"
-                  value=""
-                />
                 <div
+                  className="gf-form-select-box__input"
                   style={
                     Object {
-                      "height": 0,
-                      "left": 0,
-                      "overflow": "scroll",
-                      "position": "absolute",
-                      "top": 0,
-                      "visibility": "hidden",
-                      "whiteSpace": "pre",
+                      "display": "inline-block",
                     }
                   }
                 >
-                  
+                  <input
+                    aria-autocomplete="list"
+                    autoCapitalize="none"
+                    autoComplete="off"
+                    autoCorrect="off"
+                    disabled={false}
+                    id="react-select-3-input"
+                    onBlur={[Function]}
+                    onChange={[Function]}
+                    onFocus={[Function]}
+                    spellCheck="false"
+                    style={
+                      Object {
+                        "background": 0,
+                        "border": 0,
+                        "boxSizing": "content-box",
+                        "color": "inherit",
+                        "fontSize": "inherit",
+                        "opacity": 1,
+                        "outline": 0,
+                        "padding": 0,
+                        "width": "1px",
+                      }
+                    }
+                    tabIndex="0"
+                    type="text"
+                    value=""
+                  />
+                  <div
+                    style={
+                      Object {
+                        "height": 0,
+                        "left": 0,
+                        "overflow": "scroll",
+                        "position": "absolute",
+                        "top": 0,
+                        "visibility": "hidden",
+                        "whiteSpace": "pre",
+                      }
+                    }
+                  >
+                    
+                  </div>
                 </div>
               </div>
             </div>
-          </div>
-          <div
-            className="css-0 gf-form-select-box__indicators"
-          >
-            <span
-              className="gf-form-select-box__select-arrow "
-            />
+            <div
+              className="css-0 gf-form-select-box__indicators"
+            >
+              <span
+                className="gf-form-select-box__select-arrow "
+              />
+            </div>
           </div>
         </div>
       </div>
@@ -181,86 +185,88 @@ Array [
       >
         Aggregation
       </label>
-      <div
-        className="css-0 gf-form-input gf-form-input--form-dropdown width-15"
-        onKeyDown={[Function]}
-      >
+      <div>
         <div
-          className="css-0 gf-form-select-box__control"
-          onMouseDown={[Function]}
-          onTouchEnd={[Function]}
+          className="css-0 gf-form-input gf-form-input--form-dropdown width-15"
+          onKeyDown={[Function]}
         >
           <div
-            className="css-0 gf-form-select-box__value-container"
+            className="css-0 gf-form-select-box__control"
+            onMouseDown={[Function]}
+            onTouchEnd={[Function]}
           >
             <div
-              className="css-0 gf-form-select-box__placeholder"
-            >
-              Select Reducer
-            </div>
-            <div
-              className="css-0"
+              className="css-0 gf-form-select-box__value-container"
             >
               <div
-                className="gf-form-select-box__input"
-                style={
-                  Object {
-                    "display": "inline-block",
-                  }
-                }
+                className="css-0 gf-form-select-box__placeholder"
+              >
+                Select Reducer
+              </div>
+              <div
+                className="css-0"
               >
-                <input
-                  aria-autocomplete="list"
-                  autoCapitalize="none"
-                  autoComplete="off"
-                  autoCorrect="off"
-                  disabled={false}
-                  id="react-select-4-input"
-                  onBlur={[Function]}
-                  onChange={[Function]}
-                  onFocus={[Function]}
-                  spellCheck="false"
-                  style={
-                    Object {
-                      "background": 0,
-                      "border": 0,
-                      "boxSizing": "content-box",
-                      "color": "inherit",
-                      "fontSize": "inherit",
-                      "opacity": 1,
-                      "outline": 0,
-                      "padding": 0,
-                      "width": "1px",
-                    }
-                  }
-                  tabIndex="0"
-                  type="text"
-                  value=""
-                />
                 <div
+                  className="gf-form-select-box__input"
                   style={
                     Object {
-                      "height": 0,
-                      "left": 0,
-                      "overflow": "scroll",
-                      "position": "absolute",
-                      "top": 0,
-                      "visibility": "hidden",
-                      "whiteSpace": "pre",
+                      "display": "inline-block",
                     }
                   }
                 >
-                  
+                  <input
+                    aria-autocomplete="list"
+                    autoCapitalize="none"
+                    autoComplete="off"
+                    autoCorrect="off"
+                    disabled={false}
+                    id="react-select-4-input"
+                    onBlur={[Function]}
+                    onChange={[Function]}
+                    onFocus={[Function]}
+                    spellCheck="false"
+                    style={
+                      Object {
+                        "background": 0,
+                        "border": 0,
+                        "boxSizing": "content-box",
+                        "color": "inherit",
+                        "fontSize": "inherit",
+                        "opacity": 1,
+                        "outline": 0,
+                        "padding": 0,
+                        "width": "1px",
+                      }
+                    }
+                    tabIndex="0"
+                    type="text"
+                    value=""
+                  />
+                  <div
+                    style={
+                      Object {
+                        "height": 0,
+                        "left": 0,
+                        "overflow": "scroll",
+                        "position": "absolute",
+                        "top": 0,
+                        "visibility": "hidden",
+                        "whiteSpace": "pre",
+                      }
+                    }
+                  >
+                    
+                  </div>
                 </div>
               </div>
             </div>
-          </div>
-          <div
-            className="css-0 gf-form-select-box__indicators"
-          >
-            <span
-              className="gf-form-select-box__select-arrow "
-            />
+            <div
+              className="css-0 gf-form-select-box__indicators"
+            >
+              <span
+                className="gf-form-select-box__select-arrow "
+              />
+            </div>
           </div>
         </div>
       </div>
@@ -293,90 +299,92 @@ Array [
       >
         Alignment Period
       </label>
-      <div
-        className="css-0 gf-form-input gf-form-input--form-dropdown width-15"
-        onKeyDown={[Function]}
-      >
+      <div>
         <div
-          className="css-0 gf-form-select-box__control"
-          onMouseDown={[Function]}
-          onTouchEnd={[Function]}
+          className="css-0 gf-form-input gf-form-input--form-dropdown width-15"
+          onKeyDown={[Function]}
         >
           <div
-            className="css-0 gf-form-select-box__value-container gf-form-select-box__value-container--has-value"
+            className="css-0 gf-form-select-box__control"
+            onMouseDown={[Function]}
+            onTouchEnd={[Function]}
           >
             <div
-              className="css-0 gf-form-select-box__single-value"
+              className="css-0 gf-form-select-box__value-container gf-form-select-box__value-container--has-value"
             >
               <div
-                className="gf-form-select-box__img-value"
+                className="css-0 gf-form-select-box__single-value"
               >
-                stackdriver auto
+                <div
+                  className="gf-form-select-box__img-value"
+                >
+                  stackdriver auto
+                </div>
               </div>
-            </div>
-            <div
-              className="css-0"
-            >
               <div
-                className="gf-form-select-box__input"
-                style={
-                  Object {
-                    "display": "inline-block",
-                  }
-                }
+                className="css-0"
               >
-                <input
-                  aria-autocomplete="list"
-                  autoCapitalize="none"
-                  autoComplete="off"
-                  autoCorrect="off"
-                  disabled={false}
-                  id="react-select-5-input"
-                  onBlur={[Function]}
-                  onChange={[Function]}
-                  onFocus={[Function]}
-                  spellCheck="false"
-                  style={
-                    Object {
-                      "background": 0,
-                      "border": 0,
-                      "boxSizing": "content-box",
-                      "color": "inherit",
-                      "fontSize": "inherit",
-                      "opacity": 1,
-                      "outline": 0,
-                      "padding": 0,
-                      "width": "1px",
-                    }
-                  }
-                  tabIndex="0"
-                  type="text"
-                  value=""
-                />
                 <div
+                  className="gf-form-select-box__input"
                   style={
                     Object {
-                      "height": 0,
-                      "left": 0,
-                      "overflow": "scroll",
-                      "position": "absolute",
-                      "top": 0,
-                      "visibility": "hidden",
-                      "whiteSpace": "pre",
+                      "display": "inline-block",
                     }
                   }
                 >
-                  
+                  <input
+                    aria-autocomplete="list"
+                    autoCapitalize="none"
+                    autoComplete="off"
+                    autoCorrect="off"
+                    disabled={false}
+                    id="react-select-5-input"
+                    onBlur={[Function]}
+                    onChange={[Function]}
+                    onFocus={[Function]}
+                    spellCheck="false"
+                    style={
+                      Object {
+                        "background": 0,
+                        "border": 0,
+                        "boxSizing": "content-box",
+                        "color": "inherit",
+                        "fontSize": "inherit",
+                        "opacity": 1,
+                        "outline": 0,
+                        "padding": 0,
+                        "width": "1px",
+                      }
+                    }
+                    tabIndex="0"
+                    type="text"
+                    value=""
+                  />
+                  <div
+                    style={
+                      Object {
+                        "height": 0,
+                        "left": 0,
+                        "overflow": "scroll",
+                        "position": "absolute",
+                        "top": 0,
+                        "visibility": "hidden",
+                        "whiteSpace": "pre",
+                      }
+                    }
+                  >
+                    
+                  </div>
                 </div>
               </div>
             </div>
-          </div>
-          <div
-            className="css-0 gf-form-select-box__indicators"
-          >
-            <span
-              className="gf-form-select-box__select-arrow "
-            />
+            <div
+              className="css-0 gf-form-select-box__indicators"
+            >
+              <span
+                className="gf-form-select-box__select-arrow "
+              />
+            </div>
           </div>
         </div>
       </div>

+ 5 - 0
public/app/types/explore.ts

@@ -250,6 +250,11 @@ export interface ExploreItemState {
    */
   hiddenLogLevels?: LogLevel[];
 
+  /**
+   * How often query should be refreshed
+   */
+  refreshInterval?: string;
+
   urlState: ExploreUrlState;
 
   update: ExploreUpdateState;

+ 1 - 0
public/sass/_variables.generated.scss

@@ -174,6 +174,7 @@ $zindex-tooltip: 1030;
 $zindex-modal-backdrop: 1040;
 $zindex-modal: 1050;
 $zindex-typeahead: 1060;
+$zindex-timepicker-popover: 1070;
 
 // Buttons
 //

+ 10 - 0
public/sass/components/_buttons.scss

@@ -50,6 +50,16 @@
     opacity: 0.65;
     @include box-shadow(none);
   }
+
+  &--radius-left-0 {
+    border-top-left-radius: 0;
+    border-bottom-left-radius: 0;
+  }
+
+  &--radius-right-0 {
+    border-top-right-radius: 0;
+    border-bottom-right-radius: 0;
+  }
 }
 
 // Button Sizes

+ 11 - 0
public/sass/components/_navbar.scss

@@ -29,6 +29,7 @@
   .navbar-button--share,
   .navbar-button--settings,
   .navbar-page-btn .fa-caret-down,
+  .refresh-picker,
   .gf-timepicker-nav {
     display: none;
   }
@@ -135,6 +136,16 @@
     }
   }
 
+  &--refresh {
+    padding-left: 8px;
+    padding-right: 8px;
+  }
+
+  &--attached {
+    margin-left: 0;
+    border-radius: 0 2px 2px 0;
+  }
+
   &--tight {
     padding: 7px 4px;
 

+ 22 - 0
yarn.lock

@@ -7967,6 +7967,13 @@ get-stream@^4.0.0, get-stream@^4.1.0:
   dependencies:
     pump "^3.0.0"
 
+get-user-locale@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/get-user-locale/-/get-user-locale-1.1.1.tgz#edff0a8bbd6aa3ed0ca30cc441e1acd111543b7f"
+  integrity sha512-KuA+vMhsY+rSPK8hrmOvf7xXIMTs+L06RkgZ83jawZHSEqPLafZtQ63d3waXW3r8z6EQ49I/trraNncWM+s/2g==
+  dependencies:
+    lodash.once "^4.1.1"
+
 get-value@^2.0.3, get-value@^2.0.6:
   version "2.0.6"
   resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
@@ -11229,6 +11236,11 @@ meow@^3.3.0, meow@^3.7.0:
     redent "^1.0.0"
     trim-newlines "^1.0.0"
 
+merge-class-names@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/merge-class-names/-/merge-class-names-1.1.1.tgz#3bd2f38eb5418c464a0fef615484fdf6c8932256"
+  integrity sha512-+UUWBUoFw9QLY/UlBKU/xk9h6OhyG3BUDDuF2eIJcxmusWb/uedvNpZGkysqMw5b/ds+wkX7NJTDSdUuRsCNyA==
+
 merge-deep@^3.0.2:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/merge-deep/-/merge-deep-3.0.2.tgz#f39fa100a4f1bd34ff29f7d2bf4508fbb8d83ad2"
@@ -13930,6 +13942,16 @@ react-addons-create-fragment@^15.5.3:
     loose-envify "^1.3.1"
     object-assign "^4.1.0"
 
+react-calendar@^2.18.1:
+  version "2.18.1"
+  resolved "https://registry.yarnpkg.com/react-calendar/-/react-calendar-2.18.1.tgz#f8ef9468d8566aa0d47d9d70c88917bb2030bcb9"
+  integrity sha512-J3tVim1gLpnsCOaeez+z4QJB5oK6UYLJj5TSMOStSJBvkWMEcTzj7bq7yCJJCNLUg2Vd3i11gJXish0LUFhXaw==
+  dependencies:
+    get-user-locale "^1.1.1"
+    merge-class-names "^1.1.1"
+    prop-types "^15.6.0"
+    react-lifecycles-compat "^3.0.4"
+
 react-clientside-effect@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/react-clientside-effect/-/react-clientside-effect-1.2.0.tgz#db823695f75e9616a5e4dd6d908e5ea627fb2516"