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:
@@ -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",
|
||||
|
@@ -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);
|
||||
|
@@ -20,10 +20,6 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.select-button-value {
|
||||
color: $orange;
|
||||
}
|
||||
|
||||
&--off {
|
||||
.select-button-value {
|
||||
display: none;
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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 };
|
||||
|
@@ -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;
|
||||
|
17
yarn.lock
17
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"
|
||||
|
Reference in New Issue
Block a user