mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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
This commit is contained in:
parent
ae1df1cf89
commit
12e0616413
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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
public/app/features/explore/LogRowContext.tsx
Normal file
239
public/app/features/explore/LogRowContext.tsx
Normal file
@ -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
public/app/features/explore/LogRowContextProvider.tsx
Normal file
104
public/app/features/explore/LogRowContextProvider.tsx
Normal file
@ -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),
|
||||
});
|
||||
};
|
@ -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}
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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 => {
|
||||
|
@ -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.
|
||||
|
@ -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 {
|
||||
|
131
yarn.lock
131
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"
|
||||
|
Loading…
Reference in New Issue
Block a user