Commit bf24cbba by Andrej Ocenas Committed by GitHub

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

parent 9feac775
// 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,6 +304,7 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
</div>
{hasLiveOption && (
<div className={`explore-toolbar-content-item ${styles.liveTailButtons}`}>
<LiveTailButton
isLive={isLive}
isPaused={isPaused}
......@@ -301,6 +313,7 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
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';
import { GrafanaTheme, GrafanaThemeType, useTheme } from '@grafana/ui';
const orangeDark = '#FF780A';
const orangeDarkLighter = tinycolor(orangeDark)
const getStyles = memoizeOne((theme: GrafanaTheme) => {
const orange = theme.type === GrafanaThemeType.Dark ? '#FF780A' : '#ED5700';
const orangeLighter = tinycolor(orange)
.lighten(10)
.toString();
const orangeLight = '#ED5700';
const orangeLightLighter = tinycolor(orangeLight)
.lighten(10)
const pulseTextColor = tinycolor(orange)
.desaturate(90)
.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;
`,
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')} />
&nbsp; Live tailing
</button>
{isLive && (
<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>
</>
);
}
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,8 +118,9 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
isLive,
} = this.props;
if (isLive) {
return (
<>
<LogsCrossFadeTransition visible={isLive}>
<Collapse label="Logs" loading={false} isOpen>
<LiveLogsWithTheme
logsResult={logsResult}
......@@ -128,10 +131,8 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
onResume={this.onResume}
/>
</Collapse>
);
}
return (
</LogsCrossFadeTransition>
<LogsCrossFadeTransition visible={!isLive}>
<Collapse label="Logs" loading={loading} isOpen>
<Logs
dedupStrategy={this.props.dedupStrategy || LogsDedupStrategy.none}
......@@ -153,6 +154,8 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
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}`} />
&nbsp;
</>
) : null}
<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>
);
};
......@@ -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,
};
......
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