From 5916cb3e7c0ce5563f566d07682d8bd8888390bf Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Mon, 3 Dec 2018 11:58:25 +0100 Subject: [PATCH] Explore: Logging label stats - added filter and stats icons to log stream labels - removed click handler from label itself - click on stats icon calculates label value distribution across loaded logs lines - show stats in hover - stats have indicator which value is the current one - showing top 5 values for the given label - if selected value is not among top 5, it is added - summing up remaining label value distribution as Other --- public/app/features/explore/LogLabels.tsx | 164 ++++++++++++++++++++++ public/app/features/explore/Logs.tsx | 70 +++------ public/sass/pages/_explore.scss | 78 +++++++++- 3 files changed, 258 insertions(+), 54 deletions(-) create mode 100644 public/app/features/explore/LogLabels.tsx diff --git a/public/app/features/explore/LogLabels.tsx b/public/app/features/explore/LogLabels.tsx new file mode 100644 index 00000000000..3f8393e4059 --- /dev/null +++ b/public/app/features/explore/LogLabels.tsx @@ -0,0 +1,164 @@ +import _ from 'lodash'; +import React, { PureComponent } from 'react'; +import classnames from 'classnames'; + +import { LogsStreamLabels, LogRow } from 'app/core/logs_model'; + +interface FieldStat { + active?: boolean; + value: string; + count: number; + proportion: number; +} + +function calculateStats(rows: LogRow[], label: string): FieldStat[] { + // 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 sortedCounts = _.chain(countsByValue) + .map((count, value) => ({ count, value, proportion: count / rowCount })) + .sortBy('count') + .reverse() + .value(); + + return sortedCounts; +} + +function StatsRow({ active, count, proportion, value }: FieldStat) { + 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; +class Stats extends PureComponent<{ + stats: FieldStat[]; + 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 && } + {otherCount > 0 && } + + ); + } +} + +class Label extends PureComponent< + { + allRows?: LogRow[]; + label: string; + plain?: boolean; + value: string; + onClickLabel?: (label: string, value: string) => void; + }, + { showStats: boolean; stats: FieldStat[] } +> { + 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 stats = calculateStats(this.props.allRows, this.props.label); + return { showStats: true, stats }; + }); + }; + + render() { + const { allRows, label, plain, value } = this.props; + const { showStats, stats } = this.state; + const tooltip = `${label}: ${value}`; + return ( + + + {value} + + {!plain && ( + + )} + {!plain && allRows && } + {showStats && ( + + + + )} + + ); + } +} + +export default class LogLabels extends PureComponent<{ + allRows?: LogRow[]; + labels: LogsStreamLabels; + plain?: boolean; + onClickLabel?: (label: string, value: string) => void; +}> { + render() { + const { allRows, labels, onClickLabel, plain } = this.props; + return Object.keys(labels).map(key => ( +