From 23202ab130d804b8e2ec89754d723059f6dd043f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Tue, 22 Jan 2019 08:59:22 +0100 Subject: [PATCH 1/3] Refactored out LogRow to a separate file --- public/app/core/logs_model.ts | 20 +- public/app/features/explore/LogLabels.tsx | 6 +- public/app/features/explore/LogRow.tsx | 193 +++++++++++++++++ public/app/features/explore/Logs.tsx | 204 +----------------- .../datasource/loki/result_transformer.ts | 8 +- 5 files changed, 216 insertions(+), 215 deletions(-) create mode 100644 public/app/features/explore/LogRow.tsx diff --git a/public/app/core/logs_model.ts b/public/app/core/logs_model.ts index 4cf9a029a2a..b8af798489d 100644 --- a/public/app/core/logs_model.ts +++ b/public/app/core/logs_model.ts @@ -42,7 +42,7 @@ export interface LogSearchMatch { text: string; } -export interface LogRow { +export interface LogRowModel { duplicates?: number; entry: string; key: string; // timestamp + labels @@ -78,7 +78,7 @@ export interface LogsMetaItem { export interface LogsModel { id: string; // Identify one logs result from another meta?: LogsMetaItem[]; - rows: LogRow[]; + rows: LogRowModel[]; series?: TimeSeries[]; } @@ -188,13 +188,13 @@ export const LogsParsers: { [name: string]: LogsParser } = { }, }; -export function calculateFieldStats(rows: LogRow[], extractor: RegExp): LogsLabelStat[] { +export function calculateFieldStats(rows: LogRowModel[], extractor: RegExp): LogsLabelStat[] { // Consider only rows that satisfy the matcher const rowsWithField = rows.filter(row => extractor.test(row.entry)); const rowCount = rowsWithField.length; // Get field value counts for eligible rows - const countsByValue = _.countBy(rowsWithField, row => (row as LogRow).entry.match(extractor)[1]); + const countsByValue = _.countBy(rowsWithField, row => (row as LogRowModel).entry.match(extractor)[1]); const sortedCounts = _.chain(countsByValue) .map((count, value) => ({ count, value, proportion: count / rowCount })) .sortBy('count') @@ -204,13 +204,13 @@ export function calculateFieldStats(rows: LogRow[], extractor: RegExp): LogsLabe return sortedCounts; } -export function calculateLogsLabelStats(rows: LogRow[], label: string): LogsLabelStat[] { +export function calculateLogsLabelStats(rows: LogRowModel[], label: string): LogsLabelStat[] { // Consider only rows that have the given label const rowsWithLabel = rows.filter(row => row.labels[label] !== undefined); const rowCount = rowsWithLabel.length; // Get label value counts for eligible rows - const countsByValue = _.countBy(rowsWithLabel, row => (row as LogRow).labels[label]); + const countsByValue = _.countBy(rowsWithLabel, row => (row as LogRowModel).labels[label]); const sortedCounts = _.chain(countsByValue) .map((count, value) => ({ count, value, proportion: count / rowCount })) .sortBy('count') @@ -221,7 +221,7 @@ export function calculateLogsLabelStats(rows: LogRow[], label: string): LogsLabe } const isoDateRegexp = /\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-6]\d[,\.]\d+([+-][0-2]\d:[0-5]\d|Z)/g; -function isDuplicateRow(row: LogRow, other: LogRow, strategy: LogsDedupStrategy): boolean { +function isDuplicateRow(row: LogRowModel, other: LogRowModel, strategy: LogsDedupStrategy): boolean { switch (strategy) { case LogsDedupStrategy.exact: // Exact still strips dates @@ -243,7 +243,7 @@ export function dedupLogRows(logs: LogsModel, strategy: LogsDedupStrategy): Logs return logs; } - const dedupedRows = logs.rows.reduce((result: LogRow[], row: LogRow, index, list) => { + const dedupedRows = logs.rows.reduce((result: LogRowModel[], row: LogRowModel, index, list) => { const previous = result[result.length - 1]; if (index > 0 && isDuplicateRow(row, previous, strategy)) { previous.duplicates++; @@ -278,7 +278,7 @@ export function filterLogLevels(logs: LogsModel, hiddenLogLevels: Set) return logs; } - const filteredRows = logs.rows.reduce((result: LogRow[], row: LogRow, index, list) => { + const filteredRows = logs.rows.reduce((result: LogRowModel[], row: LogRowModel, index, list) => { if (!hiddenLogLevels.has(row.logLevel)) { result.push(row); } @@ -291,7 +291,7 @@ export function filterLogLevels(logs: LogsModel, hiddenLogLevels: Set) }; } -export function makeSeriesForLogs(rows: LogRow[], intervalMs: number): TimeSeries[] { +export function makeSeriesForLogs(rows: LogRowModel[], intervalMs: number): TimeSeries[] { // currently interval is rangeMs / resolution, which is too low for showing series as bars. // need at least 10px per bucket, so we multiply interval by 10. Should be solved higher up the chain // when executing queries & interval calculated and not here but this is a temporary fix. diff --git a/public/app/features/explore/LogLabels.tsx b/public/app/features/explore/LogLabels.tsx index 7675fb13152..1dc82637f58 100644 --- a/public/app/features/explore/LogLabels.tsx +++ b/public/app/features/explore/LogLabels.tsx @@ -1,7 +1,7 @@ import React, { PureComponent } from 'react'; import classnames from 'classnames'; -import { calculateLogsLabelStats, LogsLabelStat, LogsStreamLabels, LogRow } from 'app/core/logs_model'; +import { calculateLogsLabelStats, LogsLabelStat, LogsStreamLabels, LogRowModel } from 'app/core/logs_model'; function StatsRow({ active, count, proportion, value }: LogsLabelStat) { const percent = `${Math.round(proportion * 100)}%`; @@ -68,7 +68,7 @@ export class Stats extends PureComponent<{ class Label extends PureComponent< { - getRows?: () => LogRow[]; + getRows?: () => LogRowModel[]; label: string; plain?: boolean; value: string; @@ -133,7 +133,7 @@ class Label extends PureComponent< } export default class LogLabels extends PureComponent<{ - getRows?: () => LogRow[]; + getRows?: () => LogRowModel[]; labels: LogsStreamLabels; plain?: boolean; onClickLabel?: (label: string, value: string) => void; diff --git a/public/app/features/explore/LogRow.tsx b/public/app/features/explore/LogRow.tsx new file mode 100644 index 00000000000..7b620da0ef6 --- /dev/null +++ b/public/app/features/explore/LogRow.tsx @@ -0,0 +1,193 @@ +import React, { PureComponent } from 'react'; +import _ from 'lodash'; +import Highlighter from 'react-highlight-words'; +import classnames from 'classnames'; + +import { LogRowModel, LogsLabelStat, LogsParser, calculateFieldStats, getParser } from 'app/core/logs_model'; +import LogLabels, { Stats } from './LogLabels'; +import { findHighlightChunksInText } from 'app/core/utils/text'; + +interface RowProps { + highlighterExpressions?: string[]; + row: LogRowModel; + showDuplicates: boolean; + showLabels: boolean | null; // Tristate: null means auto + showLocalTime: boolean; + showUtc: boolean; + getRows: () => LogRowModel[]; + onClickLabel?: (label: string, value: string) => void; +} + +interface RowState { + fieldCount: number; + fieldLabel: string; + fieldStats: LogsLabelStat[]; + fieldValue: string; + parsed: boolean; + parser?: LogsParser; + parsedFieldHighlights: string[]; + showFieldStats: boolean; +} + +/** + * Renders a highlighted field. + * When hovering, a stats icon is shown. + */ +const FieldHighlight = onClick => props => { + return ( + + {props.children} + onClick(props.children)} /> + + ); +}; + +/** + * Renders a log line. + * + * When user hovers over it for a certain time, it lazily parses the log line. + * Once a parser is found, it will determine fields, that will be highlighted. + * When the user requests stats for a field, they will be calculated and rendered below the row. + */ +export class LogRow extends PureComponent { + mouseMessageTimer: NodeJS.Timer; + + state = { + fieldCount: 0, + fieldLabel: null, + fieldStats: null, + fieldValue: null, + parsed: false, + parser: undefined, + parsedFieldHighlights: [], + showFieldStats: false, + }; + + componentWillUnmount() { + clearTimeout(this.mouseMessageTimer); + } + + 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 = () => { + // Don't parse right away, user might move along + this.mouseMessageTimer = setTimeout(this.parseMessage, 500); + }; + + onMouseOutMessage = () => { + clearTimeout(this.mouseMessageTimer); + this.setState({ parsed: false }); + }; + + 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 }); + } + } + }; + + render() { + const { + getRows, + highlighterExpressions, + onClickLabel, + row, + showDuplicates, + showLabels, + showLocalTime, + showUtc, + } = this.props; + const { + fieldCount, + fieldLabel, + fieldStats, + fieldValue, + parsed, + parsedFieldHighlights, + showFieldStats, + } = this.state; + const previewHighlights = highlighterExpressions && !_.isEqual(highlighterExpressions, row.searchWords); + const highlights = previewHighlights ? highlighterExpressions : row.searchWords; + const needsHighlighter = highlights && highlights.length > 0; + const highlightClassName = classnames('logs-row__match-highlight', { + 'logs-row__match-highlight--preview': previewHighlights, + }); + return ( +
+ {showDuplicates && ( +
{row.duplicates > 0 ? `${row.duplicates + 1}x` : null}
+ )} +
+ {showUtc && ( +
+ {row.timestamp} +
+ )} + {showLocalTime && ( +
+ {row.timeLocal} +
+ )} + {showLabels && ( +
+ +
+ )} +
+ {parsed && ( + + )} + {!parsed && + needsHighlighter && ( + + )} + {!parsed && !needsHighlighter && row.entry} + {showFieldStats && ( +
+ +
+ )} +
+
+ ); + } +} diff --git a/public/app/features/explore/Logs.tsx b/public/app/features/explore/Logs.tsx index d07b31e2ff1..7c821179587 100644 --- a/public/app/features/explore/Logs.tsx +++ b/public/app/features/explore/Logs.tsx @@ -1,7 +1,5 @@ import _ from 'lodash'; import React, { PureComponent } from 'react'; -import Highlighter from 'react-highlight-words'; -import classnames from 'classnames'; import * as rangeUtil from 'app/core/utils/rangeutil'; import { RawTimeRange } from '@grafana/ui'; @@ -11,20 +9,16 @@ import { LogsModel, dedupLogRows, filterLogLevels, - getParser, LogLevel, LogsMetaKind, - LogsLabelStat, - LogsParser, - LogRow, - calculateFieldStats, } from 'app/core/logs_model'; -import { findHighlightChunksInText } from 'app/core/utils/text'; + import { Switch } from 'app/core/components/Switch/Switch'; import ToggleButtonGroup, { ToggleButton } from 'app/core/components/ToggleButtonGroup/ToggleButtonGroup'; import Graph from './Graph'; -import LogLabels, { Stats } from './LogLabels'; +import LogLabels from './LogLabels'; +import { LogRow } from './LogRow'; const PREVIEW_LIMIT = 100; @@ -43,191 +37,6 @@ const graphOptions = { }, }; -/** - * Renders a highlighted field. - * When hovering, a stats icon is shown. - */ -const FieldHighlight = onClick => props => { - return ( - - {props.children} - onClick(props.children)} /> - - ); -}; - -interface RowProps { - highlighterExpressions?: string[]; - row: LogRow; - showDuplicates: boolean; - showLabels: boolean | null; // Tristate: null means auto - showLocalTime: boolean; - showUtc: boolean; - getRows: () => LogRow[]; - onClickLabel?: (label: string, value: string) => void; -} - -interface RowState { - fieldCount: number; - fieldLabel: string; - fieldStats: LogsLabelStat[]; - fieldValue: string; - parsed: boolean; - parser?: LogsParser; - parsedFieldHighlights: string[]; - showFieldStats: boolean; -} - -/** - * Renders a log line. - * - * When user hovers over it for a certain time, it lazily parses the log line. - * Once a parser is found, it will determine fields, that will be highlighted. - * When the user requests stats for a field, they will be calculated and rendered below the row. - */ -class Row extends PureComponent { - mouseMessageTimer: NodeJS.Timer; - - state = { - fieldCount: 0, - fieldLabel: null, - fieldStats: null, - fieldValue: null, - parsed: false, - parser: undefined, - parsedFieldHighlights: [], - showFieldStats: false, - }; - - componentWillUnmount() { - clearTimeout(this.mouseMessageTimer); - } - - 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 = () => { - // Don't parse right away, user might move along - this.mouseMessageTimer = setTimeout(this.parseMessage, 500); - }; - - onMouseOutMessage = () => { - clearTimeout(this.mouseMessageTimer); - this.setState({ parsed: false }); - }; - - 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 }); - } - } - }; - - render() { - const { - getRows, - highlighterExpressions, - onClickLabel, - row, - showDuplicates, - showLabels, - showLocalTime, - showUtc, - } = this.props; - const { - fieldCount, - fieldLabel, - fieldStats, - fieldValue, - parsed, - parsedFieldHighlights, - showFieldStats, - } = this.state; - const previewHighlights = highlighterExpressions && !_.isEqual(highlighterExpressions, row.searchWords); - const highlights = previewHighlights ? highlighterExpressions : row.searchWords; - const needsHighlighter = highlights && highlights.length > 0; - const highlightClassName = classnames('logs-row__match-highlight', { - 'logs-row__match-highlight--preview': previewHighlights, - }); - return ( -
- {showDuplicates && ( -
{row.duplicates > 0 ? `${row.duplicates + 1}x` : null}
- )} -
- {showUtc && ( -
- {row.timestamp} -
- )} - {showLocalTime && ( -
- {row.timeLocal} -
- )} - {showLabels && ( -
- -
- )} -
- {parsed && ( - - )} - {!parsed && - needsHighlighter && ( - - )} - {!parsed && !needsHighlighter && row.entry} - {showFieldStats && ( -
- -
- )} -
-
- ); - } -} - function renderMetaItem(value: any, kind: LogsMetaKind) { if (kind === LogsMetaKind.LabelsMap) { return ( @@ -441,10 +250,9 @@ export default class Logs extends PureComponent {
{hasData && - !deferLogs && - // Only inject highlighterExpression in the first set for performance reasons + !deferLogs && // Only inject highlighterExpression in the first set for performance reasons firstRows.map(row => ( - { !deferLogs && renderAll && lastRows.map(row => ( - [ + (acc: LogRowModel[], stream: LogsStream) => [ ...acc, ...stream.entries.map(entry => processEntry(entry, stream.labels, stream.parsedLabels, stream.uniqueLabels, stream.search) From c369279401b6e94d84c035e828702385cfa304d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Tue, 22 Jan 2019 09:22:38 +0100 Subject: [PATCH 2/3] Splitted up LogLabels into LogLabelStats and LogLabel --- public/app/core/logs_model.ts | 6 +- public/app/features/explore/LogLabel.tsx | 74 +++++++++ public/app/features/explore/LogLabelStats.tsx | 72 +++++++++ public/app/features/explore/LogLabels.tsx | 141 +----------------- public/app/features/explore/LogRow.tsx | 15 +- public/app/features/explore/Logs.tsx | 2 +- 6 files changed, 165 insertions(+), 145 deletions(-) create mode 100644 public/app/features/explore/LogLabel.tsx create mode 100644 public/app/features/explore/LogLabelStats.tsx diff --git a/public/app/core/logs_model.ts b/public/app/core/logs_model.ts index b8af798489d..a3f78e7152a 100644 --- a/public/app/core/logs_model.ts +++ b/public/app/core/logs_model.ts @@ -56,7 +56,7 @@ export interface LogRowModel { uniqueLabels?: LogsStreamLabels; } -export interface LogsLabelStat { +export interface LogLabelStatsModel { active?: boolean; count: number; proportion: number; @@ -188,7 +188,7 @@ export const LogsParsers: { [name: string]: LogsParser } = { }, }; -export function calculateFieldStats(rows: LogRowModel[], extractor: RegExp): LogsLabelStat[] { +export function calculateFieldStats(rows: LogRowModel[], extractor: RegExp): LogLabelStatsModel[] { // Consider only rows that satisfy the matcher const rowsWithField = rows.filter(row => extractor.test(row.entry)); const rowCount = rowsWithField.length; @@ -204,7 +204,7 @@ export function calculateFieldStats(rows: LogRowModel[], extractor: RegExp): Log return sortedCounts; } -export function calculateLogsLabelStats(rows: LogRowModel[], label: string): LogsLabelStat[] { +export function calculateLogsLabelStats(rows: LogRowModel[], label: string): LogLabelStatsModel[] { // Consider only rows that have the given label const rowsWithLabel = rows.filter(row => row.labels[label] !== undefined); const rowCount = rowsWithLabel.length; diff --git a/public/app/features/explore/LogLabel.tsx b/public/app/features/explore/LogLabel.tsx new file mode 100644 index 00000000000..b4570f10c82 --- /dev/null +++ b/public/app/features/explore/LogLabel.tsx @@ -0,0 +1,74 @@ +import React, { PureComponent } from 'react'; + +import { calculateLogsLabelStats, LogLabelStatsModel, LogRowModel } from 'app/core/logs_model'; +import { LogLabelStats } from './LogLabelStats'; + +interface Props { + getRows?: () => LogRowModel[]; + label: string; + plain?: boolean; + value: string; + onClickLabel?: (label: string, value: string) => void; +} + +interface State { + showStats: boolean; + stats: LogLabelStatsModel[]; +} + +export class LogLabel extends PureComponent { + state = { + stats: null, + showStats: false, + }; + + onClickClose = () => { + this.setState({ showStats: false }); + }; + + onClickLabel = () => { + const { onClickLabel, label, value } = this.props; + if (onClickLabel) { + onClickLabel(label, value); + } + }; + + onClickStats = () => { + this.setState(state => { + if (state.showStats) { + return { showStats: false, stats: null }; + } + const allRows = this.props.getRows(); + const stats = calculateLogsLabelStats(allRows, this.props.label); + return { showStats: true, stats }; + }); + }; + + render() { + const { getRows, label, plain, value } = this.props; + const { showStats, stats } = this.state; + const tooltip = `${label}: ${value}`; + return ( + + + {value} + + {!plain && ( + + )} + {!plain && getRows && } + {showStats && ( + + + + )} + + ); + } +} diff --git a/public/app/features/explore/LogLabelStats.tsx b/public/app/features/explore/LogLabelStats.tsx new file mode 100644 index 00000000000..b0bd69170c5 --- /dev/null +++ b/public/app/features/explore/LogLabelStats.tsx @@ -0,0 +1,72 @@ +import React, { PureComponent } from 'react'; +import classnames from 'classnames'; +import { LogLabelStatsModel } from 'app/core/logs_model'; + +function LogLabelStatsRow(logLabelStatsModel: LogLabelStatsModel) { + const { active, count, proportion, value } = logLabelStatsModel; + const percent = `${Math.round(proportion * 100)}%`; + const barStyle = { width: percent }; + const className = classnames('logs-stats-row', { 'logs-stats-row--active': active }); + + return ( +
+
+
{value}
+
{count}
+
{percent}
+
+
+
+
+
+ ); +} + +const STATS_ROW_LIMIT = 5; + +interface Props { + stats: LogLabelStatsModel[]; + label: string; + value: string; + rowCount: number; + onClickClose: () => void; +} + +export class LogLabelStats extends PureComponent { + render() { + const { label, rowCount, stats, value, onClickClose } = this.props; + const topRows = stats.slice(0, STATS_ROW_LIMIT); + let activeRow = topRows.find(row => row.value === value); + let otherRows = stats.slice(STATS_ROW_LIMIT); + const insertActiveRow = !activeRow; + + // Remove active row from other to show extra + if (insertActiveRow) { + activeRow = otherRows.find(row => row.value === value); + otherRows = otherRows.filter(row => row.value !== value); + } + + const otherCount = otherRows.reduce((sum, row) => sum + row.count, 0); + const topCount = topRows.reduce((sum, row) => sum + row.count, 0); + const total = topCount + otherCount; + const otherProportion = otherCount / total; + + return ( +
+
+ + {label}: {total} of {rowCount} rows have that label + + +
+
+ {topRows.map(stat => )} + {insertActiveRow && activeRow && } + {otherCount > 0 && ( + + )} +
+
+ ); + } +} diff --git a/public/app/features/explore/LogLabels.tsx b/public/app/features/explore/LogLabels.tsx index 1dc82637f58..7105a2a5370 100644 --- a/public/app/features/explore/LogLabels.tsx +++ b/public/app/features/explore/LogLabels.tsx @@ -1,147 +1,20 @@ import React, { PureComponent } from 'react'; -import classnames from 'classnames'; -import { calculateLogsLabelStats, LogsLabelStat, LogsStreamLabels, LogRowModel } from 'app/core/logs_model'; +import { LogsStreamLabels, LogRowModel } from 'app/core/logs_model'; +import { LogLabel } from './LogLabel'; -function StatsRow({ active, count, proportion, value }: LogsLabelStat) { - const percent = `${Math.round(proportion * 100)}%`; - const barStyle = { width: percent }; - const className = classnames('logs-stats-row', { 'logs-stats-row--active': active }); - - return ( -
-
-
{value}
-
{count}
-
{percent}
-
-
-
-
-
- ); -} - -const STATS_ROW_LIMIT = 5; -export class Stats extends PureComponent<{ - stats: LogsLabelStat[]; - label: string; - value: string; - rowCount: number; - onClickClose: () => void; -}> { - render() { - const { label, rowCount, stats, value, onClickClose } = this.props; - const topRows = stats.slice(0, STATS_ROW_LIMIT); - let activeRow = topRows.find(row => row.value === value); - let otherRows = stats.slice(STATS_ROW_LIMIT); - const insertActiveRow = !activeRow; - // Remove active row from other to show extra - if (insertActiveRow) { - activeRow = otherRows.find(row => row.value === value); - otherRows = otherRows.filter(row => row.value !== value); - } - const otherCount = otherRows.reduce((sum, row) => sum + row.count, 0); - const topCount = topRows.reduce((sum, row) => sum + row.count, 0); - const total = topCount + otherCount; - const otherProportion = otherCount / total; - - return ( -
-
- - {label}: {total} of {rowCount} rows have that label - - -
-
- {topRows.map(stat => )} - {insertActiveRow && activeRow && } - {otherCount > 0 && ( - - )} -
-
- ); - } -} - -class Label extends PureComponent< - { - getRows?: () => LogRowModel[]; - label: string; - plain?: boolean; - value: string; - onClickLabel?: (label: string, value: string) => void; - }, - { showStats: boolean; stats: LogsLabelStat[] } -> { - state = { - stats: null, - showStats: false, - }; - - onClickClose = () => { - this.setState({ showStats: false }); - }; - - onClickLabel = () => { - const { onClickLabel, label, value } = this.props; - if (onClickLabel) { - onClickLabel(label, value); - } - }; - - onClickStats = () => { - this.setState(state => { - if (state.showStats) { - return { showStats: false, stats: null }; - } - const allRows = this.props.getRows(); - const stats = calculateLogsLabelStats(allRows, this.props.label); - return { showStats: true, stats }; - }); - }; - - render() { - const { getRows, label, plain, value } = this.props; - const { showStats, stats } = this.state; - const tooltip = `${label}: ${value}`; - return ( - - - {value} - - {!plain && ( - - )} - {!plain && getRows && } - {showStats && ( - - - - )} - - ); - } -} - -export default class LogLabels extends PureComponent<{ +interface Props { getRows?: () => LogRowModel[]; labels: LogsStreamLabels; plain?: boolean; onClickLabel?: (label: string, value: string) => void; -}> { +} + +export class LogLabels extends PureComponent { render() { const { getRows, labels, onClickLabel, plain } = this.props; return Object.keys(labels).map(key => ( -