LogRowContext.tsx 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. import React, { useContext, useRef, useState, useLayoutEffect } from 'react';
  2. import {
  3. ThemeContext,
  4. List,
  5. GrafanaTheme,
  6. selectThemeVariant,
  7. ClickOutsideWrapper,
  8. CustomScrollbar,
  9. DataQueryError,
  10. } from '@grafana/ui';
  11. import { css, cx } from 'emotion';
  12. import { LogRowContextRows, HasMoreContextRows, LogRowContextQueryErrors } from './LogRowContextProvider';
  13. import { LogRowModel } from 'app/core/logs_model';
  14. import { Alert } from './Error';
  15. interface LogRowContextProps {
  16. row: LogRowModel;
  17. context: LogRowContextRows;
  18. errors?: LogRowContextQueryErrors;
  19. hasMoreContextRows: HasMoreContextRows;
  20. onOutsideClick: () => void;
  21. onLoadMoreContext: () => void;
  22. }
  23. const getLogRowContextStyles = (theme: GrafanaTheme) => {
  24. const gradientTop = selectThemeVariant(
  25. {
  26. light: theme.colors.white,
  27. dark: theme.colors.dark1,
  28. },
  29. theme.type
  30. );
  31. const gradientBottom = selectThemeVariant(
  32. {
  33. light: theme.colors.gray7,
  34. dark: theme.colors.dark2,
  35. },
  36. theme.type
  37. );
  38. const boxShadowColor = selectThemeVariant(
  39. {
  40. light: theme.colors.gray5,
  41. dark: theme.colors.black,
  42. },
  43. theme.type
  44. );
  45. const borderColor = selectThemeVariant(
  46. {
  47. light: theme.colors.gray5,
  48. dark: theme.colors.dark9,
  49. },
  50. theme.type
  51. );
  52. return {
  53. commonStyles: css`
  54. position: absolute;
  55. width: calc(100% + 20px);
  56. left: -10px;
  57. height: 250px;
  58. z-index: 2;
  59. overflow: hidden;
  60. background: ${theme.colors.pageBg};
  61. background: linear-gradient(180deg, ${gradientTop} 0%, ${gradientBottom} 104.25%);
  62. box-shadow: 0px 2px 4px ${boxShadowColor}, 0px 0px 2px ${boxShadowColor};
  63. border: 1px solid ${borderColor};
  64. border-radius: ${theme.border.radius.md};
  65. `,
  66. header: css`
  67. height: 30px;
  68. padding: 0 10px;
  69. display: flex;
  70. align-items: center;
  71. background: ${borderColor};
  72. `,
  73. logs: css`
  74. height: 220px;
  75. padding: 10px;
  76. `,
  77. };
  78. };
  79. interface LogRowContextGroupHeaderProps {
  80. row: LogRowModel;
  81. rows: Array<string | DataQueryError>;
  82. onLoadMoreContext: () => void;
  83. shouldScrollToBottom?: boolean;
  84. canLoadMoreRows?: boolean;
  85. }
  86. interface LogRowContextGroupProps extends LogRowContextGroupHeaderProps {
  87. rows: Array<string | DataQueryError>;
  88. className: string;
  89. error?: string;
  90. }
  91. const LogRowContextGroupHeader: React.FunctionComponent<LogRowContextGroupHeaderProps> = ({
  92. row,
  93. rows,
  94. onLoadMoreContext,
  95. canLoadMoreRows,
  96. }) => {
  97. const theme = useContext(ThemeContext);
  98. const { header } = getLogRowContextStyles(theme);
  99. // Filtering out the original row from the context.
  100. // Loki requires a rowTimestamp+1ns for the following logs to be queried.
  101. // We don't to ns-precision calculations in Loki log row context retrieval, hence the filtering here
  102. // Also see: https://github.com/grafana/loki/issues/597
  103. const logRowsToRender = rows.filter(contextRow => contextRow !== row.raw);
  104. return (
  105. <div className={header}>
  106. <span
  107. className={css`
  108. opacity: 0.6;
  109. `}
  110. >
  111. Found {logRowsToRender.length} rows.
  112. </span>
  113. {(rows.length >= 10 || (rows.length > 10 && rows.length % 10 !== 0)) && canLoadMoreRows && (
  114. <span
  115. className={css`
  116. margin-left: 10px;
  117. &:hover {
  118. text-decoration: underline;
  119. cursor: pointer;
  120. }
  121. `}
  122. onClick={() => onLoadMoreContext()}
  123. >
  124. Load 10 more
  125. </span>
  126. )}
  127. </div>
  128. );
  129. };
  130. const LogRowContextGroup: React.FunctionComponent<LogRowContextGroupProps> = ({
  131. row,
  132. rows,
  133. error,
  134. className,
  135. shouldScrollToBottom,
  136. canLoadMoreRows,
  137. onLoadMoreContext,
  138. }) => {
  139. const theme = useContext(ThemeContext);
  140. const { commonStyles, logs } = getLogRowContextStyles(theme);
  141. const [scrollTop, setScrollTop] = useState(0);
  142. const listContainerRef = useRef<HTMLDivElement>();
  143. useLayoutEffect(() => {
  144. if (shouldScrollToBottom && listContainerRef.current) {
  145. setScrollTop(listContainerRef.current.offsetHeight);
  146. }
  147. });
  148. const headerProps = {
  149. row,
  150. rows,
  151. onLoadMoreContext,
  152. canLoadMoreRows,
  153. };
  154. return (
  155. <div className={cx(className, commonStyles)}>
  156. {/* When displaying "after" context */}
  157. {shouldScrollToBottom && !error && <LogRowContextGroupHeader {...headerProps} />}
  158. <div className={logs}>
  159. <CustomScrollbar autoHide scrollTop={scrollTop}>
  160. <div ref={listContainerRef}>
  161. {!error && (
  162. <List
  163. items={rows}
  164. renderItem={item => {
  165. return (
  166. <div
  167. className={css`
  168. padding: 5px 0;
  169. `}
  170. >
  171. {item}
  172. </div>
  173. );
  174. }}
  175. />
  176. )}
  177. {error && <Alert message={error} />}
  178. </div>
  179. </CustomScrollbar>
  180. </div>
  181. {/* When displaying "before" context */}
  182. {!shouldScrollToBottom && !error && <LogRowContextGroupHeader {...headerProps} />}
  183. </div>
  184. );
  185. };
  186. export const LogRowContext: React.FunctionComponent<LogRowContextProps> = ({
  187. row,
  188. context,
  189. errors,
  190. onOutsideClick,
  191. onLoadMoreContext,
  192. hasMoreContextRows,
  193. }) => {
  194. return (
  195. <ClickOutsideWrapper onClick={onOutsideClick}>
  196. <div>
  197. {context.after && (
  198. <LogRowContextGroup
  199. rows={context.after}
  200. error={errors && errors.after}
  201. row={row}
  202. className={css`
  203. top: -250px;
  204. `}
  205. shouldScrollToBottom
  206. canLoadMoreRows={hasMoreContextRows.after}
  207. onLoadMoreContext={onLoadMoreContext}
  208. />
  209. )}
  210. {context.before && (
  211. <LogRowContextGroup
  212. onLoadMoreContext={onLoadMoreContext}
  213. canLoadMoreRows={hasMoreContextRows.before}
  214. row={row}
  215. rows={context.before}
  216. error={errors && errors.before}
  217. className={css`
  218. top: 100%;
  219. `}
  220. />
  221. )}
  222. </div>
  223. </ClickOutsideWrapper>
  224. );
  225. };