diff --git a/public/app/features/explore/LiveLogs.tsx b/public/app/features/explore/LiveLogs.tsx index 72acc6edecb..1d2a6cc9009 100644 --- a/public/app/features/explore/LiveLogs.tsx +++ b/public/app/features/explore/LiveLogs.tsx @@ -1,6 +1,6 @@ import React, { PureComponent } from 'react'; import { css, cx } from 'emotion'; -import { Themeable, withTheme, GrafanaTheme, selectThemeVariant, LinkButton, getLogRowStyles } from '@grafana/ui'; +import { Themeable, withTheme, GrafanaTheme, selectThemeVariant, getLogRowStyles } from '@grafana/ui'; import { LogsModel, LogRowModel, TimeZone } from '@grafana/data'; @@ -32,34 +32,107 @@ const getStyles = (theme: GrafanaTheme) => ({ display: flex; align-items: center; `, + button: css` + margin-right: ${theme.spacing.sm}; + `, }); export interface Props extends Themeable { logsResult?: LogsModel; timeZone: TimeZone; stopLive: () => void; + onPause: () => void; + onResume: () => void; + isPaused: boolean; } -class LiveLogs extends PureComponent { +interface State { + logsResultToRender?: LogsModel; +} + +class LiveLogs extends PureComponent { private liveEndDiv: HTMLDivElement = null; + private scrollContainerRef = React.createRef(); + private lastScrollPos: number | null = null; + + constructor(props: Props) { + super(props); + this.state = { + logsResultToRender: props.logsResult, + }; + } componentDidUpdate(prevProps: Props) { - if (this.liveEndDiv) { - this.liveEndDiv.scrollIntoView(false); + if (!prevProps.isPaused && this.props.isPaused) { + // So we paused the view and we changed the content size, but we want to keep the relative offset from the bottom. + if (this.lastScrollPos) { + // There is last scroll pos from when user scrolled up a bit so go to that position. + const { clientHeight, scrollHeight } = this.scrollContainerRef.current; + const scrollTop = scrollHeight - (this.lastScrollPos + clientHeight); + this.scrollContainerRef.current.scrollTo(0, scrollTop); + this.lastScrollPos = null; + } else { + // We do not have any position to jump to su the assumption is user just clicked pause. We can just scroll + // to the bottom. + if (this.liveEndDiv) { + this.liveEndDiv.scrollIntoView(false); + } + } } } + static getDerivedStateFromProps(nextProps: Props) { + if (!nextProps.isPaused) { + return { + // We update what we show only if not paused. We keep any background subscriptions running and keep updating + // our state, but we do not show the updates, this allows us start again showing correct result after resuming + // without creating a gap in the log results. + logsResultToRender: nextProps.logsResult, + }; + } else { + return null; + } + } + + /** + * Handle pausing when user scrolls up so that we stop resetting his position to the bottom when new row arrives. + * We do not need to throttle it here much, adding new rows should be throttled/buffered itself in the query epics + * and after you pause we remove the handler and add it after you manually resume, so this should not be fired often. + */ + onScroll = (event: React.SyntheticEvent) => { + const { isPaused, onPause } = this.props; + const { scrollTop, clientHeight, scrollHeight } = event.currentTarget; + const distanceFromBottom = scrollHeight - (scrollTop + clientHeight); + if (distanceFromBottom >= 5 && !isPaused) { + onPause(); + this.lastScrollPos = distanceFromBottom; + } + }; + + rowsToRender = () => { + const { isPaused } = this.props; + let rowsToRender: LogRowModel[] = this.state.logsResultToRender ? this.state.logsResultToRender.rows : []; + if (!isPaused) { + // A perf optimisation here. Show just 100 rows when streaming and full length when the streaming is paused. + rowsToRender = rowsToRender.slice(-100); + } + return rowsToRender; + }; + render() { - const { theme, timeZone } = this.props; + const { theme, timeZone, onPause, onResume, isPaused } = this.props; const styles = getStyles(theme); - const rowsToRender: LogRowModel[] = this.props.logsResult ? this.props.logsResult.rows : []; const showUtc = timeZone === 'utc'; const { logsRow, logsRowLocalTime, logsRowMessage } = getLogRowStyles(theme); return ( <> -
- {rowsToRender.map((row: any, index) => { +
+ {this.rowsToRender().map((row: any, index) => { return (
{
{ this.liveEndDiv = element; - if (this.liveEndDiv) { + // This is triggered on every update so on every new row. It keeps the view scrolled at the bottom by + // default. + if (this.liveEndDiv && !isPaused) { this.liveEndDiv.scrollIntoView(false); } }} />
- - Last line received: ago - - - Stop Live - + + + {isPaused || ( + + Last line received: ago + + )}
); diff --git a/public/app/features/explore/LogsContainer.tsx b/public/app/features/explore/LogsContainer.tsx index a8baf98cca4..42d61d9b8bd 100644 --- a/public/app/features/explore/LogsContainer.tsx +++ b/public/app/features/explore/LogsContainer.tsx @@ -18,7 +18,11 @@ import { ExploreId, ExploreItemState } from 'app/types/explore'; import { StoreState } from 'app/types'; import { changeDedupStrategy, updateTimeRange } from './state/actions'; -import { toggleLogLevelAction, changeRefreshIntervalAction } from 'app/features/explore/state/actionTypes'; +import { + toggleLogLevelAction, + changeRefreshIntervalAction, + setPausedStateAction, +} from 'app/features/explore/state/actionTypes'; import { deduplicatedLogsSelector, exploreItemUIStateSelector } from 'app/features/explore/state/selectors'; import { getTimeZone } from '../profile/state/selectors'; import { LiveLogsWithTheme } from './LiveLogs'; @@ -48,6 +52,8 @@ interface LogsContainerProps { updateTimeRange: typeof updateTimeRange; range: TimeRange; absoluteRange: AbsoluteTimeRange; + setPausedStateAction: typeof setPausedStateAction; + isPaused: boolean; } export class LogsContainer extends PureComponent { @@ -62,6 +68,16 @@ export class LogsContainer extends PureComponent { this.props.stopLive({ exploreId, refreshInterval: offOption.value }); }; + onPause = () => { + const { exploreId } = this.props; + this.props.setPausedStateAction({ exploreId, isPaused: true }); + }; + + onResume = () => { + const { exploreId } = this.props; + this.props.setPausedStateAction({ exploreId, isPaused: false }); + }; + handleDedupStrategyChange = (dedupStrategy: LogsDedupStrategy) => { this.props.changeDedupStrategy(this.props.exploreId, dedupStrategy); }; @@ -104,7 +120,14 @@ export class LogsContainer extends PureComponent { if (isLive) { return ( - + ); } @@ -146,6 +169,7 @@ function mapStateToProps(state: StoreState, { exploreId }: { exploreId: string } scanning, datasourceInstance, isLive, + isPaused, range, absoluteRange, } = item; @@ -163,6 +187,7 @@ function mapStateToProps(state: StoreState, { exploreId }: { exploreId: string } dedupedResult, datasourceInstance, isLive, + isPaused, range, absoluteRange, }; @@ -173,6 +198,7 @@ const mapDispatchToProps = { toggleLogLevelAction, stopLive: changeRefreshIntervalAction, updateTimeRange, + setPausedStateAction, }; export default hot(module)( diff --git a/public/app/features/explore/state/actionTypes.ts b/public/app/features/explore/state/actionTypes.ts index a05bb3bfb55..65d34a61850 100644 --- a/public/app/features/explore/state/actionTypes.ts +++ b/public/app/features/explore/state/actionTypes.ts @@ -197,6 +197,11 @@ export interface ChangeLoadingStatePayload { loadingState: LoadingState; } +export interface SetPausedStatePayload { + exploreId: ExploreId; + isPaused: boolean; +} + /** * Adds a query row after the row with the given index. */ @@ -371,6 +376,8 @@ export const changeLoadingStateAction = actionCreatorFactory('explore/SET_PAUSED_STATE').create(); + export type HigherOrderAction = | ActionOf | SplitOpenAction diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts index b775fd1f5e9..577cf90357e 100644 --- a/public/app/features/explore/state/reducers.ts +++ b/public/app/features/explore/state/reducers.ts @@ -52,6 +52,7 @@ import { queryEndedAction, queryStreamUpdatedAction, QueryEndedPayload, + setPausedStateAction, } from './actionTypes'; import { reducerFactory, ActionOf } from 'app/core/redux'; import { updateLocation } from 'app/core/actions/location'; @@ -114,6 +115,7 @@ export const makeExploreItemState = (): ExploreItemState => ({ supportedModes: [], mode: null, isLive: false, + isPaused: false, urlReplaced: false, queryState: new PanelQueryState(), queryResponse: createEmptyQueryResponse(), @@ -209,6 +211,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta state: live ? LoadingState.Streaming : LoadingState.NotStarted, }, isLive: live, + isPaused: false, loading: live, logsResult, }; @@ -552,6 +555,16 @@ export const itemReducer = reducerFactory({} as ExploreItemSta }; }, }) + .addMapper({ + filter: setPausedStateAction, + mapper: (state, action): ExploreItemState => { + const { isPaused } = action.payload; + return { + ...state, + isPaused: isPaused, + }; + }, + }) .addMapper({ //queryStreamUpdatedAction filter: queryEndedAction, diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index 8723fa202bb..ba7db9bad58 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -251,7 +251,15 @@ export interface ExploreItemState { supportedModes: ExploreMode[]; mode: ExploreMode; + /** + * If true, the view is in live tailing mode. + */ isLive: boolean; + + /** + * If true, the live tailing view is paused. + */ + isPaused: boolean; urlReplaced: boolean; queryState: PanelQueryState;