mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Refactor: Split LogRow component (#19471)
This commit is contained in:
@@ -6,8 +6,9 @@ import { LogLabelStats } from './LogLabelStats';
|
||||
import { GrafanaTheme, Themeable } from '../../types/theme';
|
||||
import { selectThemeVariant } from '../../themes/selectThemeVariant';
|
||||
import { withTheme } from '../../themes/ThemeContext';
|
||||
import { stylesFactory } from '../../themes';
|
||||
|
||||
const getStyles = (theme: GrafanaTheme) => {
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
return {
|
||||
logsLabel: css`
|
||||
label: logs-label;
|
||||
@@ -43,7 +44,7 @@ const getStyles = (theme: GrafanaTheme) => {
|
||||
box-shadow: 0 0 20px ${selectThemeVariant({ light: theme.colors.white, dark: theme.colors.black }, theme.type)};
|
||||
`,
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
interface Props extends Themeable {
|
||||
value: string;
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import React, { FunctionComponent, useContext } from 'react';
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import { css, cx } from 'emotion';
|
||||
import { Labels, LogRowModel } from '@grafana/data';
|
||||
|
||||
import { LogLabel } from './LogLabel';
|
||||
import { GrafanaTheme } from '../../types/theme';
|
||||
import { ThemeContext } from '../../themes/ThemeContext';
|
||||
import { stylesFactory } from '../../themes';
|
||||
|
||||
const getStyles = (theme: GrafanaTheme) => ({
|
||||
const getStyles = stylesFactory(() => ({
|
||||
logsLabels: css`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
`,
|
||||
});
|
||||
}));
|
||||
|
||||
interface Props {
|
||||
labels: Labels;
|
||||
@@ -21,8 +20,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export const LogLabels: FunctionComponent<Props> = ({ getRows, labels, onClickLabel, plain }) => {
|
||||
const theme = useContext(ThemeContext);
|
||||
const styles = getStyles(theme);
|
||||
const styles = getStyles();
|
||||
|
||||
return (
|
||||
<span className={cx([styles.logsLabels])}>
|
||||
|
||||
@@ -1,32 +1,18 @@
|
||||
import React, { PureComponent, FunctionComponent, useContext } from 'react';
|
||||
import _ from 'lodash';
|
||||
// @ts-ignore
|
||||
import Highlighter from 'react-highlight-words';
|
||||
import {
|
||||
LogRowModel,
|
||||
LogLabelStatsModel,
|
||||
LogsParser,
|
||||
TimeZone,
|
||||
calculateFieldStats,
|
||||
getParser,
|
||||
findHighlightChunksInText,
|
||||
} from '@grafana/data';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { css, cx } from 'emotion';
|
||||
import { DataQueryResponse, GrafanaTheme, selectThemeVariant, ThemeContext } from '../../index';
|
||||
import React, { PureComponent } from 'react';
|
||||
import { LogRowModel, TimeZone } from '@grafana/data';
|
||||
import { cx } from 'emotion';
|
||||
import { DataQueryResponse } from '../../index';
|
||||
import {
|
||||
LogRowContextRows,
|
||||
LogRowContextQueryErrors,
|
||||
HasMoreContextRows,
|
||||
LogRowContextProvider,
|
||||
} from './LogRowContextProvider';
|
||||
import { LogRowContext } from './LogRowContext';
|
||||
import { LogLabels } from './LogLabels';
|
||||
import { LogMessageAnsi } from './LogMessageAnsi';
|
||||
import { LogLabelStats } from './LogLabelStats';
|
||||
import { Themeable } from '../../types/theme';
|
||||
import { withTheme } from '../../themes/index';
|
||||
import { getLogRowStyles } from './getLogRowStyles';
|
||||
import { LogRowMessage } from './LogRowMessage';
|
||||
|
||||
interface Props extends Themeable {
|
||||
highlighterExpressions?: string[];
|
||||
@@ -42,61 +28,9 @@ interface Props extends Themeable {
|
||||
}
|
||||
|
||||
interface State {
|
||||
fieldCount: number;
|
||||
fieldLabel: string;
|
||||
fieldStats: LogLabelStatsModel[];
|
||||
fieldValue: string;
|
||||
parsed: boolean;
|
||||
parser?: LogsParser;
|
||||
parsedFieldHighlights: string[];
|
||||
showFieldStats: boolean;
|
||||
showContext: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a highlighted field.
|
||||
* When hovering, a stats icon is shown.
|
||||
*/
|
||||
const FieldHighlight = (onClick: any): FunctionComponent<any> => (props: any) => {
|
||||
const theme = useContext(ThemeContext);
|
||||
const style = getLogRowStyles(theme);
|
||||
return (
|
||||
<span className={props.className} style={props.style}>
|
||||
{props.children}
|
||||
<span
|
||||
className={cx([style, 'logs-row__field-highlight--icon', 'fa fa-signal'])}
|
||||
onClick={() => onClick(props.children)}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
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 as tinycolor.ColorInput)
|
||||
.setAlpha(0.7)
|
||||
.toRgbString()};
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders a log line.
|
||||
*
|
||||
@@ -105,95 +39,10 @@ const getLogRowWithContextStyles = (theme: GrafanaTheme, state: State) => {
|
||||
* When the user requests stats for a field, they will be calculated and rendered below the row.
|
||||
*/
|
||||
class UnThemedLogRow extends PureComponent<Props, State> {
|
||||
mouseMessageTimer: number | null = null;
|
||||
|
||||
state: any = {
|
||||
fieldCount: 0,
|
||||
fieldLabel: null,
|
||||
fieldStats: null,
|
||||
fieldValue: null,
|
||||
parsed: false,
|
||||
parser: undefined,
|
||||
parsedFieldHighlights: [],
|
||||
showFieldStats: false,
|
||||
state: State = {
|
||||
showContext: false,
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
this.clearMouseMessageTimer();
|
||||
}
|
||||
|
||||
onClickClose = () => {
|
||||
this.setState({ showFieldStats: false });
|
||||
};
|
||||
|
||||
onClickHighlight = (fieldText: string) => {
|
||||
const { getRows } = this.props;
|
||||
const { parser } = this.state;
|
||||
const allRows = getRows();
|
||||
|
||||
// Build value-agnostic row matcher based on the field label
|
||||
const fieldLabel = parser.getLabelFromField(fieldText);
|
||||
const fieldValue = parser.getValueFromField(fieldText);
|
||||
const matcher = parser.buildMatcher(fieldLabel);
|
||||
const fieldStats = calculateFieldStats(allRows, matcher);
|
||||
const fieldCount = fieldStats.reduce((sum, stat) => sum + stat.count, 0);
|
||||
|
||||
this.setState({ fieldCount, fieldLabel, fieldStats, fieldValue, showFieldStats: true });
|
||||
};
|
||||
|
||||
onMouseOverMessage = () => {
|
||||
if (this.state.showContext || this.isTextSelected()) {
|
||||
// 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 = window.setTimeout(this.parseMessage, 500);
|
||||
};
|
||||
|
||||
onMouseOutMessage = () => {
|
||||
if (this.state.showContext) {
|
||||
// See comment in onMouseOverMessage method
|
||||
return;
|
||||
}
|
||||
this.clearMouseMessageTimer();
|
||||
this.setState({ parsed: false });
|
||||
};
|
||||
|
||||
clearMouseMessageTimer = () => {
|
||||
if (this.mouseMessageTimer) {
|
||||
clearTimeout(this.mouseMessageTimer);
|
||||
}
|
||||
};
|
||||
|
||||
parseMessage = () => {
|
||||
if (!this.state.parsed) {
|
||||
const { row } = this.props;
|
||||
const parser = getParser(row.entry);
|
||||
if (parser) {
|
||||
// Use parser to highlight detected fields
|
||||
const parsedFieldHighlights = parser.getFields(this.props.row.entry);
|
||||
this.setState({ parsedFieldHighlights, parsed: true, parser });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
isTextSelected() {
|
||||
if (!window.getSelection) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const selection = window.getSelection();
|
||||
|
||||
if (!selection) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return selection.anchorNode !== null && selection.isCollapsed === false;
|
||||
}
|
||||
|
||||
toggleContext = () => {
|
||||
this.setState(state => {
|
||||
return {
|
||||
@@ -202,11 +51,6 @@ class UnThemedLogRow extends PureComponent<Props, State> {
|
||||
});
|
||||
};
|
||||
|
||||
onContextToggle = (e: React.SyntheticEvent<HTMLElement>) => {
|
||||
e.stopPropagation();
|
||||
this.toggleContext();
|
||||
};
|
||||
|
||||
renderLogRow(
|
||||
context?: LogRowContextRows,
|
||||
errors?: LogRowContextQueryErrors,
|
||||
@@ -224,143 +68,49 @@ class UnThemedLogRow extends PureComponent<Props, State> {
|
||||
showTime,
|
||||
theme,
|
||||
} = this.props;
|
||||
const {
|
||||
fieldCount,
|
||||
fieldLabel,
|
||||
fieldStats,
|
||||
fieldValue,
|
||||
parsed,
|
||||
parsedFieldHighlights,
|
||||
showFieldStats,
|
||||
showContext,
|
||||
} = this.state;
|
||||
const { showContext } = this.state;
|
||||
const style = getLogRowStyles(theme, row.logLevel);
|
||||
const { entry, hasAnsi, raw } = row;
|
||||
const previewHighlights = highlighterExpressions && !_.isEqual(highlighterExpressions, row.searchWords);
|
||||
const highlights = previewHighlights ? highlighterExpressions : row.searchWords;
|
||||
const needsHighlighter = highlights && highlights.length > 0 && highlights[0] && highlights[0].length > 0;
|
||||
const highlightClassName = previewHighlights
|
||||
? cx([style.logsRowMatchHighLight, style.logsRowMatchHighLightPreview])
|
||||
: cx([style.logsRowMatchHighLight]);
|
||||
|
||||
const showUtc = timeZone === 'utc';
|
||||
|
||||
return (
|
||||
<ThemeContext.Consumer>
|
||||
{theme => {
|
||||
const styles = this.state.showContext
|
||||
? cx(logRowStyles, getLogRowWithContextStyles(theme, this.state).row)
|
||||
: logRowStyles;
|
||||
return (
|
||||
<div className={cx([style.logsRow])}>
|
||||
{showDuplicates && (
|
||||
<div className={cx([style.logsRowDuplicates])}>
|
||||
{row.duplicates && row.duplicates > 0 ? `${row.duplicates + 1}x` : null}
|
||||
</div>
|
||||
)}
|
||||
<div className={cx([style.logsRowLevel])} />
|
||||
{showTime && showUtc && (
|
||||
<div className={cx([style.logsRowLocalTime])} title={`Local: ${row.timeLocal} (${row.timeFromNow})`}>
|
||||
{row.timeUtc}
|
||||
</div>
|
||||
)}
|
||||
{showTime && !showUtc && (
|
||||
<div className={cx([style.logsRowLocalTime])} title={`${row.timeUtc} (${row.timeFromNow})`}>
|
||||
{row.timeLocal}
|
||||
</div>
|
||||
)}
|
||||
{showLabels && (
|
||||
<div className={cx([style.logsRowLabels])}>
|
||||
<LogLabels
|
||||
getRows={getRows}
|
||||
labels={row.uniqueLabels ? row.uniqueLabels : {}}
|
||||
onClickLabel={onClickLabel}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={cx([style.logsRowMessage])}
|
||||
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
|
||||
style={{ whiteSpace: 'pre-wrap' }}
|
||||
autoEscape
|
||||
highlightTag={FieldHighlight(this.onClickHighlight)}
|
||||
textToHighlight={entry}
|
||||
searchWords={parsedFieldHighlights}
|
||||
highlightClassName={cx([style.logsRowFieldHighLight])}
|
||||
/>
|
||||
)}
|
||||
{!parsed && needsHighlighter && (
|
||||
<Highlighter
|
||||
style={{ whiteSpace: 'pre-wrap' }}
|
||||
textToHighlight={entry}
|
||||
searchWords={highlights}
|
||||
findChunks={findHighlightChunksInText}
|
||||
highlightClassName={highlightClassName}
|
||||
/>
|
||||
)}
|
||||
{hasAnsi && !parsed && !needsHighlighter && <LogMessageAnsi value={raw} />}
|
||||
{!hasAnsi && !parsed && !needsHighlighter && entry}
|
||||
{showFieldStats && (
|
||||
<div className={cx([style.logsRowStats])}>
|
||||
<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;
|
||||
.${style.logsRow}:hover & {
|
||||
visibility: visible;
|
||||
margin-left: 10px;
|
||||
text-decoration: underline;
|
||||
}
|
||||
`}
|
||||
>
|
||||
{showContext ? 'Hide' : 'Show'} context
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</ThemeContext.Consumer>
|
||||
<div className={cx([style.logsRow])}>
|
||||
{showDuplicates && (
|
||||
<div className={cx([style.logsRowDuplicates])}>
|
||||
{row.duplicates && row.duplicates > 0 ? `${row.duplicates + 1}x` : null}
|
||||
</div>
|
||||
)}
|
||||
<div className={cx([style.logsRowLevel])} />
|
||||
{showTime && showUtc && (
|
||||
<div className={cx([style.logsRowLocalTime])} title={`Local: ${row.timeLocal} (${row.timeFromNow})`}>
|
||||
{row.timeUtc}
|
||||
</div>
|
||||
)}
|
||||
{showTime && !showUtc && (
|
||||
<div className={cx([style.logsRowLocalTime])} title={`${row.timeUtc} (${row.timeFromNow})`}>
|
||||
{row.timeLocal}
|
||||
</div>
|
||||
)}
|
||||
{showLabels && (
|
||||
<div className={cx([style.logsRowLabels])}>
|
||||
<LogLabels
|
||||
getRows={getRows}
|
||||
labels={row.uniqueLabels ? row.uniqueLabels : {}}
|
||||
onClickLabel={onClickLabel}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<LogRowMessage
|
||||
highlighterExpressions={highlighterExpressions}
|
||||
row={row}
|
||||
getRows={getRows}
|
||||
errors={errors}
|
||||
hasMoreContextRows={hasMoreContextRows}
|
||||
updateLimit={updateLimit}
|
||||
context={context}
|
||||
showContext={showContext}
|
||||
onToggleContext={this.toggleContext}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
297
packages/grafana-ui/src/components/Logs/LogRowMessage.tsx
Normal file
297
packages/grafana-ui/src/components/Logs/LogRowMessage.tsx
Normal file
@@ -0,0 +1,297 @@
|
||||
import React, { PureComponent, FunctionComponent, useContext } from 'react';
|
||||
import _ from 'lodash';
|
||||
// @ts-ignore
|
||||
import Highlighter from 'react-highlight-words';
|
||||
import {
|
||||
LogRowModel,
|
||||
LogLabelStatsModel,
|
||||
LogsParser,
|
||||
calculateFieldStats,
|
||||
getParser,
|
||||
findHighlightChunksInText,
|
||||
} from '@grafana/data';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { css, cx } from 'emotion';
|
||||
import { GrafanaTheme, selectThemeVariant, ThemeContext } from '../../index';
|
||||
import { LogRowContextQueryErrors, HasMoreContextRows, LogRowContextRows } from './LogRowContextProvider';
|
||||
import { LogRowContext } from './LogRowContext';
|
||||
import { LogMessageAnsi } from './LogMessageAnsi';
|
||||
import { LogLabelStats } from './LogLabelStats';
|
||||
import { Themeable } from '../../types/theme';
|
||||
import { withTheme } from '../../themes/index';
|
||||
import { getLogRowStyles } from './getLogRowStyles';
|
||||
import { stylesFactory } from '../../themes/stylesFactory';
|
||||
|
||||
interface Props extends Themeable {
|
||||
highlighterExpressions?: string[];
|
||||
row: LogRowModel;
|
||||
getRows: () => LogRowModel[];
|
||||
errors?: LogRowContextQueryErrors;
|
||||
hasMoreContextRows?: HasMoreContextRows;
|
||||
updateLimit?: () => void;
|
||||
context?: LogRowContextRows;
|
||||
showContext: boolean;
|
||||
onToggleContext: () => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
fieldCount: number;
|
||||
fieldLabel: string | null;
|
||||
fieldStats: LogLabelStatsModel[] | null;
|
||||
fieldValue: string | null;
|
||||
parsed: boolean;
|
||||
parser?: LogsParser;
|
||||
parsedFieldHighlights: string[];
|
||||
showFieldStats: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a highlighted field.
|
||||
* When hovering, a stats icon is shown.
|
||||
*/
|
||||
const FieldHighlight = (onClick: any): FunctionComponent<any> => (props: any) => {
|
||||
const theme = useContext(ThemeContext);
|
||||
const style = getLogRowStyles(theme);
|
||||
return (
|
||||
<span className={props.className} style={props.style}>
|
||||
{props.children}
|
||||
<span
|
||||
className={cx([style, 'logs-row__field-highlight--icon', 'fa fa-signal'])}
|
||||
onClick={() => onClick(props.children)}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
const outlineColor = selectThemeVariant(
|
||||
{
|
||||
light: theme.colors.white,
|
||||
dark: theme.colors.black,
|
||||
},
|
||||
theme.type
|
||||
);
|
||||
|
||||
return {
|
||||
positionRelative: css`
|
||||
label: positionRelative;
|
||||
position: relative;
|
||||
`,
|
||||
rowWithContext: css`
|
||||
label: rowWithContext;
|
||||
z-index: 1;
|
||||
outline: 9999px solid
|
||||
${tinycolor(outlineColor as tinycolor.ColorInput)
|
||||
.setAlpha(0.7)
|
||||
.toRgbString()};
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
class UnThemedLogRowMessage extends PureComponent<Props, State> {
|
||||
mouseMessageTimer: number | null = null;
|
||||
|
||||
state: State = {
|
||||
fieldCount: 0,
|
||||
fieldLabel: null,
|
||||
fieldStats: null,
|
||||
fieldValue: null,
|
||||
parsed: false,
|
||||
parser: undefined,
|
||||
parsedFieldHighlights: [],
|
||||
showFieldStats: false,
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
this.clearMouseMessageTimer();
|
||||
}
|
||||
|
||||
onClickClose = () => {
|
||||
this.setState({ showFieldStats: false });
|
||||
};
|
||||
|
||||
onClickHighlight = (fieldText: string) => {
|
||||
const { getRows } = this.props;
|
||||
const { parser } = this.state;
|
||||
const allRows = getRows();
|
||||
|
||||
// Build value-agnostic row matcher based on the field label
|
||||
const fieldLabel = parser!.getLabelFromField(fieldText);
|
||||
const fieldValue = parser!.getValueFromField(fieldText);
|
||||
const matcher = parser!.buildMatcher(fieldLabel);
|
||||
const fieldStats = calculateFieldStats(allRows, matcher);
|
||||
const fieldCount = fieldStats.reduce((sum, stat) => sum + stat.count, 0);
|
||||
|
||||
this.setState({ fieldCount, fieldLabel, fieldStats, fieldValue, showFieldStats: true });
|
||||
};
|
||||
|
||||
onMouseOverMessage = () => {
|
||||
if (this.props.showContext || this.isTextSelected()) {
|
||||
// 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 = window.setTimeout(this.parseMessage, 500);
|
||||
};
|
||||
|
||||
onMouseOutMessage = () => {
|
||||
if (this.props.showContext) {
|
||||
// See comment in onMouseOverMessage method
|
||||
return;
|
||||
}
|
||||
this.clearMouseMessageTimer();
|
||||
this.setState({ parsed: false });
|
||||
};
|
||||
|
||||
clearMouseMessageTimer = () => {
|
||||
if (this.mouseMessageTimer) {
|
||||
clearTimeout(this.mouseMessageTimer);
|
||||
}
|
||||
};
|
||||
|
||||
parseMessage = () => {
|
||||
if (!this.state.parsed) {
|
||||
const { row } = this.props;
|
||||
const parser = getParser(row.entry);
|
||||
if (parser) {
|
||||
// Use parser to highlight detected fields
|
||||
const parsedFieldHighlights = parser.getFields(this.props.row.entry);
|
||||
this.setState({ parsedFieldHighlights, parsed: true, parser });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
isTextSelected() {
|
||||
if (!window.getSelection) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const selection = window.getSelection();
|
||||
|
||||
if (!selection) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return selection.anchorNode !== null && selection.isCollapsed === false;
|
||||
}
|
||||
|
||||
onContextToggle = (e: React.SyntheticEvent<HTMLElement>) => {
|
||||
e.stopPropagation();
|
||||
this.props.onToggleContext();
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
highlighterExpressions,
|
||||
row,
|
||||
theme,
|
||||
errors,
|
||||
hasMoreContextRows,
|
||||
updateLimit,
|
||||
context,
|
||||
showContext,
|
||||
onToggleContext,
|
||||
} = this.props;
|
||||
const {
|
||||
fieldCount,
|
||||
fieldLabel,
|
||||
fieldStats,
|
||||
fieldValue,
|
||||
parsed,
|
||||
parsedFieldHighlights,
|
||||
showFieldStats,
|
||||
} = this.state;
|
||||
const style = getLogRowStyles(theme, row.logLevel);
|
||||
const { entry, hasAnsi, raw } = row;
|
||||
const previewHighlights = highlighterExpressions && !_.isEqual(highlighterExpressions, row.searchWords);
|
||||
const highlights = previewHighlights ? highlighterExpressions : row.searchWords;
|
||||
const needsHighlighter = highlights && highlights.length > 0 && highlights[0] && highlights[0].length > 0;
|
||||
const highlightClassName = previewHighlights
|
||||
? cx([style.logsRowMatchHighLight, style.logsRowMatchHighLightPreview])
|
||||
: cx([style.logsRowMatchHighLight]);
|
||||
const styles = getStyles(theme);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx([style.logsRowMessage])}
|
||||
onMouseEnter={this.onMouseOverMessage}
|
||||
onMouseLeave={this.onMouseOutMessage}
|
||||
>
|
||||
<div className={styles.positionRelative}>
|
||||
{showContext && context && (
|
||||
<LogRowContext
|
||||
row={row}
|
||||
context={context}
|
||||
errors={errors}
|
||||
hasMoreContextRows={hasMoreContextRows}
|
||||
onOutsideClick={onToggleContext}
|
||||
onLoadMoreContext={() => {
|
||||
if (updateLimit) {
|
||||
updateLimit();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<span className={cx(styles.positionRelative, { [styles.rowWithContext]: showContext })}>
|
||||
{parsed && (
|
||||
<Highlighter
|
||||
style={{ whiteSpace: 'pre-wrap' }}
|
||||
autoEscape
|
||||
highlightTag={FieldHighlight(this.onClickHighlight)}
|
||||
textToHighlight={entry}
|
||||
searchWords={parsedFieldHighlights}
|
||||
highlightClassName={cx([style.logsRowFieldHighLight])}
|
||||
/>
|
||||
)}
|
||||
{!parsed && needsHighlighter && (
|
||||
<Highlighter
|
||||
style={{ whiteSpace: 'pre-wrap' }}
|
||||
textToHighlight={entry}
|
||||
searchWords={highlights}
|
||||
findChunks={findHighlightChunksInText}
|
||||
highlightClassName={highlightClassName}
|
||||
/>
|
||||
)}
|
||||
{hasAnsi && !parsed && !needsHighlighter && <LogMessageAnsi value={raw} />}
|
||||
{!hasAnsi && !parsed && !needsHighlighter && entry}
|
||||
{showFieldStats && (
|
||||
<div className={cx([style.logsRowStats])}>
|
||||
<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;
|
||||
.${style.logsRow}:hover & {
|
||||
visibility: visible;
|
||||
margin-left: 10px;
|
||||
text-decoration: underline;
|
||||
}
|
||||
`}
|
||||
>
|
||||
{showContext ? 'Hide' : 'Show'} context
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const LogRowMessage = withTheme(UnThemedLogRowMessage);
|
||||
LogRowMessage.displayName = 'LogRowMessage';
|
||||
@@ -6,6 +6,7 @@ import { LogRow } from './LogRow';
|
||||
import { Themeable } from '../../types/theme';
|
||||
import { withTheme } from '../../themes/index';
|
||||
import { getLogRowStyles } from './getLogRowStyles';
|
||||
import memoizeOne from 'memoize-one';
|
||||
|
||||
const PREVIEW_LIMIT = 100;
|
||||
const RENDER_LIMIT = 500;
|
||||
@@ -65,6 +66,10 @@ class UnThemedLogRows extends PureComponent<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
makeGetRows = memoizeOne((processedRows: LogRowModel[]) => {
|
||||
return () => processedRows;
|
||||
});
|
||||
|
||||
render() {
|
||||
const {
|
||||
dedupStrategy,
|
||||
@@ -95,7 +100,7 @@ class UnThemedLogRows extends PureComponent<Props, State> {
|
||||
const lastRows = processedRows.slice(PREVIEW_LIMIT, rowCount);
|
||||
|
||||
// React profiler becomes unusable if we pass all rows to all rows and their labels, using getter instead
|
||||
const getRows = () => processedRows;
|
||||
const getRows = this.makeGetRows(processedRows);
|
||||
const getRowContext = this.props.getRowContext ? this.props.getRowContext : () => Promise.resolve([]);
|
||||
const { logsRows } = getLogRowStyles(theme);
|
||||
|
||||
|
||||
@@ -3,8 +3,9 @@ import { LogLevel } from '@grafana/data';
|
||||
|
||||
import { GrafanaTheme } from '../../types/theme';
|
||||
import { selectThemeVariant } from '../../themes/selectThemeVariant';
|
||||
import { stylesFactory } from '../../themes';
|
||||
|
||||
export const getLogRowStyles = (theme: GrafanaTheme, logLevel?: LogLevel) => {
|
||||
export const getLogRowStyles = stylesFactory((theme: GrafanaTheme, logLevel?: LogLevel) => {
|
||||
let logColor = selectThemeVariant({ light: theme.colors.gray5, dark: theme.colors.gray2 }, theme.type);
|
||||
switch (logLevel) {
|
||||
case LogLevel.crit:
|
||||
@@ -130,4 +131,4 @@ export const getLogRowStyles = (theme: GrafanaTheme, logLevel?: LogLevel) => {
|
||||
margin: 5px 0;
|
||||
`,
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user