Skip to content

Commit

Permalink
[Logs UI] Improve live streaming behavior when scrolling (elastic#44923)
Browse files Browse the repository at this point in the history
* [Logs UI] Stop live streaming on scroll or minimap click

* Silently pause streaming on scroll up

* Fix type checking

* Fix type check again

* Fix i18n
  • Loading branch information
Zacqary committed Sep 9, 2019
1 parent 22825ef commit a21b985
Show file tree
Hide file tree
Showing 14 changed files with 225 additions and 31 deletions.
1 change: 1 addition & 0 deletions x-pack/legacy/plugins/infra/common/time/time_key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface TimeKey {
time: number;
tiebreaker: number;
gid?: string;
fromAutoReload?: boolean;
}

export interface UniqueTimeKey extends TimeKey {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

/* eslint-disable max-classes-per-file */

import { EuiButtonEmpty, EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import * as React from 'react';

import euiStyled from '../../../../../../common/eui_styled_components';

interface LogTextStreamJumpToTailProps {
onClickJump?: () => void;
width: number;
}

export class LogTextStreamJumpToTail extends React.PureComponent<LogTextStreamJumpToTailProps> {
public render() {
const { onClickJump, width } = this.props;
return (
<JumpToTailWrapper width={width}>
<MessageWrapper>
<EuiText color="subdued">
<FormattedMessage
id="xpack.infra.logs.streamingNewEntriesText"
defaultMessage="Streaming new entries"
/>
</EuiText>
</MessageWrapper>
<EuiButtonEmpty size="xs" onClick={onClickJump} iconType="arrowDown">
<FormattedMessage
id="xpack.infra.logs.jumpToTailText"
defaultMessage="Jump to most recent entries"
/>
</EuiButtonEmpty>
</JumpToTailWrapper>
);
}
}

const JumpToTailWrapper = euiStyled.div<{ width: number }>`
align-items: center;
display: flex;
min-height: ${props => props.theme.eui.euiSizeXXL};
width: ${props => props.width}px;
position: fixed;
bottom: 0;
background-color: ${props => props.theme.eui.euiColorEmptyShade};
`;

const MessageWrapper = euiStyled.div`
padding: 8px 16px;
`;
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { InfraLoadingPanel } from '../../loading';
import { getStreamItemBeforeTimeKey, getStreamItemId, parseStreamItemId, StreamItem } from './item';
import { LogColumnHeaders } from './column_headers';
import { LogTextStreamLoadingItemView } from './loading_item_view';
import { LogTextStreamJumpToTail } from './jump_to_tail';
import { LogEntryRow } from './log_entry_row';
import { MeasurableItemView } from './measurable_item_view';
import { VerticalScrollPanel } from './vertical_scroll_panel';
Expand Down Expand Up @@ -52,11 +53,17 @@ interface ScrollableLogTextStreamViewProps {
intl: InjectedIntl;
highlightedItem: string | null;
currentHighlightKey: UniqueTimeKey | null;
scrollLock: {
enable: () => void;
disable: () => void;
isEnabled: boolean;
};
}

interface ScrollableLogTextStreamViewState {
target: TimeKey | null;
targetId: string | null;
items: StreamItem[];
}

class ScrollableLogTextStreamViewClass extends React.PureComponent<
Expand All @@ -70,30 +77,42 @@ class ScrollableLogTextStreamViewClass extends React.PureComponent<
const hasNewTarget = nextProps.target && nextProps.target !== prevState.target;
const hasItems = nextProps.items.length > 0;

// Prevent new entries from being appended and moving the stream forward when
// the user has scrolled up during live streaming
const nextItems =
hasItems && nextProps.scrollLock.isEnabled ? prevState.items : nextProps.items;

if (nextProps.isStreaming && hasItems) {
return {
target: nextProps.target,
targetId: getStreamItemId(nextProps.items[nextProps.items.length - 1]),
items: nextItems,
};
} else if (hasNewTarget && hasItems) {
return {
target: nextProps.target,
targetId: getStreamItemId(getStreamItemBeforeTimeKey(nextProps.items, nextProps.target!)),
items: nextItems,
};
} else if (!nextProps.target || !hasItems) {
return {
target: null,
targetId: null,
items: [],
};
}

return null;
}

public readonly state = {
target: null,
targetId: null,
};
constructor(props: ScrollableLogTextStreamViewProps) {
super(props);
this.state = {
target: null,
targetId: null,
items: props.items,
};
}

public render() {
const {
Expand All @@ -106,14 +125,13 @@ class ScrollableLogTextStreamViewClass extends React.PureComponent<
isLoadingMore,
isReloading,
isStreaming,
items,
lastLoadedTime,
scale,
wrap,
scrollLock,
} = this.props;
const { targetId } = this.state;
const { targetId, items } = this.state;
const hasItems = items.length > 0;

return (
<ScrollableLogTextStreamViewWrapper>
{isReloading && !hasItems ? (
Expand Down Expand Up @@ -163,6 +181,7 @@ class ScrollableLogTextStreamViewClass extends React.PureComponent<
target={targetId}
hideScrollbar={true}
data-test-subj={'logStream'}
isLocked={scrollLock.isEnabled}
>
{registerChild => (
<>
Expand Down Expand Up @@ -210,6 +229,12 @@ class ScrollableLogTextStreamViewClass extends React.PureComponent<
lastStreamingUpdate={isStreaming ? lastLoadedTime : null}
onLoadMore={this.handleLoadNewerItems}
/>
{scrollLock.isEnabled && (
<LogTextStreamJumpToTail
width={width}
onClickJump={this.handleJumpToTail}
/>
)}
</>
)}
</VerticalScrollPanel>
Expand Down Expand Up @@ -263,6 +288,9 @@ class ScrollableLogTextStreamViewClass extends React.PureComponent<
pagesBelow: number;
fromScroll: boolean;
}) => {
if (fromScroll && this.props.isStreaming) {
this.props.scrollLock[pagesBelow === 0 ? 'disable' : 'enable']();
}
this.props.reportVisibleInterval({
endKey: parseStreamItemId(bottomChild),
middleKey: parseStreamItemId(middleChild),
Expand All @@ -273,6 +301,15 @@ class ScrollableLogTextStreamViewClass extends React.PureComponent<
});
}
);

private handleJumpToTail = () => {
const { items, scrollLock } = this.props;
scrollLock.disable();
const lastItemTarget = getStreamItemId(items[items.length - 1]);
this.setState({
targetId: lastItemTarget,
});
};
}

export const ScrollableLogTextStreamView = injectI18n(ScrollableLogTextStreamViewClass);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ interface VerticalScrollPanelProps<Child> {
width: number;
hideScrollbar?: boolean;
'data-test-subj'?: string;
isLocked: boolean;
}

interface VerticalScrollPanelSnapshot<Child> {
Expand Down Expand Up @@ -217,7 +218,16 @@ export class VerticalScrollPanel<Child> extends React.PureComponent<
prevState: {},
snapshot: VerticalScrollPanelSnapshot<Child>
) {
this.handleUpdatedChildren(snapshot.scrollTarget, snapshot.scrollOffset);
if (
prevProps.height !== this.props.height ||
prevProps.target !== this.props.target ||
React.Children.count(prevProps.children) !== React.Children.count(this.props.children)
) {
this.handleUpdatedChildren(snapshot.scrollTarget, snapshot.scrollOffset);
}
if (prevProps.isLocked && !this.props.isLocked && this.scrollRef.current) {
this.scrollRef.current.scrollTop = this.scrollRef.current.scrollHeight;
}
}

public componentWillUnmount() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const noop = () => undefined;

interface LogTimeControlsProps {
currentTime: number | null;
startLiveStreaming: (interval: number) => any;
startLiveStreaming: () => any;
stopLiveStreaming: () => any;
isLiveStreaming: boolean;
jumpToTime: (time: number) => any;
Expand All @@ -25,7 +25,6 @@ class LogTimeControlsUI extends React.PureComponent<LogTimeControlsProps> {
const { currentTime, isLiveStreaming, intl } = this.props;

const currentMoment = currentTime ? moment(currentTime) : null;

if (isLiveStreaming) {
return (
<EuiFlexGroup gutterSize="s">
Expand Down Expand Up @@ -89,7 +88,7 @@ class LogTimeControlsUI extends React.PureComponent<LogTimeControlsProps> {
};

private startLiveStreaming = () => {
this.props.startLiveStreaming(5000);
this.props.startLiveStreaming();
};

private stopLiveStreaming = () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const withLogPosition = connect(
(state: State) => ({
firstVisiblePosition: logPositionSelectors.selectFirstVisiblePosition(state),
isAutoReloading: logPositionSelectors.selectIsAutoReloading(state),
isScrollLocked: logPositionSelectors.selectAutoReloadScrollLock(state),
lastVisiblePosition: logPositionSelectors.selectFirstVisiblePosition(state),
targetPosition: logPositionSelectors.selectTargetPosition(state),
urlState: selectPositionUrlState(state),
Expand All @@ -31,6 +32,8 @@ export const withLogPosition = connect(
reportVisiblePositions: logPositionActions.reportVisiblePositions,
startLiveStreaming: logPositionActions.startAutoReload,
stopLiveStreaming: logPositionActions.stopAutoReload,
scrollLockLiveStreaming: logPositionActions.lockAutoReloadScroll,
scrollUnlockLiveStreaming: logPositionActions.unlockAutoReloadScroll,
})
);

Expand Down Expand Up @@ -65,7 +68,7 @@ export const WithLogPositionUrlState = () => (
jumpToTargetPosition(newUrlState.position);
}
if (newUrlState && newUrlState.streamLive) {
startLiveStreaming(5000);
startLiveStreaming();
} else if (
newUrlState &&
typeof newUrlState.streamLive !== 'undefined' &&
Expand All @@ -81,7 +84,7 @@ export const WithLogPositionUrlState = () => (
jumpToTargetPositionTime(Date.now());
}
if (initialUrlState && initialUrlState.streamLive) {
startLiveStreaming(5000);
startLiveStreaming();
}
}}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const withStreamItems = connect(
isAutoReloading: logPositionSelectors.selectIsAutoReloading(state),
isReloading: logEntriesSelectors.selectIsReloadingEntries(state),
isLoadingMore: logEntriesSelectors.selectIsLoadingMoreEntries(state),
wasAutoReloadJustAborted: logPositionSelectors.selectAutoReloadJustAborted(state),
hasMoreBeforeStart: logEntriesSelectors.selectHasMoreBeforeStart(state),
hasMoreAfterEnd: logEntriesSelectors.selectHasMoreAfterEnd(state),
lastLoadedTime: logEntriesSelectors.selectEntriesLastLoadedTime(state),
Expand Down Expand Up @@ -54,12 +55,19 @@ export const WithStreamItems = withStreamItems(
const { currentHighlightKey, logEntryHighlightsById } = useContext(LogHighlightsState.Context);
const items = useMemo(
() =>
props.isReloading && !props.isAutoReloading
props.isReloading && !props.isAutoReloading && !props.wasAutoReloadJustAborted
? []
: props.entries.map(logEntry =>
createLogEntryStreamItem(logEntry, logEntryHighlightsById[logEntry.gid] || [])
),
[props.isReloading, props.isAutoReloading, props.entries, logEntryHighlightsById]

[
props.isReloading,
props.isAutoReloading,
props.wasAutoReloadJustAborted,
props.entries,
logEntryHighlightsById,
]
);

useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,15 @@ export const LogsPageLogsContent: React.FunctionComponent = () => {
</WithLogFilter>
<PageContent key={`${sourceId}-${version}`}>
<WithLogPosition>
{({ isAutoReloading, jumpToTargetPosition, reportVisiblePositions, targetPosition }) => (
{({
isAutoReloading,
jumpToTargetPosition,
reportVisiblePositions,
targetPosition,
scrollLockLiveStreaming,
scrollUnlockLiveStreaming,
isScrollLocked,
}) => (
<WithStreamItems initializeOnMount={!isAutoReloading}>
{({
currentHighlightKey,
Expand Down Expand Up @@ -109,6 +117,11 @@ export const LogsPageLogsContent: React.FunctionComponent = () => {
setFlyoutVisibility={setFlyoutVisibility}
highlightedItem={surroundingLogsId ? surroundingLogsId : null}
currentHighlightKey={currentHighlightKey}
scrollLock={{
enable: scrollLockLiveStreaming,
disable: scrollUnlockLiveStreaming,
isEnabled: isScrollLocked,
}}
/>
)}
</WithStreamItems>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,8 @@ export const LogsToolbar = injectI18n(({ intl }) => {
currentTime={visibleMidpointTime}
isLiveStreaming={isAutoReloading}
jumpToTime={jumpToTargetPositionTime}
startLiveStreaming={interval => {
startLiveStreaming(interval);
startLiveStreaming={() => {
startLiveStreaming();
setSurroundingLogsId(null);
}}
stopLiveStreaming={stopLiveStreaming}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ const actionCreator = actionCreatorFactory('x-pack/infra/local/log_position');

export const jumpToTargetPosition = actionCreator<TimeKey>('JUMP_TO_TARGET_POSITION');

export const jumpToTargetPositionTime = (time: number) =>
export const jumpToTargetPositionTime = (time: number, fromAutoReload: boolean = false) =>
jumpToTargetPosition({
tiebreaker: 0,
time,
fromAutoReload,
});

export interface ReportVisiblePositionsPayload {
Expand All @@ -31,6 +32,8 @@ export const reportVisiblePositions = actionCreator<ReportVisiblePositionsPayloa
'REPORT_VISIBLE_POSITIONS'
);

export const startAutoReload = actionCreator<number>('START_AUTO_RELOAD');

export const startAutoReload = actionCreator('START_AUTO_RELOAD');
export const stopAutoReload = actionCreator('STOP_AUTO_RELOAD');

export const lockAutoReloadScroll = actionCreator('LOCK_AUTO_RELOAD_SCROLL');
export const unlockAutoReloadScroll = actionCreator('UNLOCK_AUTO_RELOAD_SCROLL');
Loading

0 comments on commit a21b985

Please sign in to comment.