|
|
@@ -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;
|
|
|
+}
|
|
|
+
|
|
|
+interface State {
|
|
|
+ logsResultToRender?: LogsModel;
|
|
|
}
|
|
|
|
|
|
-class LiveLogs extends PureComponent<Props> {
|
|
|
+class LiveLogs extends PureComponent<Props, State> {
|
|
|
private liveEndDiv: HTMLDivElement = null;
|
|
|
+ private scrollContainerRef = React.createRef<HTMLDivElement>();
|
|
|
+ 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 (
|
|
|
<>
|
|
|
- <div className={cx(['logs-rows', styles.logsRowsLive])}>
|
|
|
- {rowsToRender.map((row: any, index) => {
|
|
|
+ <div
|
|
|
+ onScroll={isPaused ? undefined : this.onScroll}
|
|
|
+ className={cx(['logs-rows', styles.logsRowsLive])}
|
|
|
+ ref={this.scrollContainerRef}
|
|
|
+ >
|
|
|
+ {this.rowsToRender().map((row: any, index) => {
|
|
|
return (
|
|
|
<div
|
|
|
className={row.fresh ? cx([logsRow, styles.logsRowFresh]) : cx([logsRow, styles.logsRowOld])}
|
|
|
@@ -82,24 +155,29 @@ class LiveLogs extends PureComponent<Props> {
|
|
|
<div
|
|
|
ref={element => {
|
|
|
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);
|
|
|
}
|
|
|
}}
|
|
|
/>
|
|
|
</div>
|
|
|
<div className={cx([styles.logsRowsIndicator])}>
|
|
|
- <span>
|
|
|
- Last line received: <ElapsedTime resetKey={this.props.logsResult} humanize={true} /> ago
|
|
|
- </span>
|
|
|
- <LinkButton
|
|
|
- onClick={this.props.stopLive}
|
|
|
- size="md"
|
|
|
- variant="transparent"
|
|
|
- style={{ color: theme.colors.orange }}
|
|
|
- >
|
|
|
- Stop Live
|
|
|
- </LinkButton>
|
|
|
+ <button onClick={isPaused ? onResume : onPause} className={cx('btn btn-secondary', styles.button)}>
|
|
|
+ <i className={cx('fa', isPaused ? 'fa-play' : 'fa-pause')} />
|
|
|
+
|
|
|
+ {isPaused ? 'Resume' : 'Pause'}
|
|
|
+ </button>
|
|
|
+ <button onClick={this.props.stopLive} className={cx('btn btn-inverse', styles.button)}>
|
|
|
+ <i className={'fa fa-stop'} />
|
|
|
+ Stop
|
|
|
+ </button>
|
|
|
+ {isPaused || (
|
|
|
+ <span>
|
|
|
+ Last line received: <ElapsedTime resetKey={this.props.logsResult} humanize={true} /> ago
|
|
|
+ </span>
|
|
|
+ )}
|
|
|
</div>
|
|
|
</>
|
|
|
);
|