فهرست منبع

Explore: Update live tail buttons (#19143)

Andrej Ocenas 6 سال پیش
والد
کامیت
359404eb77

+ 2 - 0
package.json

@@ -26,6 +26,7 @@
     "@types/enzyme-adapter-react-16": "1.0.5",
     "@types/expect-puppeteer": "3.3.1",
     "@types/file-saver": "2.0.1",
+    "@types/hoist-non-react-statics": "3.3.0",
     "@types/is-hotkey": "0.1.1",
     "@types/jest": "24.0.13",
     "@types/jquery": "1.10.35",
@@ -208,6 +209,7 @@
     "eventemitter3": "2.0.3",
     "fast-text-encoding": "^1.0.0",
     "file-saver": "1.3.8",
+    "hoist-non-react-statics": "3.3.0",
     "immutable": "3.8.2",
     "is-hotkey": "0.1.4",
     "jquery": "3.4.1",

+ 50 - 10
packages/grafana-ui/src/components/RefreshPicker/RefreshPicker.tsx

@@ -1,24 +1,48 @@
 import React, { PureComponent } from 'react';
 import classNames from 'classnames';
 import { SelectableValue } from '@grafana/data';
+import { css } from 'emotion';
 import { Tooltip } from '../Tooltip/Tooltip';
 import { ButtonSelect } from '../Select/ButtonSelect';
+import memoizeOne from 'memoize-one';
+import { GrafanaTheme } from '../../types';
+import { withTheme } from '../../themes';
 
 export const offOption = { label: 'Off', value: '' };
 export const liveOption = { label: 'Live', value: 'LIVE' };
 export const defaultIntervals = ['5s', '10s', '30s', '1m', '5m', '15m', '30m', '1h', '2h', '1d'];
 export const isLive = (refreshInterval: string): boolean => refreshInterval === liveOption.value;
 
+const getStyles = memoizeOne((theme: GrafanaTheme) => {
+  return {
+    selectButton: css`
+      label: selectButton;
+      .select-button-value {
+        color: ${theme.colors.orange};
+      }
+    `,
+  };
+});
+
 export interface Props {
   intervals?: string[];
-  onRefresh: () => any;
+  onRefresh?: () => any;
   onIntervalChanged: (interval: string) => void;
   value?: string;
-  tooltip: string;
+  tooltip?: string;
   hasLiveOption?: boolean;
+  // You can supply your own refresh button element. In that case onRefresh and tooltip are ignored.
+  refreshButton?: React.ReactNode;
+  buttonSelectClassName?: string;
+  theme: GrafanaTheme;
 }
 
-export class RefreshPicker extends PureComponent<Props> {
+export class RefreshPickerBase extends PureComponent<Props> {
+  // Make it exported as static properties to be easier to access. The global exports need to be accessed by direct
+  // import of this source file which won't work if this was installed as package.
+  static offOption = offOption;
+  static liveOption = liveOption;
+
   constructor(props: Props) {
     super(props);
   }
@@ -46,10 +70,11 @@ export class RefreshPicker extends PureComponent<Props> {
   };
 
   render() {
-    const { onRefresh, intervals, tooltip, value } = this.props;
+    const { onRefresh, intervals, tooltip, value, refreshButton, buttonSelectClassName, theme } = this.props;
     const options = this.intervalsToOptions(intervals);
     const currentValue = value || '';
     const selectedValue = options.find(item => item.value === currentValue) || offOption;
+    const styles = getStyles(theme);
 
     const cssClasses = classNames({
       'refresh-picker': true,
@@ -60,13 +85,20 @@ export class RefreshPicker extends PureComponent<Props> {
     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--border-right-0" onClick={onRefresh}>
-              <i className="fa fa-refresh" />
-            </button>
-          </Tooltip>
+          {refreshButton ? (
+            refreshButton
+          ) : (
+            <Tooltip placement="top" content={tooltip!}>
+              <button
+                className="btn btn--radius-right-0 navbar-button navbar-button--border-right-0"
+                onClick={onRefresh!}
+              >
+                <i className="fa fa-refresh" />
+              </button>
+            </Tooltip>
+          )}
           <ButtonSelect
-            className="navbar-button--attached btn--radius-left-0$"
+            className={classNames('navbar-button--attached', styles.selectButton, buttonSelectClassName)}
             value={selectedValue}
             label={selectedValue.label}
             options={options}
@@ -78,3 +110,11 @@ export class RefreshPicker extends PureComponent<Props> {
     );
   }
 }
+
+export const RefreshPicker = withTheme<
+  Props,
+  {
+    offOption: typeof RefreshPickerBase.offOption;
+    liveOption: typeof RefreshPickerBase.liveOption;
+  }
+>(RefreshPickerBase);

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

@@ -20,10 +20,6 @@
     width: 100%;
   }
 
-  .select-button-value {
-    color: $orange;
-  }
-
   &--off {
     .select-button-value {
       display: none;

+ 11 - 4
packages/grafana-ui/src/themes/ThemeContext.tsx

@@ -1,4 +1,6 @@
-import React from 'react';
+import React, { useContext } from 'react';
+import hoistNonReactStatics from 'hoist-non-react-statics';
+
 import { getTheme } from './getTheme';
 import { GrafanaThemeType, Themeable } from '../types/theme';
 
@@ -8,13 +10,18 @@ type Subtract<T, K> = Omit<T, keyof K>;
 // Use Grafana Dark theme by default
 export const ThemeContext = React.createContext(getTheme(GrafanaThemeType.Dark));
 
-export const withTheme = <P extends Themeable>(Component: React.ComponentType<P>) => {
+export const withTheme = <P extends Themeable, S extends {} = {}>(Component: React.ComponentType<P>) => {
   const WithTheme: React.FunctionComponent<Subtract<P, Themeable>> = props => {
     // @ts-ignore
     return <ThemeContext.Consumer>{theme => <Component {...props} theme={theme} />}</ThemeContext.Consumer>;
   };
 
   WithTheme.displayName = `WithTheme(${Component.displayName})`;
-
-  return WithTheme;
+  hoistNonReactStatics(WithTheme, Component);
+  type Hoisted = typeof WithTheme & S;
+  return WithTheme as Hoisted;
 };
+
+export function useTheme() {
+  return useContext(ThemeContext);
+}

+ 2 - 2
packages/grafana-ui/src/themes/index.ts

@@ -1,5 +1,5 @@
-import { ThemeContext, withTheme } from './ThemeContext';
+import { ThemeContext, withTheme, useTheme } from './ThemeContext';
 import { getTheme, mockTheme } from './getTheme';
 import { selectThemeVariant } from './selectThemeVariant';
 
-export { ThemeContext, withTheme, mockTheme, getTheme, selectThemeVariant };
+export { ThemeContext, withTheme, mockTheme, getTheme, selectThemeVariant, useTheme };

+ 11 - 39
public/app/features/explore/ExploreTimeControls.tsx

@@ -8,7 +8,7 @@ import { TimeRange, TimeOption, TimeZone, RawTimeRange, dateTimeForTimeZone } fr
 // State
 
 // Components
-import { TimePicker, RefreshPicker, SetInterval } from '@grafana/ui';
+import { TimePicker } from '@grafana/ui';
 
 // Utils & Services
 import { defaultSelectOptions } from '@grafana/ui/src/components/TimePicker/TimePicker';
@@ -16,14 +16,8 @@ import { getShiftedTimeRange, getZoomedTimeRange } from 'app/core/utils/timePick
 
 export interface Props {
   exploreId: ExploreId;
-  hasLiveOption: boolean;
-  isLive: boolean;
-  loading: boolean;
   range: TimeRange;
-  refreshInterval: string;
   timeZone: TimeZone;
-  onRunQuery: () => void;
-  onChangeRefreshInterval: (interval: string) => void;
   onChangeTime: (range: RawTimeRange) => void;
 }
 
@@ -73,40 +67,18 @@ export class ExploreTimeControls extends Component<Props> {
   };
 
   render() {
-    const {
-      hasLiveOption,
-      isLive,
-      loading,
-      range,
-      refreshInterval,
-      timeZone,
-      onRunQuery,
-      onChangeRefreshInterval,
-    } = this.props;
+    const { range, timeZone } = this.props;
 
     return (
-      <>
-        {!isLive && (
-          <TimePicker
-            value={range}
-            onChange={this.onChangeTimePicker}
-            timeZone={timeZone}
-            onMoveBackward={this.onMoveBack}
-            onMoveForward={this.onMoveForward}
-            onZoom={this.onZoom}
-            selectOptions={this.setActiveTimeOption(defaultSelectOptions, range.raw)}
-          />
-        )}
-
-        <RefreshPicker
-          onIntervalChanged={onChangeRefreshInterval}
-          onRefresh={onRunQuery}
-          value={refreshInterval}
-          tooltip="Refresh"
-          hasLiveOption={hasLiveOption}
-        />
-        {refreshInterval && <SetInterval func={onRunQuery} interval={refreshInterval} loading={loading} />}
-      </>
+      <TimePicker
+        value={range}
+        onChange={this.onChangeTimePicker}
+        timeZone={timeZone}
+        onMoveBackward={this.onMoveBack}
+        onMoveForward={this.onMoveForward}
+        onZoom={this.onZoom}
+        selectOptions={this.setActiveTimeOption(defaultSelectOptions, range.raw)}
+      />
     );
   }
 }

+ 83 - 67
public/app/features/explore/ExploreToolbar.tsx

@@ -5,8 +5,17 @@ import { hot } from 'react-hot-loader';
 import memoizeOne from 'memoize-one';
 import classNames from 'classnames';
 
-import { ExploreId, ExploreMode } from 'app/types/explore';
-import { DataSourceSelectItem, ToggleButtonGroup, ToggleButton, DataQuery, Tooltip, ButtonSelect } from '@grafana/ui';
+import { ExploreId, ExploreItemState, ExploreMode } from 'app/types/explore';
+import {
+  DataSourceSelectItem,
+  ToggleButtonGroup,
+  ToggleButton,
+  DataQuery,
+  Tooltip,
+  ButtonSelect,
+  RefreshPicker,
+  SetInterval,
+} from '@grafana/ui';
 import { RawTimeRange, TimeZone, TimeRange, SelectableValue } from '@grafana/data';
 import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
 import { StoreState } from 'app/types/store';
@@ -20,44 +29,15 @@ import {
   changeMode,
   clearOrigin,
 } from './state/actions';
+import { changeRefreshIntervalAction, setPausedStateAction } from './state/actionTypes';
 import { updateLocation } from 'app/core/actions';
 import { getTimeZone } from '../profile/state/selectors';
 import { getDashboardSrv } from '../dashboard/services/DashboardSrv';
 import kbn from '../../core/utils/kbn';
 import { ExploreTimeControls } from './ExploreTimeControls';
-
-enum IconSide {
-  left = 'left',
-  right = 'right',
-}
-
-const createResponsiveButton = (options: {
-  splitted: boolean;
-  title: string;
-  onClick: () => void;
-  buttonClassName?: string;
-  iconClassName?: string;
-  iconSide?: IconSide;
-  disabled?: boolean;
-}) => {
-  const defaultOptions = {
-    iconSide: IconSide.left,
-  };
-  const props = { ...options, defaultOptions };
-  const { title, onClick, buttonClassName, iconClassName, splitted, iconSide, disabled } = props;
-
-  return (
-    <button
-      className={`btn navbar-button ${buttonClassName ? buttonClassName : ''}`}
-      onClick={onClick}
-      disabled={disabled || false}
-    >
-      {iconClassName && iconSide === IconSide.left ? <i className={`${iconClassName}`} /> : null}
-      <span className="btn-title">{!splitted ? title : ''}</span>
-      {iconClassName && iconSide === IconSide.right ? <i className={`${iconClassName}`} /> : null}
-    </button>
-  );
-};
+import { LiveTailButton } from './LiveTailButton';
+import { ResponsiveButton } from './ResponsiveButton';
+import { RunButton } from './RunButton';
 
 interface OwnProps {
   exploreId: ExploreId;
@@ -77,6 +57,7 @@ interface StateProps {
   selectedModeOption: SelectableValue<ExploreMode>;
   hasLiveOption: boolean;
   isLive: boolean;
+  isPaused: boolean;
   originPanelId: number;
   queries: DataQuery[];
 }
@@ -91,6 +72,8 @@ interface DispatchProps {
   changeMode: typeof changeMode;
   clearOrigin: typeof clearOrigin;
   updateLocation: typeof updateLocation;
+  changeRefreshIntervalAction: typeof changeRefreshIntervalAction;
+  setPausedStateAction: typeof setPausedStateAction;
 }
 
 type Props = StateProps & DispatchProps & OwnProps;
@@ -147,6 +130,28 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
     });
   };
 
+  stopLive = () => {
+    const { exploreId } = this.props;
+    // TODO referencing this from perspective of refresh picker when there is designated button for it now is not
+    //  great. Needs another refactor.
+    this.props.changeRefreshIntervalAction({ exploreId, refreshInterval: RefreshPicker.offOption.value });
+  };
+
+  startLive = () => {
+    const { exploreId } = this.props;
+    this.props.changeRefreshIntervalAction({ exploreId, refreshInterval: RefreshPicker.liveOption.value });
+  };
+
+  pauseLive = () => {
+    const { exploreId } = this.props;
+    this.props.setPausedStateAction({ exploreId, isPaused: true });
+  };
+
+  resumeLive = () => {
+    const { exploreId } = this.props;
+    this.props.setPausedStateAction({ exploreId, isPaused: false });
+  };
+
   render() {
     const {
       datasourceMissing,
@@ -165,6 +170,7 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
       selectedModeOption,
       hasLiveOption,
       isLive,
+      isPaused,
       originPanelId,
     } = this.props;
 
@@ -249,30 +255,25 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
 
             {exploreId === 'left' && !splitted ? (
               <div className="explore-toolbar-content-item">
-                {createResponsiveButton({
-                  splitted,
-                  title: 'Split',
-                  onClick: split,
-                  iconClassName: 'fa fa-fw fa-columns icon-margin-right',
-                  iconSide: IconSide.left,
-                  disabled: isLive,
-                })}
+                <ResponsiveButton
+                  splitted={splitted}
+                  title="Split"
+                  onClick={split}
+                  iconClassName="fa fa-fw fa-columns icon-margin-right"
+                  disabled={isLive}
+                />
               </div>
             ) : null}
-            <div className="explore-toolbar-content-item">
-              <ExploreTimeControls
-                exploreId={exploreId}
-                hasLiveOption={hasLiveOption}
-                isLive={isLive}
-                loading={loading}
-                range={range}
-                refreshInterval={refreshInterval}
-                timeZone={timeZone}
-                onChangeTime={onChangeTime}
-                onChangeRefreshInterval={this.onChangeRefreshInterval}
-                onRunQuery={this.onRunQuery}
-              />
-            </div>
+            {!isLive && (
+              <div className="explore-toolbar-content-item">
+                <ExploreTimeControls
+                  exploreId={exploreId}
+                  range={range}
+                  timeZone={timeZone}
+                  onChangeTime={onChangeTime}
+                />
+              </div>
+            )}
 
             <div className="explore-toolbar-content-item">
               <button className="btn navbar-button" onClick={this.onClearAll}>
@@ -280,16 +281,27 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
               </button>
             </div>
             <div className="explore-toolbar-content-item">
-              {createResponsiveButton({
-                splitted,
-                title: 'Run Query',
-                onClick: this.onRunQuery,
-                buttonClassName: 'navbar-button--secondary',
-                iconClassName:
-                  loading && !isLive ? 'fa fa-spinner fa-fw fa-spin run-icon' : 'fa fa-level-down fa-fw run-icon',
-                iconSide: IconSide.right,
-              })}
+              <RunButton
+                refreshInterval={refreshInterval}
+                onChangeRefreshInterval={this.onChangeRefreshInterval}
+                splitted={splitted}
+                loading={loading || (isLive && !isPaused)}
+                onRun={this.onRunQuery}
+                showDropdown={!isLive}
+              />
+              {refreshInterval && <SetInterval func={this.onRunQuery} interval={refreshInterval} loading={loading} />}
             </div>
+
+            {hasLiveOption && (
+              <LiveTailButton
+                isLive={isLive}
+                isPaused={isPaused}
+                start={this.startLive}
+                pause={this.pauseLive}
+                resume={this.resumeLive}
+                stop={this.stopLive}
+              />
+            )}
           </div>
         </div>
       </div>
@@ -334,7 +346,7 @@ const getModeOptionsMemoized = memoizeOne(
 
 const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps => {
   const splitted = state.explore.split;
-  const exploreItem = state.explore[exploreId];
+  const exploreItem: ExploreItemState = state.explore[exploreId];
   const {
     datasourceInstance,
     datasourceMissing,
@@ -345,6 +357,7 @@ const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps
     supportedModes,
     mode,
     isLive,
+    isPaused,
     originPanelId,
     queries,
   } = exploreItem;
@@ -369,6 +382,7 @@ const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps
     selectedModeOption,
     hasLiveOption,
     isLive,
+    isPaused,
     originPanelId,
     queries,
   };
@@ -384,6 +398,8 @@ const mapDispatchToProps: DispatchProps = {
   split: splitOpen,
   changeMode: changeMode,
   clearOrigin,
+  changeRefreshIntervalAction,
+  setPausedStateAction,
 };
 
 export const ExploreToolbar = hot(module)(

+ 104 - 0
public/app/features/explore/LiveTailButton.tsx

@@ -0,0 +1,104 @@
+import React from 'react';
+import classNames from 'classnames';
+import { css } from 'emotion';
+import memoizeOne from 'memoize-one';
+import { GrafanaTheme, GrafanaThemeType, useTheme } from '@grafana/ui';
+import tinycolor from 'tinycolor2';
+
+const orangeDark = '#FF780A';
+const orangeDarkLighter = tinycolor(orangeDark)
+  .lighten(10)
+  .toString();
+const orangeLight = '#ED5700';
+const orangeLightLighter = tinycolor(orangeLight)
+  .lighten(10)
+  .toString();
+
+const getStyles = memoizeOne((theme: GrafanaTheme) => {
+  const orange = theme.type === GrafanaThemeType.Dark ? orangeDark : orangeLight;
+  const orangeLighter = theme.type === GrafanaThemeType.Dark ? orangeDarkLighter : orangeLightLighter;
+  const textColor = theme.type === GrafanaThemeType.Dark ? theme.colors.white : theme.colors.black;
+
+  return {
+    noRightBorderStyle: css`
+      label: noRightBorderStyle;
+      border-right: 0;
+    `,
+    isLive: css`
+      label: isLive;
+      border-color: ${orange};
+      color: ${orange};
+      background: transparent;
+      &:focus {
+        border-color: ${orange};
+        color: ${orange};
+      }
+      &:active,
+      &:hover {
+        border-color: ${orangeLighter};
+        color: ${orangeLighter};
+      }
+    `,
+    isPaused: css`
+      label: isPaused;
+      border-color: ${orange};
+      background: transparent;
+      animation: pulse 2s ease-out 0s infinite normal forwards;
+      &:focus {
+        border-color: ${orange};
+      }
+      &:active,
+      &:hover {
+        border-color: ${orangeLighter};
+      }
+      @keyframes pulse {
+        0% {
+          color: ${textColor};
+        }
+        50% {
+          color: ${orange};
+        }
+        100% {
+          color: ${textColor};
+        }
+      }
+    `,
+  };
+});
+
+type LiveTailButtonProps = {
+  start: () => void;
+  stop: () => void;
+  pause: () => void;
+  resume: () => void;
+  isLive: boolean;
+  isPaused: boolean;
+};
+export function LiveTailButton(props: LiveTailButtonProps) {
+  const { start, pause, resume, isLive, isPaused, stop } = props;
+  const theme = useTheme();
+  const styles = getStyles(theme);
+
+  const onClickMain = isLive ? (isPaused ? resume : pause) : start;
+
+  return (
+    <div className="explore-toolbar-content-item">
+      <button
+        className={classNames('btn navbar-button', {
+          [`btn--radius-right-0 ${styles.noRightBorderStyle}`]: isLive,
+          [styles.isLive]: isLive && !isPaused,
+          [styles.isPaused]: isLive && isPaused,
+        })}
+        onClick={onClickMain}
+      >
+        <i className={classNames('fa', isPaused || !isLive ? 'fa-play' : 'fa-pause')} />
+        &nbsp; Live tailing
+      </button>
+      {isLive && (
+        <button className={`btn navbar-button navbar-button--attached ${styles.isLive}`} onClick={stop}>
+          <i className={'fa fa-stop'} />
+        </button>
+      )}
+    </div>
+  );
+}

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

@@ -1,7 +1,7 @@
 import React, { PureComponent } from 'react';
 import { hot } from 'react-hot-loader';
 import { connect } from 'react-redux';
-import { DataSourceApi, Collapse } from '@grafana/ui';
+import { DataSourceApi, Collapse, RefreshPicker } from '@grafana/ui';
 
 import {
   RawTimeRange,
@@ -26,7 +26,6 @@ import {
 import { deduplicatedLogsSelector, exploreItemUIStateSelector } from 'app/features/explore/state/selectors';
 import { getTimeZone } from '../profile/state/selectors';
 import { LiveLogsWithTheme } from './LiveLogs';
-import { offOption } from '@grafana/ui/src/components/RefreshPicker/RefreshPicker';
 import { Logs } from './Logs';
 
 interface LogsContainerProps {
@@ -65,7 +64,7 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
 
   onStopLive = () => {
     const { exploreId } = this.props;
-    this.props.stopLive({ exploreId, refreshInterval: offOption.value });
+    this.props.stopLive({ exploreId, refreshInterval: RefreshPicker.offOption.value });
   };
 
   onPause = () => {

+ 36 - 0
public/app/features/explore/ResponsiveButton.tsx

@@ -0,0 +1,36 @@
+import React from 'react';
+
+export enum IconSide {
+  left = 'left',
+  right = 'right',
+}
+
+type Props = {
+  splitted: boolean;
+  title: string;
+  onClick: () => void;
+  buttonClassName?: string;
+  iconClassName?: string;
+  iconSide?: IconSide;
+  disabled?: boolean;
+};
+
+export const ResponsiveButton = (props: Props) => {
+  const defaultProps = {
+    iconSide: IconSide.left,
+  };
+  props = { ...defaultProps, ...props };
+  const { title, onClick, buttonClassName, iconClassName, splitted, iconSide, disabled } = props;
+
+  return (
+    <button
+      className={`btn navbar-button ${buttonClassName ? buttonClassName : ''}`}
+      onClick={onClick}
+      disabled={disabled || false}
+    >
+      {iconClassName && iconSide === IconSide.left ? <i className={`${iconClassName}`} /> : null}
+      <span className="btn-title">{!splitted ? title : ''}</span>
+      {iconClassName && iconSide === IconSide.right ? <i className={`${iconClassName}`} /> : null}
+    </button>
+  );
+};

+ 52 - 0
public/app/features/explore/RunButton.tsx

@@ -0,0 +1,52 @@
+import React from 'react';
+import { RefreshPicker } from '@grafana/ui';
+import memoizeOne from 'memoize-one';
+import { css } from 'emotion';
+
+import { ResponsiveButton } from './ResponsiveButton';
+
+const getStyles = memoizeOne(() => {
+  return {
+    selectButtonOverride: css`
+      label: selectButtonOverride;
+      .select-button-value {
+        color: white !important;
+      }
+    `,
+  };
+});
+
+type Props = {
+  splitted: boolean;
+  loading: boolean;
+  onRun: () => void;
+  refreshInterval: string;
+  onChangeRefreshInterval: (interval: string) => void;
+  showDropdown: boolean;
+};
+
+export function RunButton(props: Props) {
+  const { splitted, loading, onRun, onChangeRefreshInterval, refreshInterval, showDropdown } = props;
+  const styles = getStyles();
+  const runButton = (
+    <ResponsiveButton
+      splitted={splitted}
+      title="Run Query"
+      onClick={onRun}
+      buttonClassName="navbar-button--secondary btn--radius-right-0 "
+      iconClassName={loading ? 'fa fa-spinner fa-fw fa-spin run-icon' : 'fa fa-refresh fa-fw'}
+    />
+  );
+
+  if (showDropdown) {
+    return (
+      <RefreshPicker
+        onIntervalChanged={onChangeRefreshInterval}
+        value={refreshInterval}
+        buttonSelectClassName={`navbar-button--secondary ${styles.selectButtonOverride}`}
+        refreshButton={runButton}
+      />
+    );
+  }
+  return runButton;
+}

+ 0 - 4
public/sass/pages/_explore.scss

@@ -6,10 +6,6 @@
   margin-left: 0.25em;
 }
 
-.run-icon {
-  transform: rotate(90deg);
-}
-
 .datasource-picker {
   .ds-picker {
     min-width: 200px;

+ 12 - 5
yarn.lock

@@ -3048,6 +3048,13 @@
     "@types/minimatch" "*"
     "@types/node" "*"
 
+"@types/hoist-non-react-statics@3.3.0":
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.0.tgz#a59c0c995cc885bef1b8ec2241b114f9b35b517b"
+  integrity sha512-O2OGyW9wlO2bbDmZRH17MecArQfsIa1g//ve2IJk6BnmwEglFz5kdhP1BlgeqjVNH5IHIhsc83DWFo8StCe8+Q==
+  dependencies:
+    "@types/react" "*"
+
 "@types/hoist-non-react-statics@^3.3.0":
   version "3.3.1"
   resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
@@ -9246,16 +9253,16 @@ hmac-drbg@^1.0.0:
     minimalistic-assert "^1.0.0"
     minimalistic-crypto-utils "^1.0.1"
 
-hoist-non-react-statics@^2.3.1:
-  version "2.5.5"
-  resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47"
-
-hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0:
+hoist-non-react-statics@3.3.0, hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0:
   version "3.3.0"
   resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.0.tgz#b09178f0122184fb95acf525daaecb4d8f45958b"
   dependencies:
     react-is "^16.7.0"
 
+hoist-non-react-statics@^2.3.1:
+  version "2.5.5"
+  resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47"
+
 homedir-polyfill@^1.0.1:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8"