Browse Source

Explore: display log line context (#17097)

* Extend DataSourceAPI to enable log row context retrieval

* Add react-use package

* Display log row context in UI

* Make Loki datasource return "after" log context in correct order

* Don't show Load more context links  when there are no more new results

* Update getLogRowContext to return DataQueryResponse

* Use DataQueryResponse in log row context provider, filter out original row  being duplicated in context
Dominik Prokop 6 years ago
parent
commit
12e0616413

+ 1 - 0
package.json

@@ -224,6 +224,7 @@
     "react-sizeme": "2.5.2",
     "react-table": "6.9.2",
     "react-transition-group": "2.6.1",
+    "react-use": "9.0.0",
     "react-virtualized": "9.21.0",
     "react-window": "1.7.1",
     "redux": "4.0.1",

+ 9 - 0
packages/grafana-ui/src/types/datasource.ts

@@ -172,6 +172,11 @@ export abstract class DataSourceApi<
    */
   getQueryDisplayText?(query: TQuery): string;
 
+  /**
+   * Retrieve context for a given log row
+   */
+  getLogRowContext?(row: any, limit?: number): Promise<DataQueryResponse>;
+
   /**
    * Set after constructor call, as the data source instance is the most common thing to pass around
    * we attach the components to this instance for easy access
@@ -282,6 +287,10 @@ export interface DataQueryResponse {
   data: DataQueryResponseData[];
 }
 
+export interface LogRowContextQueryResponse {
+  data: Array<Array<string | DataQueryError>>;
+}
+
 export interface DataQuery {
   /**
    * A - Z

+ 196 - 53
public/app/features/explore/LogRow.tsx

@@ -8,6 +8,16 @@ import { LogLabels } from './LogLabels';
 import { findHighlightChunksInText } from 'app/core/utils/text';
 import { LogLabelStats } from './LogLabelStats';
 import { LogMessageAnsi } from './LogMessageAnsi';
+import { css, cx } from 'emotion';
+import {
+  LogRowContextProvider,
+  LogRowContextRows,
+  HasMoreContextRows,
+  LogRowContextQueryErrors,
+} from './LogRowContextProvider';
+import { ThemeContext, selectThemeVariant, GrafanaTheme, DataQueryResponse } from '@grafana/ui';
+import { LogRowContext } from './LogRowContext';
+import tinycolor from 'tinycolor2';
 
 interface Props {
   highlighterExpressions?: string[];
@@ -18,6 +28,9 @@ interface Props {
   showUtc: boolean;
   getRows: () => LogRowModel[];
   onClickLabel?: (label: string, value: string) => void;
+  onContextClick?: () => void;
+  getRowContext?: (row: LogRowModel, limit: number) => Promise<DataQueryResponse>;
+  className?: string;
 }
 
 interface State {
@@ -29,6 +42,7 @@ interface State {
   parser?: LogsParser;
   parsedFieldHighlights: string[];
   showFieldStats: boolean;
+  showContext: boolean;
 }
 
 /**
@@ -44,6 +58,32 @@ const FieldHighlight = onClick => props => {
   );
 };
 
+const logRowStyles = css`
+  position: relative;
+  /* z-index: 0; */
+  /* outline: none; */
+`;
+
+const getLogRowWithContextStyles = (theme: GrafanaTheme, state: State) => {
+  const outlineColor = selectThemeVariant(
+    {
+      light: theme.colors.white,
+      dark: theme.colors.black,
+    },
+    theme.type
+  );
+
+  return {
+    row: css`
+      z-index: 1;
+      outline: 9999px solid
+        ${tinycolor(outlineColor)
+          .setAlpha(0.7)
+          .toRgbString()};
+    `,
+  };
+};
+
 /**
  * Renders a log line.
  *
@@ -63,6 +103,7 @@ export class LogRow extends PureComponent<Props, State> {
     parser: undefined,
     parsedFieldHighlights: [],
     showFieldStats: false,
+    showContext: false,
   };
 
   componentWillUnmount() {
@@ -89,11 +130,21 @@ export class LogRow extends PureComponent<Props, State> {
   };
 
   onMouseOverMessage = () => {
+    if (this.state.showContext) {
+      // When showing context we don't want to the LogRow rerender as it will mess up state of context block
+      // making the "after" context to be scrolled to the top, what is desired only on open
+      // The log row message needs to be refactored to separate component that encapsulates parsing and parsed message state
+      return;
+    }
     // Don't parse right away, user might move along
     this.mouseMessageTimer = setTimeout(this.parseMessage, 500);
   };
 
   onMouseOutMessage = () => {
+    if (this.state.showContext) {
+      // See comment in onMouseOverMessage method
+      return;
+    }
     clearTimeout(this.mouseMessageTimer);
     this.setState({ parsed: false });
   };
@@ -110,7 +161,25 @@ export class LogRow extends PureComponent<Props, State> {
     }
   };
 
-  render() {
+  toggleContext = () => {
+    this.setState(state => {
+      return {
+        showContext: !state.showContext,
+      };
+    });
+  };
+
+  onContextToggle = (e: React.SyntheticEvent<HTMLElement>) => {
+    e.stopPropagation();
+    this.toggleContext();
+  };
+
+  renderLogRow(
+    context?: LogRowContextRows,
+    errors?: LogRowContextQueryErrors,
+    hasMoreContextRows?: HasMoreContextRows,
+    updateLimit?: () => void
+  ) {
     const {
       getRows,
       highlighterExpressions,
@@ -129,6 +198,7 @@ export class LogRow extends PureComponent<Props, State> {
       parsed,
       parsedFieldHighlights,
       showFieldStats,
+      showContext,
     } = this.state;
     const { entry, hasAnsi, raw } = row;
     const previewHighlights = highlighterExpressions && !_.isEqual(highlighterExpressions, row.searchWords);
@@ -139,59 +209,132 @@ export class LogRow extends PureComponent<Props, State> {
     });
 
     return (
-      <div className="logs-row">
-        {showDuplicates && (
-          <div className="logs-row__duplicates">{row.duplicates > 0 ? `${row.duplicates + 1}x` : null}</div>
-        )}
-        <div className={row.logLevel ? `logs-row__level logs-row__level--${row.logLevel}` : ''} />
-        {showUtc && (
-          <div className="logs-row__time" title={`Local: ${row.timeLocal} (${row.timeFromNow})`}>
-            {row.timestamp}
-          </div>
-        )}
-        {showLocalTime && (
-          <div className="logs-row__localtime" title={`${row.timestamp} (${row.timeFromNow})`}>
-            {row.timeLocal}
-          </div>
-        )}
-        {showLabels && (
-          <div className="logs-row__labels">
-            <LogLabels getRows={getRows} labels={row.uniqueLabels} onClickLabel={onClickLabel} />
-          </div>
-        )}
-        <div className="logs-row__message" onMouseEnter={this.onMouseOverMessage} onMouseLeave={this.onMouseOutMessage}>
-          {parsed && (
-            <Highlighter
-              autoEscape
-              highlightTag={FieldHighlight(this.onClickHighlight)}
-              textToHighlight={entry}
-              searchWords={parsedFieldHighlights}
-              highlightClassName="logs-row__field-highlight"
-            />
-          )}
-          {!parsed && needsHighlighter && (
-            <Highlighter
-              textToHighlight={entry}
-              searchWords={highlights}
-              findChunks={findHighlightChunksInText}
-              highlightClassName={highlightClassName}
-            />
-          )}
-          {hasAnsi && !parsed && !needsHighlighter && <LogMessageAnsi value={raw} />}
-          {!hasAnsi && !parsed && !needsHighlighter && entry}
-          {showFieldStats && (
-            <div className="logs-row__stats">
-              <LogLabelStats
-                stats={fieldStats}
-                label={fieldLabel}
-                value={fieldValue}
-                onClickClose={this.onClickClose}
-                rowCount={fieldCount}
-              />
+      <ThemeContext.Consumer>
+        {theme => {
+          const styles = this.state.showContext
+            ? cx(logRowStyles, getLogRowWithContextStyles(theme, this.state).row)
+            : logRowStyles;
+          console.log(styles);
+          return (
+            <div className={`logs-row ${this.props.className}`}>
+              {showDuplicates && (
+                <div className="logs-row__duplicates">{row.duplicates > 0 ? `${row.duplicates + 1}x` : null}</div>
+              )}
+              <div className={row.logLevel ? `logs-row__level logs-row__level--${row.logLevel}` : ''} />
+              {showUtc && (
+                <div className="logs-row__time" title={`Local: ${row.timeLocal} (${row.timeFromNow})`}>
+                  {row.timestamp}
+                </div>
+              )}
+              {showLocalTime && (
+                <div className="logs-row__localtime" title={`${row.timestamp} (${row.timeFromNow})`}>
+                  {row.timeLocal}
+                </div>
+              )}
+              {showLabels && (
+                <div className="logs-row__labels">
+                  <LogLabels getRows={getRows} labels={row.uniqueLabels} onClickLabel={onClickLabel} />
+                </div>
+              )}
+              <div
+                className="logs-row__message"
+                onMouseEnter={this.onMouseOverMessage}
+                onMouseLeave={this.onMouseOutMessage}
+              >
+                <div
+                  className={css`
+                    position: relative;
+                  `}
+                >
+                  {showContext && context && (
+                    <LogRowContext
+                      row={row}
+                      context={context}
+                      errors={errors}
+                      hasMoreContextRows={hasMoreContextRows}
+                      onOutsideClick={this.toggleContext}
+                      onLoadMoreContext={() => {
+                        if (updateLimit) {
+                          updateLimit();
+                        }
+                      }}
+                    />
+                  )}
+                  <span className={styles}>
+                    {parsed && (
+                      <Highlighter
+                        autoEscape
+                        highlightTag={FieldHighlight(this.onClickHighlight)}
+                        textToHighlight={entry}
+                        searchWords={parsedFieldHighlights}
+                        highlightClassName="logs-row__field-highlight"
+                      />
+                    )}
+                    {!parsed && needsHighlighter && (
+                      <Highlighter
+                        textToHighlight={entry}
+                        searchWords={highlights}
+                        findChunks={findHighlightChunksInText}
+                        highlightClassName={highlightClassName}
+                      />
+                    )}
+                    {hasAnsi && !parsed && !needsHighlighter && <LogMessageAnsi value={raw} />}
+                    {!hasAnsi && !parsed && !needsHighlighter && entry}
+                    {showFieldStats && (
+                      <div className="logs-row__stats">
+                        <LogLabelStats
+                          stats={fieldStats}
+                          label={fieldLabel}
+                          value={fieldValue}
+                          onClickClose={this.onClickClose}
+                          rowCount={fieldCount}
+                        />
+                      </div>
+                    )}
+                  </span>
+                  {row.searchWords && row.searchWords.length > 0 && (
+                    <span
+                      onClick={this.onContextToggle}
+                      className={css`
+                        visibility: hidden;
+                        white-space: nowrap;
+                        position: relative;
+                        z-index: ${showContext ? 1 : 0};
+                        cursor: pointer;
+                        .logs-row:hover & {
+                          visibility: visible;
+                          margin-left: 10px;
+                          text-decoration: underline;
+                        }
+                      `}
+                    >
+                      {showContext ? 'Hide' : 'Show'} context
+                    </span>
+                  )}
+                </div>
+              </div>
             </div>
-          )}
-        </div>
-      </div>
+          );
+        }}
+      </ThemeContext.Consumer>
     );
   }
+
+  render() {
+    const { showContext } = this.state;
+
+    if (showContext) {
+      return (
+        <>
+          <LogRowContextProvider row={this.props.row} getRowContext={this.props.getRowContext}>
+            {({ result, errors, hasMoreContextRows, updateLimit }) => {
+              return <>{this.renderLogRow(result, errors, hasMoreContextRows, updateLimit)}</>;
+            }}
+          </LogRowContextProvider>
+        </>
+      );
+    }
+
+    return this.renderLogRow();
+  }
 }

+ 239 - 0
public/app/features/explore/LogRowContext.tsx

@@ -0,0 +1,239 @@
+import React, { useContext, useRef, useState, useLayoutEffect } from 'react';
+import {
+  ThemeContext,
+  List,
+  GrafanaTheme,
+  selectThemeVariant,
+  ClickOutsideWrapper,
+  CustomScrollbar,
+  DataQueryError,
+} from '@grafana/ui';
+import { css, cx } from 'emotion';
+import { LogRowContextRows, HasMoreContextRows, LogRowContextQueryErrors } from './LogRowContextProvider';
+import { LogRowModel } from 'app/core/logs_model';
+import { Alert } from './Error';
+
+interface LogRowContextProps {
+  row: LogRowModel;
+  context: LogRowContextRows;
+  errors?: LogRowContextQueryErrors;
+  hasMoreContextRows: HasMoreContextRows;
+  onOutsideClick: () => void;
+  onLoadMoreContext: () => void;
+}
+
+const getLogRowContextStyles = (theme: GrafanaTheme) => {
+  const gradientTop = selectThemeVariant(
+    {
+      light: theme.colors.white,
+      dark: theme.colors.dark1,
+    },
+    theme.type
+  );
+  const gradientBottom = selectThemeVariant(
+    {
+      light: theme.colors.gray7,
+      dark: theme.colors.dark2,
+    },
+    theme.type
+  );
+
+  const boxShadowColor = selectThemeVariant(
+    {
+      light: theme.colors.gray5,
+      dark: theme.colors.black,
+    },
+    theme.type
+  );
+  const borderColor = selectThemeVariant(
+    {
+      light: theme.colors.gray5,
+      dark: theme.colors.dark9,
+    },
+    theme.type
+  );
+
+  return {
+    commonStyles: css`
+      position: absolute;
+      width: calc(100% + 20px);
+      left: -10px;
+      height: 250px;
+      z-index: 2;
+      overflow: hidden;
+      background: ${theme.colors.pageBg};
+      background: linear-gradient(180deg, ${gradientTop} 0%, ${gradientBottom} 104.25%);
+      box-shadow: 0px 2px 4px ${boxShadowColor}, 0px 0px 2px ${boxShadowColor};
+      border: 1px solid ${borderColor};
+      border-radius: ${theme.border.radius.md};
+    `,
+    header: css`
+      height: 30px;
+      padding: 0 10px;
+      display: flex;
+      align-items: center;
+      background: ${borderColor};
+    `,
+    logs: css`
+      height: 220px;
+      padding: 10px;
+    `,
+  };
+};
+
+interface LogRowContextGroupHeaderProps {
+  row: LogRowModel;
+  rows: Array<string | DataQueryError>;
+  onLoadMoreContext: () => void;
+  shouldScrollToBottom?: boolean;
+  canLoadMoreRows?: boolean;
+}
+interface LogRowContextGroupProps extends LogRowContextGroupHeaderProps {
+  rows: Array<string | DataQueryError>;
+  className: string;
+  error?: string;
+}
+
+const LogRowContextGroupHeader: React.FunctionComponent<LogRowContextGroupHeaderProps> = ({
+  row,
+  rows,
+  onLoadMoreContext,
+  canLoadMoreRows,
+}) => {
+  const theme = useContext(ThemeContext);
+  const { header } = getLogRowContextStyles(theme);
+
+  // Filtering out the original row from the context.
+  // Loki requires a rowTimestamp+1ns for the following logs to be queried.
+  // We don't to ns-precision calculations in Loki log row context retrieval, hence the filtering here
+  // Also see: https://github.com/grafana/loki/issues/597
+  const logRowsToRender = rows.filter(contextRow => contextRow !== row.raw);
+
+  return (
+    <div className={header}>
+      <span
+        className={css`
+          opacity: 0.6;
+        `}
+      >
+        Found {logRowsToRender.length} rows.
+      </span>
+      {(rows.length >= 10 || (rows.length > 10 && rows.length % 10 !== 0)) && canLoadMoreRows && (
+        <span
+          className={css`
+            margin-left: 10px;
+            &:hover {
+              text-decoration: underline;
+              cursor: pointer;
+            }
+          `}
+          onClick={() => onLoadMoreContext()}
+        >
+          Load 10 more
+        </span>
+      )}
+    </div>
+  );
+};
+
+const LogRowContextGroup: React.FunctionComponent<LogRowContextGroupProps> = ({
+  row,
+  rows,
+  error,
+  className,
+  shouldScrollToBottom,
+  canLoadMoreRows,
+  onLoadMoreContext,
+}) => {
+  const theme = useContext(ThemeContext);
+  const { commonStyles, logs } = getLogRowContextStyles(theme);
+  const [scrollTop, setScrollTop] = useState(0);
+  const listContainerRef = useRef<HTMLDivElement>();
+
+  useLayoutEffect(() => {
+    if (shouldScrollToBottom && listContainerRef.current) {
+      setScrollTop(listContainerRef.current.offsetHeight);
+    }
+  });
+
+  const headerProps = {
+    row,
+    rows,
+    onLoadMoreContext,
+    canLoadMoreRows,
+  };
+
+  return (
+    <div className={cx(className, commonStyles)}>
+      {/* When displaying "after" context */}
+      {shouldScrollToBottom && !error && <LogRowContextGroupHeader {...headerProps} />}
+      <div className={logs}>
+        <CustomScrollbar autoHide scrollTop={scrollTop}>
+          <div ref={listContainerRef}>
+            {!error && (
+              <List
+                items={rows}
+                renderItem={item => {
+                  return (
+                    <div
+                      className={css`
+                        padding: 5px 0;
+                      `}
+                    >
+                      {item}
+                    </div>
+                  );
+                }}
+              />
+            )}
+            {error && <Alert message={error} />}
+          </div>
+        </CustomScrollbar>
+      </div>
+      {/* When displaying "before" context */}
+      {!shouldScrollToBottom && !error && <LogRowContextGroupHeader {...headerProps} />}
+    </div>
+  );
+};
+
+export const LogRowContext: React.FunctionComponent<LogRowContextProps> = ({
+  row,
+  context,
+  errors,
+  onOutsideClick,
+  onLoadMoreContext,
+  hasMoreContextRows,
+}) => {
+  return (
+    <ClickOutsideWrapper onClick={onOutsideClick}>
+      <div>
+        {context.after && (
+          <LogRowContextGroup
+            rows={context.after}
+            error={errors && errors.after}
+            row={row}
+            className={css`
+              top: -250px;
+            `}
+            shouldScrollToBottom
+            canLoadMoreRows={hasMoreContextRows.after}
+            onLoadMoreContext={onLoadMoreContext}
+          />
+        )}
+
+        {context.before && (
+          <LogRowContextGroup
+            onLoadMoreContext={onLoadMoreContext}
+            canLoadMoreRows={hasMoreContextRows.before}
+            row={row}
+            rows={context.before}
+            error={errors && errors.before}
+            className={css`
+              top: 100%;
+            `}
+          />
+        )}
+      </div>
+    </ClickOutsideWrapper>
+  );
+};

+ 104 - 0
public/app/features/explore/LogRowContextProvider.tsx

@@ -0,0 +1,104 @@
+import { LogRowModel } from 'app/core/logs_model';
+import { LogRowContextQueryResponse, SeriesData, DataQueryResponse, DataQueryError } from '@grafana/ui';
+import { useState, useEffect } from 'react';
+import useAsync from 'react-use/lib/useAsync';
+
+export interface LogRowContextRows {
+  before?: Array<string | DataQueryError>;
+  after?: Array<string | DataQueryError>;
+}
+export interface LogRowContextQueryErrors {
+  before?: string;
+  after?: string;
+}
+
+export interface HasMoreContextRows {
+  before: boolean;
+  after: boolean;
+}
+
+interface LogRowContextProviderProps {
+  row: LogRowModel;
+  getRowContext: (row: LogRowModel, limit: number) => Promise<DataQueryResponse>;
+  children: (props: {
+    result: LogRowContextRows;
+    errors: LogRowContextQueryErrors;
+    hasMoreContextRows: HasMoreContextRows;
+    updateLimit: () => void;
+  }) => JSX.Element;
+}
+
+export const LogRowContextProvider: React.FunctionComponent<LogRowContextProviderProps> = ({
+  getRowContext,
+  row,
+  children,
+}) => {
+  const [limit, setLimit] = useState(10);
+  const [result, setResult] = useState<LogRowContextQueryResponse>(null);
+  const [errors, setErrors] = useState<LogRowContextQueryErrors>(null);
+  const [hasMoreContextRows, setHasMoreContextRows] = useState({
+    before: true,
+    after: true,
+  });
+
+  const { value } = useAsync(async () => {
+    const context = await getRowContext(row, limit);
+    return {
+      data: context.data.map(series => {
+        if ((series as SeriesData).rows) {
+          return (series as SeriesData).rows.map(row => row[1]);
+        } else {
+          return [series];
+        }
+        return [];
+      }),
+    };
+  }, [limit]);
+
+  useEffect(() => {
+    if (value) {
+      setResult(currentResult => {
+        let hasMoreLogsBefore = true,
+          hasMoreLogsAfter = true;
+        let beforeContextError, afterContextError;
+
+        if (currentResult && currentResult.data[0].length === value.data[0].length) {
+          hasMoreLogsBefore = false;
+        }
+
+        if (currentResult && currentResult.data[1].length === value.data[1].length) {
+          hasMoreLogsAfter = false;
+        }
+
+        if (value.data[0] && value.data[0].length > 0 && value.data[0][0].message) {
+          beforeContextError = value.data[0][0].message;
+        }
+        if (value.data[1] && value.data[1].length > 0 && value.data[1][0].message) {
+          afterContextError = value.data[1][0].message;
+        }
+
+        setHasMoreContextRows({
+          before: hasMoreLogsBefore,
+          after: hasMoreLogsAfter,
+        });
+
+        setErrors({
+          before: beforeContextError,
+          after: afterContextError,
+        });
+
+        return value;
+      });
+    }
+  }, [value]);
+
+  return children({
+    result: {
+      before: result ? result.data[0] : [],
+      after: result ? result.data[1] : [],
+    },
+    errors,
+    hasMoreContextRows,
+    updateLimit: () => setLimit(limit + 10),
+  });
+};

+ 4 - 1
public/app/features/explore/Logs.tsx

@@ -5,7 +5,7 @@ import * as rangeUtil from '@grafana/ui/src/utils/rangeutil';
 import { RawTimeRange, Switch, LogLevel, TimeZone, TimeRange, AbsoluteTimeRange } from '@grafana/ui';
 import TimeSeries from 'app/core/time_series2';
 
-import { LogsDedupDescription, LogsDedupStrategy, LogsModel, LogsMetaKind } from 'app/core/logs_model';
+import { LogsDedupDescription, LogsDedupStrategy, LogsModel, LogsMetaKind, LogRowModel } from 'app/core/logs_model';
 
 import ToggleButtonGroup, { ToggleButton } from 'app/core/components/ToggleButtonGroup/ToggleButtonGroup';
 
@@ -60,6 +60,7 @@ interface Props {
   onStopScanning?: () => void;
   onDedupStrategyChange: (dedupStrategy: LogsDedupStrategy) => void;
   onToggleLogLevel: (hiddenLogLevels: Set<LogLevel>) => void;
+  getRowContext?: (row: LogRowModel, limit: number) => Promise<any>;
 }
 
 interface State {
@@ -252,6 +253,7 @@ export default class Logs extends PureComponent<Props, State> {
               <LogRow
                 key={index}
                 getRows={getRows}
+                getRowContext={this.props.getRowContext}
                 highlighterExpressions={highlighterExpressions}
                 row={row}
                 showDuplicates={showDuplicates}
@@ -268,6 +270,7 @@ export default class Logs extends PureComponent<Props, State> {
               <LogRow
                 key={PREVIEW_LIMIT + index}
                 getRows={getRows}
+                getRowContext={this.props.getRowContext}
                 row={row}
                 showDuplicates={showDuplicates}
                 showLabels={showLabels && hasLabel}

+ 26 - 3
public/app/features/explore/LogsContainer.tsx

@@ -1,10 +1,19 @@
 import React, { PureComponent } from 'react';
 import { hot } from 'react-hot-loader';
 import { connect } from 'react-redux';
-import { RawTimeRange, TimeRange, LogLevel, TimeZone, AbsoluteTimeRange, toUtc, dateTime } from '@grafana/ui';
+import {
+  RawTimeRange,
+  TimeRange,
+  LogLevel,
+  TimeZone,
+  AbsoluteTimeRange,
+  toUtc,
+  dateTime,
+  DataSourceApi,
+} from '@grafana/ui';
 
 import { ExploreId, ExploreItemState } from 'app/types/explore';
-import { LogsModel, LogsDedupStrategy } from 'app/core/logs_model';
+import { LogsModel, LogsDedupStrategy, LogRowModel } from 'app/core/logs_model';
 import { StoreState } from 'app/types';
 
 import { changeDedupStrategy, changeTime } from './state/actions';
@@ -15,6 +24,7 @@ import { deduplicatedLogsSelector, exploreItemUIStateSelector } from 'app/featur
 import { getTimeZone } from '../profile/state/selectors';
 
 interface LogsContainerProps {
+  datasourceInstance: DataSourceApi | null;
   exploreId: ExploreId;
   loading: boolean;
   logsHighlighterExpressions?: string[];
@@ -58,9 +68,20 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
     });
   };
 
+  getLogRowContext = async (row: LogRowModel, limit: number) => {
+    const { datasourceInstance } = this.props;
+
+    if (datasourceInstance) {
+      return datasourceInstance.getLogRowContext(row, limit);
+    }
+
+    return [];
+  };
+
   render() {
     const {
       exploreId,
+
       loading,
       logsHighlighterExpressions,
       logsResult,
@@ -97,6 +118,7 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
           scanRange={scanRange}
           width={width}
           hiddenLogLevels={hiddenLogLevels}
+          getRowContext={this.getLogRowContext}
         />
       </Panel>
     );
@@ -106,7 +128,7 @@ export class LogsContainer extends PureComponent<LogsContainerProps> {
 function mapStateToProps(state: StoreState, { exploreId }) {
   const explore = state.explore;
   const item: ExploreItemState = explore[exploreId];
-  const { logsHighlighterExpressions, logsResult, logIsLoading, scanning, scanRange, range } = item;
+  const { logsHighlighterExpressions, logsResult, logIsLoading, scanning, scanRange, range, datasourceInstance } = item;
   const loading = logIsLoading;
   const { dedupStrategy } = exploreItemUIStateSelector(item);
   const hiddenLogLevels = new Set(item.hiddenLogLevels);
@@ -124,6 +146,7 @@ function mapStateToProps(state: StoreState, { exploreId }) {
     dedupStrategy,
     hiddenLogLevels,
     dedupedResult,
+    datasourceInstance,
   };
 }
 

+ 0 - 1
public/app/features/explore/state/reducers.ts

@@ -379,7 +379,6 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
       const { queryIntervals } = state;
       const { result, resultType, latency } = action.payload;
       const results = calculateResultsFromQueryTransactions(result, resultType, queryIntervals.intervalMs);
-
       return {
         ...state,
         graphResult: resultType === 'Graph' ? results.graphResult : state.graphResult,

+ 78 - 0
public/app/plugins/datasource/loki/datasource.ts

@@ -21,6 +21,7 @@ import { LokiQuery, LokiOptions } from './types';
 import { BackendSrv } from 'app/core/services/backend_srv';
 import { TemplateSrv } from 'app/features/templating/template_srv';
 import { safeStringifyValue } from 'app/core/utils/explore';
+import { LogRowModel } from 'app/core/logs_model';
 
 export const DEFAULT_MAX_LINES = 1000;
 
@@ -187,6 +188,83 @@ export class LokiDatasource extends DataSourceApi<LokiQuery, LokiOptions> {
     return Math.ceil(date.valueOf() * 1e6);
   }
 
+  prepareLogRowContextQueryTargets = (row: LogRowModel, limit: number) => {
+    const query = Object.keys(row.labels)
+      .map(label => {
+        return `${label}="${row.labels[label]}"`;
+      })
+      .join(',');
+    const contextTimeBuffer = 2 * 60 * 60 * 1000 * 1e6; // 2h buffer
+    const timeEpochNs = row.timeEpochMs * 1e6;
+
+    const commontTargetOptons = {
+      limit,
+      query: `{${query}}`,
+    };
+    return [
+      // Target for "before" context
+      {
+        ...commontTargetOptons,
+        start: timeEpochNs - contextTimeBuffer,
+        end: timeEpochNs,
+        direction: 'BACKWARD',
+      },
+      // Target for "after" context
+      {
+        ...commontTargetOptons,
+        start: timeEpochNs, // TODO: We should add 1ns here for the original row not no be included in the result
+        end: timeEpochNs + contextTimeBuffer,
+        direction: 'FORWARD',
+      },
+    ];
+  };
+
+  getLogRowContext = (row: LogRowModel, limit?: number) => {
+    // Preparing two targets, for preceeding and following log queries
+    const targets = this.prepareLogRowContextQueryTargets(row, limit || 10);
+
+    return Promise.all(
+      targets.map(target => {
+        return this._request('/api/prom/query', target).catch(e => {
+          const error: DataQueryError = {
+            message: 'Error during context query. Please check JS console logs.',
+            status: e.status,
+            statusText: e.statusText,
+          };
+          return error;
+        });
+      })
+    ).then((results: any[]) => {
+      const series: Array<Array<SeriesData | DataQueryError>> = [];
+      const emptySeries = {
+        fields: [],
+        rows: [],
+      } as SeriesData;
+
+      for (let i = 0; i < results.length; i++) {
+        const result = results[i];
+        series[i] = [];
+        if (result.data) {
+          for (const stream of result.data.streams || []) {
+            const seriesData = logStreamToSeriesData(stream);
+            series[i].push(seriesData);
+          }
+        } else {
+          series[i].push(result);
+        }
+      }
+
+      // Following context logs are requested in "forward" direction.
+      // This means, that we need to reverse those to make them sorted
+      // in descending order (by timestamp)
+      if (series[1][0] && (series[1][0] as SeriesData).rows) {
+        (series[1][0] as SeriesData).rows.reverse();
+      }
+
+      return { data: [series[0][0] || emptySeries, series[1][0] || emptySeries] };
+    });
+  };
+
   testDatasource() {
     return this._request('/api/prom/label')
       .then(res => {

+ 1 - 0
public/app/types/explore.ts

@@ -179,6 +179,7 @@ export interface ExploreItemState {
    * Log query result to be displayed in the logs result viewer.
    */
   logsResult?: LogsModel;
+
   /**
    * Query intervals for graph queries to determine how many datapoints to return.
    * Needs to be updated when `datasourceInstance` or `containerWidth` is changed.

+ 1 - 0
public/sass/components/_panel_logs.scss

@@ -73,6 +73,7 @@ $column-horizontal-spacing: 10px;
     padding-right: $column-horizontal-spacing;
     border-top: 1px solid transparent;
     border-bottom: 1px solid transparent;
+    height: 100%;
   }
 
   &:hover {

+ 126 - 5
yarn.lock

@@ -4054,6 +4054,11 @@ boolbase@^1.0.0, boolbase@~1.0.0:
   resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
   integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
 
+bowser@^1.7.3:
+  version "1.9.4"
+  resolved "https://registry.yarnpkg.com/bowser/-/bowser-1.9.4.tgz#890c58a2813a9d3243704334fa81b96a5c150c9a"
+  integrity sha512-9IdMmj2KjigRq6oWhmwv1W36pDuA4STQZ8q6YO9um+x07xgYNCD3Oou+WP/3L1HNz7iqythGet3/p4wvc8AAwQ==
+
 boxen@^1.2.1:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b"
@@ -5143,7 +5148,7 @@ copy-descriptor@^0.1.0:
   resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
   integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
 
-copy-to-clipboard@^3.0.8:
+copy-to-clipboard@^3.0.8, copy-to-clipboard@^3.1.0:
   version "3.2.0"
   resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.2.0.tgz#d2724a3ccbfed89706fac8a894872c979ac74467"
   integrity sha512-eOZERzvCmxS8HWzugj4Uxl8OJxa7T2k1Gi0X5qavwydHIfuSHq2dTD09LOg/XyGq4Zpb5IsR/2OJ5lbOegz78w==
@@ -5366,6 +5371,14 @@ css-declaration-sorter@^4.0.1:
     postcss "^7.0.1"
     timsort "^0.3.0"
 
+css-in-js-utils@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/css-in-js-utils/-/css-in-js-utils-2.0.1.tgz#3b472b398787291b47cfe3e44fecfdd9e914ba99"
+  integrity sha512-PJF0SpJT+WdbVVt0AOYp9C8GnuruRlL/UFW7932nLWmFLQTaWEzTBQEx7/hn4BuV+WON75iAViSUJLiU3PKbpA==
+  dependencies:
+    hyphenate-style-name "^1.0.2"
+    isobject "^3.0.1"
+
 css-loader@2.1.1, css-loader@^2.1.0:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-2.1.1.tgz#d8254f72e412bb2238bb44dd674ffbef497333ea"
@@ -5416,7 +5429,7 @@ css-tree@1.0.0-alpha.28:
     mdn-data "~1.1.0"
     source-map "^0.5.3"
 
-css-tree@1.0.0-alpha.29:
+css-tree@1.0.0-alpha.29, css-tree@^1.0.0-alpha.28:
   version "1.0.0-alpha.29"
   resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.29.tgz#3fa9d4ef3142cbd1c301e7664c1f352bd82f5a39"
   integrity sha512-sRNb1XydwkW9IOci6iB2xmy8IGCj6r/fr+JWitvJ2JxQRPzN3T4AGGVWCMlVmVwM1gtgALJRmGIlWv5ppnGGkg==
@@ -5541,7 +5554,7 @@ cssstyle@^1.0.0:
   dependencies:
     cssom "0.3.x"
 
-csstype@^2.2.0, csstype@^2.5.2, csstype@^2.5.7:
+csstype@^2.2.0, csstype@^2.5.2, csstype@^2.5.5, csstype@^2.5.7:
   version "2.6.4"
   resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.4.tgz#d585a6062096e324e7187f80e04f92bd0f00e37f"
   integrity sha512-lAJUJP3M6HxFXbqtGRc0iZrdyeN+WzOWeY0q/VnFzI+kqVrYIzC7bWlKqCW7oCIdzoPkvfp82EVvrTlQ8zsWQg==
@@ -6801,6 +6814,13 @@ error-ex@^1.2.0, error-ex@^1.3.1:
   dependencies:
     is-arrayish "^0.2.1"
 
+error-stack-parser@^2.0.1:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.0.2.tgz#4ae8dbaa2bf90a8b450707b9149dcabca135520d"
+  integrity sha512-E1fPutRDdIj/hohG0UpT5mayXNCxXP9d+snxFsPU9X0XgccOumKraa3juDMwTUyi7+Bu5+mCGagjg4IYeNbOdw==
+  dependencies:
+    stackframe "^1.0.4"
+
 es-abstract@^1.10.0, es-abstract@^1.11.0, es-abstract@^1.12.0, es-abstract@^1.13.0, es-abstract@^1.4.3, es-abstract@^1.5.0, es-abstract@^1.5.1, es-abstract@^1.7.0, es-abstract@^1.9.0:
   version "1.13.0"
   resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.13.0.tgz#ac86145fdd5099d8dd49558ccba2eaf9b88e24e9"
@@ -7355,6 +7375,11 @@ fast-text-encoding@^1.0.0:
   resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.0.tgz#3e5ce8293409cfaa7177a71b9ca84e1b1e6f25ef"
   integrity sha512-R9bHCvweUxxwkDwhjav5vxpFvdPGlVngtqmx4pIZfSUhM/Q4NiIUHB456BAf+Q1Nwu3HEZYONtu+Rya+af4jiQ==
 
+fastest-stable-stringify@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/fastest-stable-stringify/-/fastest-stable-stringify-1.0.1.tgz#9122d406d4c9d98bea644a6b6853d5874b87b028"
+  integrity sha1-kSLUBtTJ2YvqZEpraFPVh0uHsCg=
+
 fastparse@^1.1.1:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.2.tgz#91728c5a5942eced8531283c79441ee4122c35a9"
@@ -8837,6 +8862,11 @@ husky@1.3.1:
     run-node "^1.0.0"
     slash "^2.0.0"
 
+hyphenate-style-name@^1.0.2:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.3.tgz#097bb7fa0b8f1a9cf0bd5c734cf95899981a9b48"
+  integrity sha512-EcuixamT82oplpoJ2XU4pDtKGWQ7b00CD9f1ug9IaQ3p1bkHMiKCZ9ut9QDI6qsa6cpUuB+A/I+zLtdNK4n2DQ==
+
 iconv-lite@0.4, iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13:
   version "0.4.24"
   resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
@@ -9024,6 +9054,14 @@ init-package-json@^1.10.3:
     validate-npm-package-license "^3.0.1"
     validate-npm-package-name "^3.0.0"
 
+inline-style-prefixer@^4.0.0:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/inline-style-prefixer/-/inline-style-prefixer-4.0.2.tgz#d390957d26f281255fe101da863158ac6eb60911"
+  integrity sha512-N8nVhwfYga9MiV9jWlwfdj1UDIaZlBFu4cJSJkIr7tZX7sHpHhGR5su1qdpW+7KPL8ISTvCIkcaFi/JdBknvPg==
+  dependencies:
+    bowser "^1.7.3"
+    css-in-js-utils "^2.0.0"
+
 inquirer@6.2.1:
   version "6.2.1"
   resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.2.1.tgz#9943fc4882161bdb0b0c9276769c75b32dbfcd52"
@@ -11565,6 +11603,20 @@ nan@^2.10.0, nan@^2.12.1, nan@^2.6.2:
   resolved "https://registry.yarnpkg.com/nan/-/nan-2.13.2.tgz#f51dc7ae66ba7d5d55e1e6d4d8092e802c9aefe7"
   integrity sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==
 
+nano-css@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/nano-css/-/nano-css-5.1.0.tgz#03c2b4ea2edefd445ac0c0e0f2565ea62e2aa81a"
+  integrity sha512-08F1rBmp0JuAteOR/uk/c40q/+UxWr224m/ZCHjjgy8dhkFQptvNwj/408KYQc13PIV9aGvqmtUD49PqBB5Ppg==
+  dependencies:
+    css-tree "^1.0.0-alpha.28"
+    csstype "^2.5.5"
+    fastest-stable-stringify "^1.0.1"
+    inline-style-prefixer "^4.0.0"
+    rtl-css-js "^1.9.0"
+    sourcemap-codec "^1.4.1"
+    stacktrace-js "^2.0.0"
+    stylis "3.5.0"
+
 nanomatch@^1.2.9:
   version "1.2.13"
   resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
@@ -14100,7 +14152,7 @@ react-error-overlay@^5.1.4:
   resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-5.1.5.tgz#884530fd055476c764eaa8ab13b8ecf1f57bbf2c"
   integrity sha512-O9JRum1Zq/qCPFH5qVEvDDrVun8Jv9vbHtZXCR1EuRj9sKg1xJTlHxBzU6AkCzpvxRLuiY4OKImy3cDLQ+UTdg==
 
-react-fast-compare@^2.0.2:
+react-fast-compare@^2.0.2, react-fast-compare@^2.0.4:
   version "2.0.4"
   resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
   integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
@@ -14379,6 +14431,19 @@ react-transition-group@^2.2.1:
     prop-types "^15.6.2"
     react-lifecycles-compat "^3.0.4"
 
+react-use@9.0.0:
+  version "9.0.0"
+  resolved "https://registry.yarnpkg.com/react-use/-/react-use-9.0.0.tgz#142bec53fa465db2a6e43c68a8c9ef2acc000592"
+  integrity sha512-jlXJneB96yl4VvAXDKyE6cmdIeWk0cO7Gomh870Qu0vXZ9YM2JjjR09E9vIPPPI2M27RWo2dZKXspv44Wxtoog==
+  dependencies:
+    copy-to-clipboard "^3.1.0"
+    nano-css "^5.1.0"
+    react-fast-compare "^2.0.4"
+    react-wait "^0.3.0"
+    screenfull "^4.1.0"
+    throttle-debounce "^2.0.1"
+    ts-easing "^0.2.0"
+
 react-virtualized@9.21.0:
   version "9.21.0"
   resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.21.0.tgz#8267c40ffb48db35b242a36dea85edcf280a6506"
@@ -14391,6 +14456,11 @@ react-virtualized@9.21.0:
     prop-types "^15.6.0"
     react-lifecycles-compat "^3.0.4"
 
+react-wait@^0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/react-wait/-/react-wait-0.3.0.tgz#0cdd4d919012451a5bc3ab0a16d00c6fd9a8c10b"
+  integrity sha512-kB5x/kMKWcn0uVr9gBdNz21/oGbQwEQnF3P9p6E9yLfJ9DRcKS0fagbgYMFI0YFOoyKDj+2q6Rwax0kTYJF37g==
+
 react-window@1.7.1:
   version "1.7.1"
   resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.7.1.tgz#c1db640415b97b85bc0a1c66eb82dadabca39b86"
@@ -15206,6 +15276,13 @@ rsvp@^4.8.4:
   resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.4.tgz#b50e6b34583f3dd89329a2f23a8a2be072845911"
   integrity sha512-6FomvYPfs+Jy9TfXmBpBuMWNH94SgCsZmJKcanySzgNNP6LjWxBvyLTa9KaMfDDM5oxRfrKDB0r/qeRsLwnBfA==
 
+rtl-css-js@^1.9.0:
+  version "1.11.0"
+  resolved "https://registry.yarnpkg.com/rtl-css-js/-/rtl-css-js-1.11.0.tgz#a7151930ef9d54656607d754ebb172ddfc9ef836"
+  integrity sha512-YnZ6jWxZxlWlcQAGF9vOmiF9bEmoQmSHE+wsrsiILkdK9HqiRPAIll4SY/QDzbvEu2lB2h62+hfg3TYzjnldbA==
+  dependencies:
+    "@babel/runtime" "^7.1.2"
+
 run-async@^0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389"
@@ -15400,6 +15477,11 @@ schema-utils@^1.0.0:
     ajv-errors "^1.0.0"
     ajv-keywords "^3.1.0"
 
+screenfull@^4.1.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/screenfull/-/screenfull-4.2.0.tgz#d5252a5a0f56504719abbed9ebbcd9208115da03"
+  integrity sha512-qpyI9XbwuMJElWRP5vTgxkFAl4k7HpyhIqBFOZEwX9QBXn0MAuRSpn7LOc6/4CeSwoz61oBu1VPV+2fbIWC+5Q==
+
 scss-tokenizer@^0.2.3:
   version "0.2.3"
   resolved "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz#8eb06db9a9723333824d3f5530641149847ce5d1"
@@ -15978,7 +16060,7 @@ source-map@^0.7.2, source-map@^0.7.3:
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
   integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==
 
-sourcemap-codec@^1.4.4:
+sourcemap-codec@^1.4.1, sourcemap-codec@^1.4.4:
   version "1.4.4"
   resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.4.tgz#c63ea927c029dd6bd9a2b7fa03b3fec02ad56e9f"
   integrity sha512-CYAPYdBu34781kLHkaW3m6b/uUSyMOC2R61gcYMWooeuaGtjof86ZA/8T+qVPPt7np1085CR9hmMGrySwEc8Xg==
@@ -16093,6 +16175,13 @@ stable@^0.1.8, stable@~0.1.3, stable@~0.1.5:
   resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"
   integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==
 
+stack-generator@^2.0.1:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/stack-generator/-/stack-generator-2.0.3.tgz#bb74385c67ffc4ccf3c4dee5831832d4e509c8a0"
+  integrity sha512-kdzGoqrnqsMxOEuXsXyQTmvWXZmG0f3Ql2GDx5NtmZs59sT2Bt9Vdyq0XdtxUi58q/+nxtbF9KOQ9HkV1QznGg==
+  dependencies:
+    stackframe "^1.0.4"
+
 stack-parser@^0.0.1:
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/stack-parser/-/stack-parser-0.0.1.tgz#7d3b63a17887e9e2c2bf55dbd3318fe34a39d1e7"
@@ -16103,6 +16192,28 @@ stack-utils@^1.0.1:
   resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.2.tgz#33eba3897788558bebfc2db059dc158ec36cebb8"
   integrity sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA==
 
+stackframe@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.0.4.tgz#357b24a992f9427cba6b545d96a14ed2cbca187b"
+  integrity sha512-to7oADIniaYwS3MhtCa/sQhrxidCCQiF/qp4/m5iN3ipf0Y7Xlri0f6eG29r08aL7JYl8n32AF3Q5GYBZ7K8vw==
+
+stacktrace-gps@^3.0.1:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/stacktrace-gps/-/stacktrace-gps-3.0.2.tgz#33f8baa4467323ab2bd1816efa279942ba431ccc"
+  integrity sha512-9o+nWhiz5wFnrB3hBHs2PTyYrS60M1vvpSzHxwxnIbtY2q9Nt51hZvhrG1+2AxD374ecwyS+IUwfkHRE/2zuGg==
+  dependencies:
+    source-map "0.5.6"
+    stackframe "^1.0.4"
+
+stacktrace-js@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/stacktrace-js/-/stacktrace-js-2.0.0.tgz#776ca646a95bc6c6b2b90776536a7fc72c6ddb58"
+  integrity sha1-d2ymRqlbxsayuQd2U2p/xyxt21g=
+  dependencies:
+    error-stack-parser "^2.0.1"
+    stack-generator "^2.0.1"
+    stacktrace-gps "^3.0.1"
+
 staged-git-files@1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/staged-git-files/-/staged-git-files-1.1.2.tgz#4326d33886dc9ecfa29a6193bf511ba90a46454b"
@@ -16410,6 +16521,11 @@ stylis-rule-sheet@^0.0.10:
   resolved "https://registry.yarnpkg.com/stylis-rule-sheet/-/stylis-rule-sheet-0.0.10.tgz#44e64a2b076643f4b52e5ff71efc04d8c3c4a430"
   integrity sha512-nTbZoaqoBnmK+ptANthb10ZRZOGC+EmTLLUxeYIuHNkEKcmKgXX1XWKkUBT2Ac4es3NybooPe0SmvKdhKJZAuw==
 
+stylis@3.5.0:
+  version "3.5.0"
+  resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.5.0.tgz#016fa239663d77f868fef5b67cf201c4b7c701e1"
+  integrity sha512-pP7yXN6dwMzAR29Q0mBrabPCe0/mNO1MSr93bhay+hcZondvMMTpeGyd8nbhYJdyperNT2DRxONQuUGcJr5iPw==
+
 stylis@^3.5.0:
   version "3.5.4"
   resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.5.4.tgz#f665f25f5e299cf3d64654ab949a57c768b73fbe"
@@ -16665,6 +16781,11 @@ throat@^4.0.0:
   resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a"
   integrity sha1-iQN8vJLFarGJJua6TLsgDhVnKmo=
 
+throttle-debounce@^2.0.1:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-2.1.0.tgz#257e648f0a56bd9e54fe0f132c4ab8611df4e1d5"
+  integrity sha512-AOvyNahXQuU7NN+VVvOOX+uW6FPaWdAOdRP5HfwYxAfCzXTFKRMoIMk+n+po318+ktcChx+F1Dd91G3YHeMKyg==
+
 throttleit@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c"