From bf24cbba76f6ef59146994d1f52fbc0a33e9e767 Mon Sep 17 00:00:00 2001 From: Andrej Ocenas Date: Fri, 20 Sep 2019 13:00:11 +0200 Subject: [PATCH] Explore: live tail UI fixes and improvements (#19187) --- public/app/features/explore/Explore.tsx | 14 ++- .../app/features/explore/ExploreToolbar.tsx | 29 +++++-- public/app/features/explore/LiveLogs.tsx | 4 +- .../app/features/explore/LiveTailButton.tsx | 85 ++++++++++++++----- public/app/features/explore/Logs.tsx | 1 - public/app/features/explore/LogsContainer.tsx | 75 ++++++++-------- .../app/features/explore/ResponsiveButton.tsx | 14 ++- public/app/features/explore/RunButton.tsx | 3 +- public/app/features/explore/state/reducers.ts | 2 +- .../explore/utils/LogsCrossFadeTransition.tsx | 69 +++++++++++++++ 10 files changed, 221 insertions(+), 75 deletions(-) create mode 100644 public/app/features/explore/utils/LogsCrossFadeTransition.tsx diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 3adcd5221d0..03ae6f32f23 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -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; changeSize: typeof changeSize; @@ -257,6 +268,7 @@ export class Explore extends React.PureComponent { queryResponse, } = this.props; const exploreClass = split ? 'explore explore-split' : 'explore'; + const styles = getStyles(); return (
@@ -284,7 +296,7 @@ export class Explore extends React.PureComponent { } return ( -
+
{showingStartPage && (
diff --git a/public/app/features/explore/ExploreToolbar.tsx b/public/app/features/explore/ExploreToolbar.tsx index 1687e1b0475..81f463adac1 100644 --- a/public/app/features/explore/ExploreToolbar.tsx +++ b/public/app/features/explore/ExploreToolbar.tsx @@ -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 { 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 { 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 {
{hasLiveOption && ( - +
+ +
)}
diff --git a/public/app/features/explore/LiveLogs.tsx b/public/app/features/explore/LiveLogs.tsx index 49312f6e8ff..2bafb9694a0 100644 --- a/public/app/features/explore/LiveLogs.tsx +++ b/public/app/features/explore/LiveLogs.tsx @@ -157,7 +157,7 @@ class LiveLogs extends PureComponent { const { logsRow, logsRowLocalTime, logsRowMessage } = getLogRowStyles(theme); return ( - <> +
{ )}
- +
); } } diff --git a/public/app/features/explore/LiveTailButton.tsx b/public/app/features/explore/LiveTailButton.tsx index 25897536b71..06b8ef996cf 100644 --- a/public/app/features/explore/LiveTailButton.tsx +++ b/public/app/features/explore/LiveTailButton.tsx @@ -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 ( -
+ <> - {isLive && ( - - )} -
+ +
+ +
+
+ ); } diff --git a/public/app/features/explore/Logs.tsx b/public/app/features/explore/Logs.tsx index 576f19a603a..b101ae05c4d 100644 --- a/public/app/features/explore/Logs.tsx +++ b/public/app/features/explore/Logs.tsx @@ -1,4 +1,3 @@ -import _ from 'lodash'; import React, { PureComponent } from 'react'; import { diff --git a/public/app/features/explore/LogsContainer.tsx b/public/app/features/explore/LogsContainer.tsx index 9d780b05d57..4893b5d156c 100644 --- a/public/app/features/explore/LogsContainer.tsx +++ b/public/app/features/explore/LogsContainer.tsx @@ -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 { onStopLive = () => { const { exploreId } = this.props; + this.onPause(); this.props.stopLive({ exploreId, refreshInterval: RefreshPicker.offOption.value }); }; @@ -116,43 +118,44 @@ export class LogsContainer extends PureComponent { isLive, } = this.props; - if (isLive) { - return ( - - - - ); - } - return ( - - - + <> + + + + + + + + + + + ); } } diff --git a/public/app/features/explore/ResponsiveButton.tsx b/public/app/features/explore/ResponsiveButton.tsx index 33aa2fedebf..c31200df109 100644 --- a/public/app/features/explore/ResponsiveButton.tsx +++ b/public/app/features/explore/ResponsiveButton.tsx @@ -28,9 +28,19 @@ export const ResponsiveButton = (props: Props) => { onClick={onClick} disabled={disabled || false} > - {iconClassName && iconSide === IconSide.left ? : null} + {iconClassName && iconSide === IconSide.left ? ( + <> + +   + + ) : null} {!splitted ? title : ''} - {iconClassName && iconSide === IconSide.right ? : null} + {iconClassName && iconSide === IconSide.right ? ( + <> +   + + + ) : null} ); }; diff --git a/public/app/features/explore/RunButton.tsx b/public/app/features/explore/RunButton.tsx index b704d584cba..9c2c3fe3230 100644 --- a/public/app/features/explore/RunButton.tsx +++ b/public/app/features/explore/RunButton.tsx @@ -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'} /> ); diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts index 899c60eec26..d687435cb72 100644 --- a/public/app/features/explore/state/reducers.ts +++ b/public/app/features/explore/state/reducers.ts @@ -206,7 +206,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta state: live ? LoadingState.Streaming : LoadingState.NotStarted, }, isLive: live, - isPaused: false, + isPaused: live ? false : state.isPaused, loading: live, logsResult, }; diff --git a/public/app/features/explore/utils/LogsCrossFadeTransition.tsx b/public/app/features/explore/utils/LogsCrossFadeTransition.tsx new file mode 100644 index 00000000000..d4ba955601d --- /dev/null +++ b/public/app/features/explore/utils/LogsCrossFadeTransition.tsx @@ -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 ( + + {children} + + ); +}