diff --git a/packages/grafana-ui/src/components/Logs/LogLabel.tsx b/packages/grafana-ui/src/components/Logs/LogLabel.tsx index c97e76e76f1..4b9ff1652fa 100644 --- a/packages/grafana-ui/src/components/Logs/LogLabel.tsx +++ b/packages/grafana-ui/src/components/Logs/LogLabel.tsx @@ -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; diff --git a/packages/grafana-ui/src/components/Logs/LogLabels.tsx b/packages/grafana-ui/src/components/Logs/LogLabels.tsx index 22d8aa60027..25a455e5148 100644 --- a/packages/grafana-ui/src/components/Logs/LogLabels.tsx +++ b/packages/grafana-ui/src/components/Logs/LogLabels.tsx @@ -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 = ({ getRows, labels, onClickLabel, plain }) => { - const theme = useContext(ThemeContext); - const styles = getStyles(theme); + const styles = getStyles(); return ( diff --git a/packages/grafana-ui/src/components/Logs/LogRow.tsx b/packages/grafana-ui/src/components/Logs/LogRow.tsx index 07981c57d7c..9b8480d1308 100644 --- a/packages/grafana-ui/src/components/Logs/LogRow.tsx +++ b/packages/grafana-ui/src/components/Logs/LogRow.tsx @@ -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 => (props: any) => { - const theme = useContext(ThemeContext); - const style = getLogRowStyles(theme); - return ( - - {props.children} - onClick(props.children)} - /> - - ); -}; - -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 { - 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 { }); }; - onContextToggle = (e: React.SyntheticEvent) => { - e.stopPropagation(); - this.toggleContext(); - }; - renderLogRow( context?: LogRowContextRows, errors?: LogRowContextQueryErrors, @@ -224,143 +68,49 @@ class UnThemedLogRow extends PureComponent { 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 ( - - {theme => { - const styles = this.state.showContext - ? cx(logRowStyles, getLogRowWithContextStyles(theme, this.state).row) - : logRowStyles; - return ( -
- {showDuplicates && ( -
- {row.duplicates && row.duplicates > 0 ? `${row.duplicates + 1}x` : null} -
- )} -
- {showTime && showUtc && ( -
- {row.timeUtc} -
- )} - {showTime && !showUtc && ( -
- {row.timeLocal} -
- )} - {showLabels && ( -
- -
- )} -
-
- {showContext && context && ( - { - if (updateLimit) { - updateLimit(); - } - }} - /> - )} - - {parsed && ( - - )} - {!parsed && needsHighlighter && ( - - )} - {hasAnsi && !parsed && !needsHighlighter && } - {!hasAnsi && !parsed && !needsHighlighter && entry} - {showFieldStats && ( -
- -
- )} -
- {row.searchWords && row.searchWords.length > 0 && ( - - {showContext ? 'Hide' : 'Show'} context - - )} -
-
-
- ); - }} - +
+ {showDuplicates && ( +
+ {row.duplicates && row.duplicates > 0 ? `${row.duplicates + 1}x` : null} +
+ )} +
+ {showTime && showUtc && ( +
+ {row.timeUtc} +
+ )} + {showTime && !showUtc && ( +
+ {row.timeLocal} +
+ )} + {showLabels && ( +
+ +
+ )} + +
); } diff --git a/packages/grafana-ui/src/components/Logs/LogRowMessage.tsx b/packages/grafana-ui/src/components/Logs/LogRowMessage.tsx new file mode 100644 index 00000000000..c09394a9269 --- /dev/null +++ b/packages/grafana-ui/src/components/Logs/LogRowMessage.tsx @@ -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 => (props: any) => { + const theme = useContext(ThemeContext); + const style = getLogRowStyles(theme); + return ( + + {props.children} + onClick(props.children)} + /> + + ); +}; + +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 { + 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) => { + 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 ( +
+
+ {showContext && context && ( + { + if (updateLimit) { + updateLimit(); + } + }} + /> + )} + + {parsed && ( + + )} + {!parsed && needsHighlighter && ( + + )} + {hasAnsi && !parsed && !needsHighlighter && } + {!hasAnsi && !parsed && !needsHighlighter && entry} + {showFieldStats && ( +
+ +
+ )} +
+ {row.searchWords && row.searchWords.length > 0 && ( + + {showContext ? 'Hide' : 'Show'} context + + )} +
+
+ ); + } +} + +export const LogRowMessage = withTheme(UnThemedLogRowMessage); +LogRowMessage.displayName = 'LogRowMessage'; diff --git a/packages/grafana-ui/src/components/Logs/LogRows.tsx b/packages/grafana-ui/src/components/Logs/LogRows.tsx index b2f60ca0cec..0dc13e70c4a 100644 --- a/packages/grafana-ui/src/components/Logs/LogRows.tsx +++ b/packages/grafana-ui/src/components/Logs/LogRows.tsx @@ -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 { } } + makeGetRows = memoizeOne((processedRows: LogRowModel[]) => { + return () => processedRows; + }); + render() { const { dedupStrategy, @@ -95,7 +100,7 @@ class UnThemedLogRows extends PureComponent { 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); diff --git a/packages/grafana-ui/src/components/Logs/getLogRowStyles.ts b/packages/grafana-ui/src/components/Logs/getLogRowStyles.ts index ff16bbd4a60..a510ab69f39 100644 --- a/packages/grafana-ui/src/components/Logs/getLogRowStyles.ts +++ b/packages/grafana-ui/src/components/Logs/getLogRowStyles.ts @@ -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; `, }; -}; +});