mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Explore: Update live tail buttons (#19143)
This commit is contained in:
@@ -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)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
public/app/features/explore/LiveTailButton.tsx
Normal file
104
public/app/features/explore/LiveTailButton.tsx
Normal 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')} />
|
||||
Live tailing
|
||||
</button>
|
||||
{isLive && (
|
||||
<button className={`btn navbar-button navbar-button--attached ${styles.isLive}`} onClick={stop}>
|
||||
<i className={'fa fa-stop'} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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
public/app/features/explore/ResponsiveButton.tsx
Normal file
36
public/app/features/explore/ResponsiveButton.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
52
public/app/features/explore/RunButton.tsx
Normal file
52
public/app/features/explore/RunButton.tsx
Normal 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;
|
||||
}
|
||||
@@ -6,10 +6,6 @@
|
||||
margin-left: 0.25em;
|
||||
}
|
||||
|
||||
.run-icon {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.datasource-picker {
|
||||
.ds-picker {
|
||||
min-width: 200px;
|
||||
|
||||
Reference in New Issue
Block a user