LogRowContextProvider.tsx 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. import { DataQueryResponse, DataQueryError } from '@grafana/ui';
  2. import { LogRowModel, toDataFrame, Field } from '@grafana/data';
  3. import { useState, useEffect } from 'react';
  4. import flatten from 'lodash/flatten';
  5. import useAsync from 'react-use/lib/useAsync';
  6. export interface LogRowContextRows {
  7. before?: string[];
  8. after?: string[];
  9. }
  10. export interface LogRowContextQueryErrors {
  11. before?: string;
  12. after?: string;
  13. }
  14. export interface HasMoreContextRows {
  15. before: boolean;
  16. after: boolean;
  17. }
  18. interface LogRowContextProviderProps {
  19. row: LogRowModel;
  20. getRowContext: (row: LogRowModel, options?: any) => Promise<DataQueryResponse>;
  21. children: (props: {
  22. result: LogRowContextRows;
  23. errors: LogRowContextQueryErrors;
  24. hasMoreContextRows: HasMoreContextRows;
  25. updateLimit: () => void;
  26. }) => JSX.Element;
  27. }
  28. export const getRowContexts = async (
  29. getRowContext: (row: LogRowModel, options?: any) => Promise<DataQueryResponse>,
  30. row: LogRowModel,
  31. limit: number
  32. ) => {
  33. const promises = [
  34. getRowContext(row, {
  35. limit,
  36. }),
  37. getRowContext(row, {
  38. limit: limit + 1, // Lets add one more to the limit as we're filtering out one row see comment below
  39. direction: 'FORWARD',
  40. }),
  41. ];
  42. const results: Array<DataQueryResponse | DataQueryError> = await Promise.all(promises.map(p => p.catch(e => e)));
  43. return {
  44. data: results.map(result => {
  45. const dataResult: DataQueryResponse = result as DataQueryResponse;
  46. if (!dataResult.data) {
  47. return [];
  48. }
  49. const data: any[] = [];
  50. for (let index = 0; index < dataResult.data.length; index++) {
  51. const dataFrame = toDataFrame(dataResult.data[index]);
  52. const timestampField: Field<string> = dataFrame.fields.filter(field => field.name === 'ts')[0];
  53. for (let fieldIndex = 0; fieldIndex < timestampField.values.length; fieldIndex++) {
  54. const timestamp = timestampField.values.get(fieldIndex);
  55. // We need to filter out the row we're basing our search from because of how start/end params work in Loki API
  56. // see https://github.com/grafana/loki/issues/597#issuecomment-506408980
  57. // the alternative to create our own add 1 nanosecond method to the a timestamp string would be quite complex
  58. if (timestamp === row.timestamp) {
  59. continue;
  60. }
  61. const lineField: Field<string> = dataFrame.fields.filter(field => field.name === 'line')[0];
  62. const line = lineField.values.get(fieldIndex); // assuming that both fields have same length
  63. if (data.length === 0) {
  64. data[0] = [line];
  65. } else {
  66. data[0].push(line);
  67. }
  68. }
  69. }
  70. return data;
  71. }),
  72. errors: results.map(result => {
  73. const errorResult: DataQueryError = result as DataQueryError;
  74. if (!errorResult.message) {
  75. return null;
  76. }
  77. return errorResult.message;
  78. }),
  79. };
  80. };
  81. export const LogRowContextProvider: React.FunctionComponent<LogRowContextProviderProps> = ({
  82. getRowContext,
  83. row,
  84. children,
  85. }) => {
  86. // React Hook that creates a number state value called limit to component state and a setter function called setLimit
  87. // The intial value for limit is 10
  88. // Used for the number of rows to retrieve from backend from a specific point in time
  89. const [limit, setLimit] = useState(10);
  90. // React Hook that creates an object state value called result to component state and a setter function called setResult
  91. // The intial value for result is null
  92. // Used for sorting the response from backend
  93. const [result, setResult] = useState<{
  94. data: string[][];
  95. errors: string[];
  96. }>(null);
  97. // React Hook that creates an object state value called hasMoreContextRows to component state and a setter function called setHasMoreContextRows
  98. // The intial value for hasMoreContextRows is {before: true, after: true}
  99. // Used for indicating in UI if there are more rows to load in a given direction
  100. const [hasMoreContextRows, setHasMoreContextRows] = useState({
  101. before: true,
  102. after: true,
  103. });
  104. // React Hook that resolves two promises every time the limit prop changes
  105. // First promise fetches limit number of rows backwards in time from a specific point in time
  106. // Second promise fetches limit number of rows forwards in time from a specific point in time
  107. const { value } = useAsync(async () => {
  108. return await getRowContexts(getRowContext, row, limit); // Moved it to a separate function for debugging purposes
  109. }, [limit]);
  110. // React Hook that performs a side effect every time the value (from useAsync hook) prop changes
  111. // The side effect changes the result state with the response from the useAsync hook
  112. // The side effect changes the hasMoreContextRows state if there are more context rows before or after the current result
  113. useEffect(() => {
  114. if (value) {
  115. setResult(currentResult => {
  116. let hasMoreLogsBefore = true,
  117. hasMoreLogsAfter = true;
  118. if (currentResult && currentResult.data[0].length === value.data[0].length) {
  119. hasMoreLogsBefore = false;
  120. }
  121. if (currentResult && currentResult.data[1].length === value.data[1].length) {
  122. hasMoreLogsAfter = false;
  123. }
  124. setHasMoreContextRows({
  125. before: hasMoreLogsBefore,
  126. after: hasMoreLogsAfter,
  127. });
  128. return value;
  129. });
  130. }
  131. }, [value]);
  132. return children({
  133. result: {
  134. before: result ? flatten(result.data[0]) : [],
  135. after: result ? flatten(result.data[1]) : [],
  136. },
  137. errors: {
  138. before: result ? result.errors[0] : null,
  139. after: result ? result.errors[1] : null,
  140. },
  141. hasMoreContextRows,
  142. updateLimit: () => setLimit(limit + 10),
  143. });
  144. };