import React, { PureComponent } from 'react'; import { css, cx } from 'emotion'; import tinycolor from 'tinycolor2'; import { Themeable, withTheme, getLogRowStyles } from '@grafana/ui'; import { GrafanaTheme, LogRowModel, TimeZone } from '@grafana/data'; import ElapsedTime from './ElapsedTime'; const getStyles = (theme: GrafanaTheme) => ({ logsRowsLive: css` label: logs-rows-live; font-family: ${theme.typography.fontFamily.monospace}; font-size: ${theme.typography.size.sm}; display: flex; flex-flow: column nowrap; height: 65vh; overflow-y: auto; :first-child { margin-top: auto !important; } `, logsRowFade: css` label: logs-row-fresh; color: ${theme.colors.text}; background-color: ${tinycolor(theme.colors.blueLight) .setAlpha(0.25) .toString()}; animation: fade 1s ease-out 1s 1 normal forwards; @keyframes fade { from { background-color: ${tinycolor(theme.colors.blueLight) .setAlpha(0.25) .toString()}; } to { background-color: transparent; } } `, logsRowsIndicator: css` font-size: ${theme.typography.size.md}; padding-top: ${theme.spacing.sm}; display: flex; align-items: center; `, button: css` margin-right: ${theme.spacing.sm}; `, fullWidth: css` width: 100%; `, }); export interface Props extends Themeable { logRows?: LogRowModel[]; timeZone: TimeZone; stopLive: () => void; onPause: () => void; onResume: () => void; isPaused: boolean; } interface State { logRowsToRender?: LogRowModel[]; } class LiveLogs extends PureComponent { private liveEndDiv: HTMLDivElement | null = null; private scrollContainerRef = React.createRef(); private lastScrollPos: number | null = null; constructor(props: Props) { super(props); this.state = { logRowsToRender: props.logRows, }; } componentDidUpdate(prevProps: Props) { 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 && this.scrollContainerRef.current) { // 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, state: State) { 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. logRowsToRender: nextProps.logRows, }; } 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 { logRowsToRender: rowsToRender = [] } = this.state; 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, onPause, onResume, isPaused } = this.props; const styles = getStyles(theme); const showUtc = timeZone === 'utc'; const { logsRow, logsRowLocalTime, logsRowMessage } = getLogRowStyles(theme); return (
{this.rowsToRender().map((row: LogRowModel) => { return ( {showUtc && ( )} {!showUtc && ( )} ); })} { this.liveEndDiv = element; // 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); } }} />
{row.timeUtc} {row.timeLocal} {row.entry}
{isPaused || ( Last line received: ago )}
); } } export const LiveLogsWithTheme = withTheme(LiveLogs);