LiveLogs.tsx 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. import { css, cx } from '@emotion/css';
  2. import React, { PureComponent } from 'react';
  3. import tinycolor from 'tinycolor2';
  4. import { LogRowModel, TimeZone, dateTimeFormat, GrafanaTheme2 } from '@grafana/data';
  5. import { LogMessageAnsi, getLogRowStyles, Icon, Button, Themeable2, withTheme2 } from '@grafana/ui';
  6. import { ElapsedTime } from './ElapsedTime';
  7. const getStyles = (theme: GrafanaTheme2) => ({
  8. logsRowsLive: css`
  9. label: logs-rows-live;
  10. font-family: ${theme.typography.fontFamilyMonospace};
  11. font-size: ${theme.typography.bodySmall.fontSize};
  12. display: flex;
  13. flex-flow: column nowrap;
  14. height: 60vh;
  15. overflow-y: scroll;
  16. :first-child {
  17. margin-top: auto !important;
  18. }
  19. `,
  20. logsRowFade: css`
  21. label: logs-row-fresh;
  22. color: ${theme.colors.text};
  23. background-color: ${tinycolor(theme.colors.info.transparent).setAlpha(0.25).toString()};
  24. animation: fade 1s ease-out 1s 1 normal forwards;
  25. @keyframes fade {
  26. from {
  27. background-color: ${tinycolor(theme.colors.info.transparent).setAlpha(0.25).toString()};
  28. }
  29. to {
  30. background-color: transparent;
  31. }
  32. }
  33. `,
  34. logsRowsIndicator: css`
  35. font-size: ${theme.typography.h6.fontSize};
  36. padding-top: ${theme.spacing(1)};
  37. display: flex;
  38. align-items: center;
  39. `,
  40. button: css`
  41. margin-right: ${theme.spacing(1)};
  42. `,
  43. fullWidth: css`
  44. width: 100%;
  45. `,
  46. });
  47. export interface Props extends Themeable2 {
  48. logRows?: LogRowModel[];
  49. timeZone: TimeZone;
  50. stopLive: () => void;
  51. onPause: () => void;
  52. onResume: () => void;
  53. isPaused: boolean;
  54. }
  55. interface State {
  56. logRowsToRender?: LogRowModel[];
  57. }
  58. class LiveLogs extends PureComponent<Props, State> {
  59. private liveEndDiv: HTMLDivElement | null = null;
  60. private scrollContainerRef = React.createRef<HTMLTableSectionElement>();
  61. constructor(props: Props) {
  62. super(props);
  63. this.state = {
  64. logRowsToRender: props.logRows,
  65. };
  66. }
  67. static getDerivedStateFromProps(nextProps: Props, state: State) {
  68. if (!nextProps.isPaused) {
  69. return {
  70. // We update what we show only if not paused. We keep any background subscriptions running and keep updating
  71. // our state, but we do not show the updates, this allows us start again showing correct result after resuming
  72. // without creating a gap in the log results.
  73. logRowsToRender: nextProps.logRows,
  74. };
  75. } else {
  76. return null;
  77. }
  78. }
  79. /**
  80. * Handle pausing when user scrolls up so that we stop resetting his position to the bottom when new row arrives.
  81. * We do not need to throttle it here much, adding new rows should be throttled/buffered itself in the query epics
  82. * and after you pause we remove the handler and add it after you manually resume, so this should not be fired often.
  83. */
  84. onScroll = (event: React.SyntheticEvent) => {
  85. const { isPaused, onPause } = this.props;
  86. const { scrollTop, clientHeight, scrollHeight } = event.currentTarget;
  87. const distanceFromBottom = scrollHeight - (scrollTop + clientHeight);
  88. if (distanceFromBottom >= 5 && !isPaused) {
  89. onPause();
  90. }
  91. };
  92. rowsToRender = () => {
  93. const { isPaused } = this.props;
  94. let { logRowsToRender: rowsToRender = [] } = this.state;
  95. if (!isPaused) {
  96. // A perf optimisation here. Show just 100 rows when streaming and full length when the streaming is paused.
  97. rowsToRender = rowsToRender.slice(-100);
  98. }
  99. return rowsToRender;
  100. };
  101. render() {
  102. const { theme, timeZone, onPause, onResume, isPaused } = this.props;
  103. const styles = getStyles(theme);
  104. const { logsRow, logsRowLocalTime, logsRowMessage } = getLogRowStyles(theme);
  105. return (
  106. <div>
  107. <table className={styles.fullWidth}>
  108. <tbody
  109. onScroll={isPaused ? undefined : this.onScroll}
  110. className={cx(['logs-rows', styles.logsRowsLive])}
  111. ref={this.scrollContainerRef}
  112. >
  113. {this.rowsToRender().map((row: LogRowModel) => {
  114. return (
  115. <tr className={cx(logsRow, styles.logsRowFade)} key={row.uid}>
  116. <td className={cx(logsRowLocalTime)}>{dateTimeFormat(row.timeEpochMs, { timeZone })}</td>
  117. <td className={cx(logsRowMessage)}>{row.hasAnsi ? <LogMessageAnsi value={row.raw} /> : row.entry}</td>
  118. </tr>
  119. );
  120. })}
  121. <tr
  122. ref={(element) => {
  123. this.liveEndDiv = element;
  124. // This is triggered on every update so on every new row. It keeps the view scrolled at the bottom by
  125. // default.
  126. // As scrollTo is not implemented in JSDOM it needs to be part of the condition
  127. if (this.liveEndDiv && this.scrollContainerRef.current?.scrollTo && !isPaused) {
  128. this.scrollContainerRef.current?.scrollTo(0, this.scrollContainerRef.current.scrollHeight);
  129. }
  130. }}
  131. />
  132. </tbody>
  133. </table>
  134. <div className={styles.logsRowsIndicator}>
  135. <Button variant="secondary" onClick={isPaused ? onResume : onPause} className={styles.button}>
  136. <Icon name={isPaused ? 'play' : 'pause'} />
  137. &nbsp;
  138. {isPaused ? 'Resume' : 'Pause'}
  139. </Button>
  140. <Button variant="secondary" onClick={this.props.stopLive} className={styles.button}>
  141. <Icon name="square-shape" size="lg" type="mono" />
  142. &nbsp; Exit live mode
  143. </Button>
  144. {isPaused || (
  145. <span>
  146. Last line received: <ElapsedTime resetKey={this.props.logRows} humanize={true} /> ago
  147. </span>
  148. )}
  149. </div>
  150. </div>
  151. );
  152. }
  153. }
  154. export const LiveLogsWithTheme = withTheme2(LiveLogs);