|
|
|
|
@ -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> |
|
|
|
|
</> |
|
|
|
|
); |
|
|
|
|
|