Explore: Update live tail buttons (#19143)

This commit is contained in:
Andrej Ocenas
2019-09-17 11:25:12 +02:00
committed by GitHub
parent 494b4aaf88
commit 359404eb77
13 changed files with 365 additions and 138 deletions

View File

@@ -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)}
/>
);
}
}

View File

@@ -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)(

View File

@@ -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>
);
}

View File

@@ -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 = () => {

View File

@@ -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>
);
};

View File

@@ -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;
}

View File

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