diff --git a/public/app/core/logs_model.ts b/public/app/core/logs_model.ts index 4cf9a029a2a..a3f78e7152a 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 @@ -56,7 +56,7 @@ export interface LogRow { uniqueLabels?: LogsStreamLabels; } -export interface LogsLabelStat { +export interface LogLabelStatsModel { active?: boolean; count: number; proportion: number; @@ -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): LogLabelStatsModel[] { // 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): LogLabelStatsModel[] { // 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/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 7675fb13152..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, LogRow } 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?: () => LogRow[]; - 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<{ - getRows?: () => LogRow[]; +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 => ( -