mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Explore: live tail UI fixes and improvements (#19187)
This commit is contained in:
parent
9feac7753b
commit
bf24cbba76
@ -1,6 +1,7 @@
|
||||
// Libraries
|
||||
import React, { ComponentClass } from 'react';
|
||||
import { hot } from 'react-hot-loader';
|
||||
import { css } from 'emotion';
|
||||
// @ts-ignore
|
||||
import { connect } from 'react-redux';
|
||||
import { AutoSizer } from 'react-virtualized';
|
||||
@ -52,6 +53,16 @@ import { ErrorContainer } from './ErrorContainer';
|
||||
import { scanStopAction } from './state/actionTypes';
|
||||
import { ExploreGraphPanel } from './ExploreGraphPanel';
|
||||
|
||||
const getStyles = memoizeOne(() => {
|
||||
return {
|
||||
logsMain: css`
|
||||
label: logsMain;
|
||||
// Is needed for some transition animations to work.
|
||||
position: relative;
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
interface ExploreProps {
|
||||
StartPage?: ComponentClass<ExploreStartPageProps>;
|
||||
changeSize: typeof changeSize;
|
||||
@ -257,6 +268,7 @@ export class Explore extends React.PureComponent<ExploreProps> {
|
||||
queryResponse,
|
||||
} = this.props;
|
||||
const exploreClass = split ? 'explore explore-split' : 'explore';
|
||||
const styles = getStyles();
|
||||
|
||||
return (
|
||||
<div className={exploreClass} ref={this.getRef}>
|
||||
@ -284,7 +296,7 @@ export class Explore extends React.PureComponent<ExploreProps> {
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="m-t-2" style={{ width }}>
|
||||
<main className={`m-t-2 ${styles.logsMain}`} style={{ width }}>
|
||||
<ErrorBoundaryAlert>
|
||||
{showingStartPage && (
|
||||
<div className="grafana-info-box grafana-info-box--max-lg">
|
||||
|
@ -4,6 +4,7 @@ import { connect } from 'react-redux';
|
||||
import { hot } from 'react-hot-loader';
|
||||
import memoizeOne from 'memoize-one';
|
||||
import classNames from 'classnames';
|
||||
import { css } from 'emotion';
|
||||
|
||||
import { ExploreId, ExploreItemState, ExploreMode } from 'app/types/explore';
|
||||
import {
|
||||
@ -39,6 +40,14 @@ import { LiveTailButton } from './LiveTailButton';
|
||||
import { ResponsiveButton } from './ResponsiveButton';
|
||||
import { RunButton } from './RunButton';
|
||||
|
||||
const getStyles = memoizeOne(() => {
|
||||
return {
|
||||
liveTailButtons: css`
|
||||
margin-left: 10px;
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
interface OwnProps {
|
||||
exploreId: ExploreId;
|
||||
onChangeTime: (range: RawTimeRange, changedByScanner?: boolean) => void;
|
||||
@ -132,6 +141,7 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
|
||||
|
||||
stopLive = () => {
|
||||
const { exploreId } = this.props;
|
||||
this.pauseLive();
|
||||
// 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 });
|
||||
@ -174,6 +184,7 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
|
||||
originPanelId,
|
||||
} = this.props;
|
||||
|
||||
const styles = getStyles();
|
||||
const originDashboardIsEditable = Number.isInteger(originPanelId);
|
||||
const panelReturnClasses = classNames('btn', 'navbar-button', {
|
||||
'btn--radius-right-0': originDashboardIsEditable,
|
||||
@ -293,14 +304,16 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
|
||||
</div>
|
||||
|
||||
{hasLiveOption && (
|
||||
<LiveTailButton
|
||||
isLive={isLive}
|
||||
isPaused={isPaused}
|
||||
start={this.startLive}
|
||||
pause={this.pauseLive}
|
||||
resume={this.resumeLive}
|
||||
stop={this.stopLive}
|
||||
/>
|
||||
<div className={`explore-toolbar-content-item ${styles.liveTailButtons}`}>
|
||||
<LiveTailButton
|
||||
isLive={isLive}
|
||||
isPaused={isPaused}
|
||||
start={this.startLive}
|
||||
pause={this.pauseLive}
|
||||
resume={this.resumeLive}
|
||||
stop={this.stopLive}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -157,7 +157,7 @@ class LiveLogs extends PureComponent<Props, State> {
|
||||
const { logsRow, logsRowLocalTime, logsRowMessage } = getLogRowStyles(theme);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<div
|
||||
onScroll={isPaused ? undefined : this.onScroll}
|
||||
className={cx(['logs-rows', styles.logsRowsLive])}
|
||||
@ -210,7 +210,7 @@ class LiveLogs extends PureComponent<Props, State> {
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -2,28 +2,30 @@ 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';
|
||||
import { CSSTransition } from 'react-transition-group';
|
||||
|
||||
const orangeDark = '#FF780A';
|
||||
const orangeDarkLighter = tinycolor(orangeDark)
|
||||
.lighten(10)
|
||||
.toString();
|
||||
const orangeLight = '#ED5700';
|
||||
const orangeLightLighter = tinycolor(orangeLight)
|
||||
.lighten(10)
|
||||
.toString();
|
||||
import { GrafanaTheme, GrafanaThemeType, useTheme } from '@grafana/ui';
|
||||
|
||||
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;
|
||||
const orange = theme.type === GrafanaThemeType.Dark ? '#FF780A' : '#ED5700';
|
||||
const orangeLighter = tinycolor(orange)
|
||||
.lighten(10)
|
||||
.toString();
|
||||
const pulseTextColor = tinycolor(orange)
|
||||
.desaturate(90)
|
||||
.toString();
|
||||
|
||||
return {
|
||||
noRightBorderStyle: css`
|
||||
label: noRightBorderStyle;
|
||||
border-right: 0;
|
||||
`,
|
||||
liveButton: css`
|
||||
label: liveButton;
|
||||
transition: background-color 1s, border-color 1s, color 1s;
|
||||
margin: 0;
|
||||
`,
|
||||
isLive: css`
|
||||
label: isLive;
|
||||
border-color: ${orange};
|
||||
@ -43,7 +45,7 @@ const getStyles = memoizeOne((theme: GrafanaTheme) => {
|
||||
label: isPaused;
|
||||
border-color: ${orange};
|
||||
background: transparent;
|
||||
animation: pulse 2s ease-out 0s infinite normal forwards;
|
||||
animation: pulse 3s ease-out 0s infinite normal forwards;
|
||||
&:focus {
|
||||
border-color: ${orange};
|
||||
}
|
||||
@ -53,16 +55,40 @@ const getStyles = memoizeOne((theme: GrafanaTheme) => {
|
||||
}
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
color: ${textColor};
|
||||
color: ${pulseTextColor};
|
||||
}
|
||||
50% {
|
||||
color: ${orange};
|
||||
}
|
||||
100% {
|
||||
color: ${textColor};
|
||||
color: ${pulseTextColor};
|
||||
}
|
||||
}
|
||||
`,
|
||||
stopButtonEnter: css`
|
||||
label: stopButtonEnter;
|
||||
width: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
`,
|
||||
stopButtonEnterActive: css`
|
||||
label: stopButtonEnterActive;
|
||||
opacity: 1;
|
||||
width: 32px;
|
||||
transition: opacity 500ms ease-in 50ms, width 500ms ease-in 50ms;
|
||||
`,
|
||||
stopButtonExit: css`
|
||||
label: stopButtonExit;
|
||||
width: 32px;
|
||||
opacity: 1;
|
||||
overflow: hidden;
|
||||
`,
|
||||
stopButtonExitActive: css`
|
||||
label: stopButtonExitActive;
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
transition: opacity 500ms ease-in 50ms, width 500ms ease-in 50ms;
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
@ -82,9 +108,9 @@ export function LiveTailButton(props: LiveTailButtonProps) {
|
||||
const onClickMain = isLive ? (isPaused ? resume : pause) : start;
|
||||
|
||||
return (
|
||||
<div className="explore-toolbar-content-item">
|
||||
<>
|
||||
<button
|
||||
className={classNames('btn navbar-button', {
|
||||
className={classNames('btn navbar-button', styles.liveButton, {
|
||||
[`btn--radius-right-0 ${styles.noRightBorderStyle}`]: isLive,
|
||||
[styles.isLive]: isLive && !isPaused,
|
||||
[styles.isPaused]: isLive && isPaused,
|
||||
@ -94,11 +120,24 @@ export function LiveTailButton(props: LiveTailButtonProps) {
|
||||
<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>
|
||||
<CSSTransition
|
||||
mountOnEnter={true}
|
||||
unmountOnExit={true}
|
||||
timeout={500}
|
||||
in={isLive}
|
||||
classNames={{
|
||||
enter: styles.stopButtonEnter,
|
||||
enterActive: styles.stopButtonEnterActive,
|
||||
exit: styles.stopButtonExit,
|
||||
exitActive: styles.stopButtonExitActive,
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<button className={`btn navbar-button navbar-button--attached ${styles.isLive}`} onClick={stop}>
|
||||
<i className={'fa fa-stop'} />
|
||||
</button>
|
||||
</div>
|
||||
</CSSTransition>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
import _ from 'lodash';
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
import {
|
||||
|
@ -27,6 +27,7 @@ import { deduplicatedLogsSelector, exploreItemUIStateSelector } from 'app/featur
|
||||
import { getTimeZone } from '../profile/state/selectors';
|
||||
import { LiveLogsWithTheme } from './LiveLogs';
|
||||
import { Logs } from './Logs';
|
||||
import { LogsCrossFadeTransition } from './utils/LogsCrossFadeTransition';
|
||||
|
||||
interface LogsContainerProps {
|
||||
datasourceInstance: DataSourceApi | null;
|
||||
@ -64,6 +65,7 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
|
||||
|
||||
onStopLive = () => {
|
||||
const { exploreId } = this.props;
|
||||
this.onPause();
|
||||
this.props.stopLive({ exploreId, refreshInterval: RefreshPicker.offOption.value });
|
||||
};
|
||||
|
||||
@ -116,43 +118,44 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
|
||||
isLive,
|
||||
} = this.props;
|
||||
|
||||
if (isLive) {
|
||||
return (
|
||||
<Collapse label="Logs" loading={false} isOpen>
|
||||
<LiveLogsWithTheme
|
||||
logsResult={logsResult}
|
||||
timeZone={timeZone}
|
||||
stopLive={this.onStopLive}
|
||||
isPaused={this.props.isPaused}
|
||||
onPause={this.onPause}
|
||||
onResume={this.onResume}
|
||||
/>
|
||||
</Collapse>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Collapse label="Logs" loading={loading} isOpen>
|
||||
<Logs
|
||||
dedupStrategy={this.props.dedupStrategy || LogsDedupStrategy.none}
|
||||
data={logsResult}
|
||||
dedupedData={dedupedResult}
|
||||
highlighterExpressions={logsHighlighterExpressions}
|
||||
loading={loading}
|
||||
onChangeTime={this.onChangeTime}
|
||||
onClickLabel={onClickLabel}
|
||||
onStartScanning={onStartScanning}
|
||||
onStopScanning={onStopScanning}
|
||||
onDedupStrategyChange={this.handleDedupStrategyChange}
|
||||
onToggleLogLevel={this.handleToggleLogLevel}
|
||||
absoluteRange={absoluteRange}
|
||||
timeZone={timeZone}
|
||||
scanning={scanning}
|
||||
scanRange={range.raw}
|
||||
width={width}
|
||||
getRowContext={this.getLogRowContext}
|
||||
/>
|
||||
</Collapse>
|
||||
<>
|
||||
<LogsCrossFadeTransition visible={isLive}>
|
||||
<Collapse label="Logs" loading={false} isOpen>
|
||||
<LiveLogsWithTheme
|
||||
logsResult={logsResult}
|
||||
timeZone={timeZone}
|
||||
stopLive={this.onStopLive}
|
||||
isPaused={this.props.isPaused}
|
||||
onPause={this.onPause}
|
||||
onResume={this.onResume}
|
||||
/>
|
||||
</Collapse>
|
||||
</LogsCrossFadeTransition>
|
||||
<LogsCrossFadeTransition visible={!isLive}>
|
||||
<Collapse label="Logs" loading={loading} isOpen>
|
||||
<Logs
|
||||
dedupStrategy={this.props.dedupStrategy || LogsDedupStrategy.none}
|
||||
data={logsResult}
|
||||
dedupedData={dedupedResult}
|
||||
highlighterExpressions={logsHighlighterExpressions}
|
||||
loading={loading}
|
||||
onChangeTime={this.onChangeTime}
|
||||
onClickLabel={onClickLabel}
|
||||
onStartScanning={onStartScanning}
|
||||
onStopScanning={onStopScanning}
|
||||
onDedupStrategyChange={this.handleDedupStrategyChange}
|
||||
onToggleLogLevel={this.handleToggleLogLevel}
|
||||
absoluteRange={absoluteRange}
|
||||
timeZone={timeZone}
|
||||
scanning={scanning}
|
||||
scanRange={range.raw}
|
||||
width={width}
|
||||
getRowContext={this.getLogRowContext}
|
||||
/>
|
||||
</Collapse>
|
||||
</LogsCrossFadeTransition>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -28,9 +28,19 @@ export const ResponsiveButton = (props: Props) => {
|
||||
onClick={onClick}
|
||||
disabled={disabled || false}
|
||||
>
|
||||
{iconClassName && iconSide === IconSide.left ? <i className={`${iconClassName}`} /> : null}
|
||||
{iconClassName && iconSide === IconSide.left ? (
|
||||
<>
|
||||
<i className={`${iconClassName}`} />
|
||||
|
||||
</>
|
||||
) : null}
|
||||
<span className="btn-title">{!splitted ? title : ''}</span>
|
||||
{iconClassName && iconSide === IconSide.right ? <i className={`${iconClassName}`} /> : null}
|
||||
{iconClassName && iconSide === IconSide.right ? (
|
||||
<>
|
||||
|
||||
<i className={`${iconClassName}`} />
|
||||
</>
|
||||
) : null}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { RefreshPicker } from '@grafana/ui';
|
||||
import memoizeOne from 'memoize-one';
|
||||
import { css } from 'emotion';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { ResponsiveButton } from './ResponsiveButton';
|
||||
|
||||
@ -33,7 +34,7 @@ export function RunButton(props: Props) {
|
||||
splitted={splitted}
|
||||
title="Run Query"
|
||||
onClick={onRun}
|
||||
buttonClassName="navbar-button--secondary btn--radius-right-0 "
|
||||
buttonClassName={classNames('navbar-button--secondary', { 'btn--radius-right-0': showDropdown })}
|
||||
iconClassName={loading ? 'fa fa-spinner fa-fw fa-spin run-icon' : 'fa fa-refresh fa-fw'}
|
||||
/>
|
||||
);
|
||||
|
@ -206,7 +206,7 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
|
||||
state: live ? LoadingState.Streaming : LoadingState.NotStarted,
|
||||
},
|
||||
isLive: live,
|
||||
isPaused: false,
|
||||
isPaused: live ? false : state.isPaused,
|
||||
loading: live,
|
||||
logsResult,
|
||||
};
|
||||
|
@ -0,0 +1,69 @@
|
||||
import React from 'react';
|
||||
import memoizeOne from 'memoize-one';
|
||||
import { css } from 'emotion';
|
||||
import { CSSTransition } from 'react-transition-group';
|
||||
|
||||
const transitionDuration = 500;
|
||||
// We add a bit of delay to the transition as another perf optimisation. As at the start we need to render
|
||||
// quite a bit of new rows, if we start transition at the same time there can be frame rate drop. This gives time
|
||||
// for react to first render them and then do the animation.
|
||||
const transitionDelay = 100;
|
||||
|
||||
const getStyles = memoizeOne(() => {
|
||||
return {
|
||||
logsEnter: css`
|
||||
label: logsEnter;
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
height: auto;
|
||||
width: auto;
|
||||
`,
|
||||
logsEnterActive: css`
|
||||
label: logsEnterActive;
|
||||
opacity: 1;
|
||||
transition: opacity ${transitionDuration}ms ease-out ${transitionDelay}ms;
|
||||
`,
|
||||
logsExit: css`
|
||||
label: logsExit;
|
||||
position: absolute;
|
||||
opacity: 1;
|
||||
height: auto;
|
||||
width: auto;
|
||||
`,
|
||||
logsExitActive: css`
|
||||
label: logsExitActive;
|
||||
opacity: 0;
|
||||
transition: opacity ${transitionDuration}ms ease-out ${transitionDelay}ms;
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
visible: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Cross fade transition component that is tied a bit too much to the logs containers so not very useful elsewhere
|
||||
* right now.
|
||||
*/
|
||||
export function LogsCrossFadeTransition(props: Props) {
|
||||
const { visible, children } = props;
|
||||
const styles = getStyles();
|
||||
return (
|
||||
<CSSTransition
|
||||
in={visible}
|
||||
mountOnEnter={true}
|
||||
unmountOnExit={true}
|
||||
timeout={transitionDuration + transitionDelay}
|
||||
classNames={{
|
||||
enter: styles.logsEnter,
|
||||
enterActive: styles.logsEnterActive,
|
||||
exit: styles.logsExit,
|
||||
exitActive: styles.logsExitActive,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</CSSTransition>
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue
Block a user