Commit bf24cbba by Andrej Ocenas Committed by GitHub

Explore: live tail UI fixes and improvements (#19187)

parent 9feac775
// Libraries // Libraries
import React, { ComponentClass } from 'react'; import React, { ComponentClass } from 'react';
import { hot } from 'react-hot-loader'; import { hot } from 'react-hot-loader';
import { css } from 'emotion';
// @ts-ignore // @ts-ignore
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { AutoSizer } from 'react-virtualized'; import { AutoSizer } from 'react-virtualized';
...@@ -52,6 +53,16 @@ import { ErrorContainer } from './ErrorContainer'; ...@@ -52,6 +53,16 @@ import { ErrorContainer } from './ErrorContainer';
import { scanStopAction } from './state/actionTypes'; import { scanStopAction } from './state/actionTypes';
import { ExploreGraphPanel } from './ExploreGraphPanel'; import { ExploreGraphPanel } from './ExploreGraphPanel';
const getStyles = memoizeOne(() => {
return {
logsMain: css`
label: logsMain;
// Is needed for some transition animations to work.
position: relative;
`,
};
});
interface ExploreProps { interface ExploreProps {
StartPage?: ComponentClass<ExploreStartPageProps>; StartPage?: ComponentClass<ExploreStartPageProps>;
changeSize: typeof changeSize; changeSize: typeof changeSize;
...@@ -257,6 +268,7 @@ export class Explore extends React.PureComponent<ExploreProps> { ...@@ -257,6 +268,7 @@ export class Explore extends React.PureComponent<ExploreProps> {
queryResponse, queryResponse,
} = this.props; } = this.props;
const exploreClass = split ? 'explore explore-split' : 'explore'; const exploreClass = split ? 'explore explore-split' : 'explore';
const styles = getStyles();
return ( return (
<div className={exploreClass} ref={this.getRef}> <div className={exploreClass} ref={this.getRef}>
...@@ -284,7 +296,7 @@ export class Explore extends React.PureComponent<ExploreProps> { ...@@ -284,7 +296,7 @@ export class Explore extends React.PureComponent<ExploreProps> {
} }
return ( return (
<main className="m-t-2" style={{ width }}> <main className={`m-t-2 ${styles.logsMain}`} style={{ width }}>
<ErrorBoundaryAlert> <ErrorBoundaryAlert>
{showingStartPage && ( {showingStartPage && (
<div className="grafana-info-box grafana-info-box--max-lg"> <div className="grafana-info-box grafana-info-box--max-lg">
......
...@@ -4,6 +4,7 @@ import { connect } from 'react-redux'; ...@@ -4,6 +4,7 @@ import { connect } from 'react-redux';
import { hot } from 'react-hot-loader'; import { hot } from 'react-hot-loader';
import memoizeOne from 'memoize-one'; import memoizeOne from 'memoize-one';
import classNames from 'classnames'; import classNames from 'classnames';
import { css } from 'emotion';
import { ExploreId, ExploreItemState, ExploreMode } from 'app/types/explore'; import { ExploreId, ExploreItemState, ExploreMode } from 'app/types/explore';
import { import {
...@@ -39,6 +40,14 @@ import { LiveTailButton } from './LiveTailButton'; ...@@ -39,6 +40,14 @@ import { LiveTailButton } from './LiveTailButton';
import { ResponsiveButton } from './ResponsiveButton'; import { ResponsiveButton } from './ResponsiveButton';
import { RunButton } from './RunButton'; import { RunButton } from './RunButton';
const getStyles = memoizeOne(() => {
return {
liveTailButtons: css`
margin-left: 10px;
`,
};
});
interface OwnProps { interface OwnProps {
exploreId: ExploreId; exploreId: ExploreId;
onChangeTime: (range: RawTimeRange, changedByScanner?: boolean) => void; onChangeTime: (range: RawTimeRange, changedByScanner?: boolean) => void;
...@@ -132,6 +141,7 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> { ...@@ -132,6 +141,7 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
stopLive = () => { stopLive = () => {
const { exploreId } = this.props; const { exploreId } = this.props;
this.pauseLive();
// TODO referencing this from perspective of refresh picker when there is designated button for it now is not // TODO referencing this from perspective of refresh picker when there is designated button for it now is not
// great. Needs another refactor. // great. Needs another refactor.
this.props.changeRefreshIntervalAction({ exploreId, refreshInterval: RefreshPicker.offOption.value }); this.props.changeRefreshIntervalAction({ exploreId, refreshInterval: RefreshPicker.offOption.value });
...@@ -174,6 +184,7 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> { ...@@ -174,6 +184,7 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
originPanelId, originPanelId,
} = this.props; } = this.props;
const styles = getStyles();
const originDashboardIsEditable = Number.isInteger(originPanelId); const originDashboardIsEditable = Number.isInteger(originPanelId);
const panelReturnClasses = classNames('btn', 'navbar-button', { const panelReturnClasses = classNames('btn', 'navbar-button', {
'btn--radius-right-0': originDashboardIsEditable, 'btn--radius-right-0': originDashboardIsEditable,
...@@ -293,14 +304,16 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> { ...@@ -293,14 +304,16 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
</div> </div>
{hasLiveOption && ( {hasLiveOption && (
<LiveTailButton <div className={`explore-toolbar-content-item ${styles.liveTailButtons}`}>
isLive={isLive} <LiveTailButton
isPaused={isPaused} isLive={isLive}
start={this.startLive} isPaused={isPaused}
pause={this.pauseLive} start={this.startLive}
resume={this.resumeLive} pause={this.pauseLive}
stop={this.stopLive} resume={this.resumeLive}
/> stop={this.stopLive}
/>
</div>
)} )}
</div> </div>
</div> </div>
......
...@@ -157,7 +157,7 @@ class LiveLogs extends PureComponent<Props, State> { ...@@ -157,7 +157,7 @@ class LiveLogs extends PureComponent<Props, State> {
const { logsRow, logsRowLocalTime, logsRowMessage } = getLogRowStyles(theme); const { logsRow, logsRowLocalTime, logsRowMessage } = getLogRowStyles(theme);
return ( return (
<> <div>
<div <div
onScroll={isPaused ? undefined : this.onScroll} onScroll={isPaused ? undefined : this.onScroll}
className={cx(['logs-rows', styles.logsRowsLive])} className={cx(['logs-rows', styles.logsRowsLive])}
...@@ -210,7 +210,7 @@ class LiveLogs extends PureComponent<Props, State> { ...@@ -210,7 +210,7 @@ class LiveLogs extends PureComponent<Props, State> {
</span> </span>
)} )}
</div> </div>
</> </div>
); );
} }
} }
......
...@@ -2,28 +2,30 @@ import React from 'react'; ...@@ -2,28 +2,30 @@ import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { css } from 'emotion'; import { css } from 'emotion';
import memoizeOne from 'memoize-one'; import memoizeOne from 'memoize-one';
import { GrafanaTheme, GrafanaThemeType, useTheme } from '@grafana/ui';
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
import { CSSTransition } from 'react-transition-group';
const orangeDark = '#FF780A'; import { GrafanaTheme, GrafanaThemeType, useTheme } from '@grafana/ui';
const orangeDarkLighter = tinycolor(orangeDark)
.lighten(10)
.toString();
const orangeLight = '#ED5700';
const orangeLightLighter = tinycolor(orangeLight)
.lighten(10)
.toString();
const getStyles = memoizeOne((theme: GrafanaTheme) => { const getStyles = memoizeOne((theme: GrafanaTheme) => {
const orange = theme.type === GrafanaThemeType.Dark ? orangeDark : orangeLight; const orange = theme.type === GrafanaThemeType.Dark ? '#FF780A' : '#ED5700';
const orangeLighter = theme.type === GrafanaThemeType.Dark ? orangeDarkLighter : orangeLightLighter; const orangeLighter = tinycolor(orange)
const textColor = theme.type === GrafanaThemeType.Dark ? theme.colors.white : theme.colors.black; .lighten(10)
.toString();
const pulseTextColor = tinycolor(orange)
.desaturate(90)
.toString();
return { return {
noRightBorderStyle: css` noRightBorderStyle: css`
label: noRightBorderStyle; label: noRightBorderStyle;
border-right: 0; border-right: 0;
`, `,
liveButton: css`
label: liveButton;
transition: background-color 1s, border-color 1s, color 1s;
margin: 0;
`,
isLive: css` isLive: css`
label: isLive; label: isLive;
border-color: ${orange}; border-color: ${orange};
...@@ -43,7 +45,7 @@ const getStyles = memoizeOne((theme: GrafanaTheme) => { ...@@ -43,7 +45,7 @@ const getStyles = memoizeOne((theme: GrafanaTheme) => {
label: isPaused; label: isPaused;
border-color: ${orange}; border-color: ${orange};
background: transparent; background: transparent;
animation: pulse 2s ease-out 0s infinite normal forwards; animation: pulse 3s ease-out 0s infinite normal forwards;
&:focus { &:focus {
border-color: ${orange}; border-color: ${orange};
} }
...@@ -53,16 +55,40 @@ const getStyles = memoizeOne((theme: GrafanaTheme) => { ...@@ -53,16 +55,40 @@ const getStyles = memoizeOne((theme: GrafanaTheme) => {
} }
@keyframes pulse { @keyframes pulse {
0% { 0% {
color: ${textColor}; color: ${pulseTextColor};
} }
50% { 50% {
color: ${orange}; color: ${orange};
} }
100% { 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) { ...@@ -82,9 +108,9 @@ export function LiveTailButton(props: LiveTailButtonProps) {
const onClickMain = isLive ? (isPaused ? resume : pause) : start; const onClickMain = isLive ? (isPaused ? resume : pause) : start;
return ( return (
<div className="explore-toolbar-content-item"> <>
<button <button
className={classNames('btn navbar-button', { className={classNames('btn navbar-button', styles.liveButton, {
[`btn--radius-right-0 ${styles.noRightBorderStyle}`]: isLive, [`btn--radius-right-0 ${styles.noRightBorderStyle}`]: isLive,
[styles.isLive]: isLive && !isPaused, [styles.isLive]: isLive && !isPaused,
[styles.isPaused]: isLive && isPaused, [styles.isPaused]: isLive && isPaused,
...@@ -94,11 +120,24 @@ export function LiveTailButton(props: LiveTailButtonProps) { ...@@ -94,11 +120,24 @@ export function LiveTailButton(props: LiveTailButtonProps) {
<i className={classNames('fa', isPaused || !isLive ? 'fa-play' : 'fa-pause')} /> <i className={classNames('fa', isPaused || !isLive ? 'fa-play' : 'fa-pause')} />
&nbsp; Live tailing &nbsp; Live tailing
</button> </button>
{isLive && ( <CSSTransition
<button className={`btn navbar-button navbar-button--attached ${styles.isLive}`} onClick={stop}> mountOnEnter={true}
<i className={'fa fa-stop'} /> unmountOnExit={true}
</button> timeout={500}
)} in={isLive}
</div> 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>
</>
); );
} }
import _ from 'lodash';
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { import {
......
...@@ -27,6 +27,7 @@ import { deduplicatedLogsSelector, exploreItemUIStateSelector } from 'app/featur ...@@ -27,6 +27,7 @@ import { deduplicatedLogsSelector, exploreItemUIStateSelector } from 'app/featur
import { getTimeZone } from '../profile/state/selectors'; import { getTimeZone } from '../profile/state/selectors';
import { LiveLogsWithTheme } from './LiveLogs'; import { LiveLogsWithTheme } from './LiveLogs';
import { Logs } from './Logs'; import { Logs } from './Logs';
import { LogsCrossFadeTransition } from './utils/LogsCrossFadeTransition';
interface LogsContainerProps { interface LogsContainerProps {
datasourceInstance: DataSourceApi | null; datasourceInstance: DataSourceApi | null;
...@@ -64,6 +65,7 @@ export class LogsContainer extends PureComponent<LogsContainerProps> { ...@@ -64,6 +65,7 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
onStopLive = () => { onStopLive = () => {
const { exploreId } = this.props; const { exploreId } = this.props;
this.onPause();
this.props.stopLive({ exploreId, refreshInterval: RefreshPicker.offOption.value }); this.props.stopLive({ exploreId, refreshInterval: RefreshPicker.offOption.value });
}; };
...@@ -116,43 +118,44 @@ export class LogsContainer extends PureComponent<LogsContainerProps> { ...@@ -116,43 +118,44 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
isLive, isLive,
} = this.props; } = 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 ( return (
<Collapse label="Logs" loading={loading} isOpen> <>
<Logs <LogsCrossFadeTransition visible={isLive}>
dedupStrategy={this.props.dedupStrategy || LogsDedupStrategy.none} <Collapse label="Logs" loading={false} isOpen>
data={logsResult} <LiveLogsWithTheme
dedupedData={dedupedResult} logsResult={logsResult}
highlighterExpressions={logsHighlighterExpressions} timeZone={timeZone}
loading={loading} stopLive={this.onStopLive}
onChangeTime={this.onChangeTime} isPaused={this.props.isPaused}
onClickLabel={onClickLabel} onPause={this.onPause}
onStartScanning={onStartScanning} onResume={this.onResume}
onStopScanning={onStopScanning} />
onDedupStrategyChange={this.handleDedupStrategyChange} </Collapse>
onToggleLogLevel={this.handleToggleLogLevel} </LogsCrossFadeTransition>
absoluteRange={absoluteRange} <LogsCrossFadeTransition visible={!isLive}>
timeZone={timeZone} <Collapse label="Logs" loading={loading} isOpen>
scanning={scanning} <Logs
scanRange={range.raw} dedupStrategy={this.props.dedupStrategy || LogsDedupStrategy.none}
width={width} data={logsResult}
getRowContext={this.getLogRowContext} dedupedData={dedupedResult}
/> highlighterExpressions={logsHighlighterExpressions}
</Collapse> 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) => { ...@@ -28,9 +28,19 @@ export const ResponsiveButton = (props: Props) => {
onClick={onClick} onClick={onClick}
disabled={disabled || false} disabled={disabled || false}
> >
{iconClassName && iconSide === IconSide.left ? <i className={`${iconClassName}`} /> : null} {iconClassName && iconSide === IconSide.left ? (
<>
<i className={`${iconClassName}`} />
&nbsp;
</>
) : null}
<span className="btn-title">{!splitted ? title : ''}</span> <span className="btn-title">{!splitted ? title : ''}</span>
{iconClassName && iconSide === IconSide.right ? <i className={`${iconClassName}`} /> : null} {iconClassName && iconSide === IconSide.right ? (
<>
&nbsp;
<i className={`${iconClassName}`} />
</>
) : null}
</button> </button>
); );
}; };
...@@ -2,6 +2,7 @@ import React from 'react'; ...@@ -2,6 +2,7 @@ import React from 'react';
import { RefreshPicker } from '@grafana/ui'; import { RefreshPicker } from '@grafana/ui';
import memoizeOne from 'memoize-one'; import memoizeOne from 'memoize-one';
import { css } from 'emotion'; import { css } from 'emotion';
import classNames from 'classnames';
import { ResponsiveButton } from './ResponsiveButton'; import { ResponsiveButton } from './ResponsiveButton';
...@@ -33,7 +34,7 @@ export function RunButton(props: Props) { ...@@ -33,7 +34,7 @@ export function RunButton(props: Props) {
splitted={splitted} splitted={splitted}
title="Run Query" title="Run Query"
onClick={onRun} 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'} 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 ...@@ -206,7 +206,7 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
state: live ? LoadingState.Streaming : LoadingState.NotStarted, state: live ? LoadingState.Streaming : LoadingState.NotStarted,
}, },
isLive: live, isLive: live,
isPaused: false, isPaused: live ? false : state.isPaused,
loading: live, loading: live,
logsResult, logsResult,
}; };
......
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>
);
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment