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 => (
+
+ ));
+ }
+}
diff --git a/public/app/features/explore/Logs.tsx b/public/app/features/explore/Logs.tsx
index 79207f28074..6b4bebfd0bc 100644
--- a/public/app/features/explore/Logs.tsx
+++ b/public/app/features/explore/Logs.tsx
@@ -10,7 +10,6 @@ import {
dedupLogRows,
filterLogLevels,
LogLevel,
- LogsStreamLabels,
LogsMetaKind,
LogRow,
} from 'app/core/logs_model';
@@ -18,6 +17,7 @@ import { findHighlightChunksInText } from 'app/core/utils/text';
import { Switch } from 'app/core/components/Switch/Switch';
import Graph from './Graph';
+import LogLabels from './LogLabels';
const PREVIEW_LIMIT = 100;
@@ -35,52 +35,8 @@ const graphOptions = {
},
};
-function renderMetaItem(value: any, kind: LogsMetaKind) {
- if (kind === LogsMetaKind.LabelsMap) {
- return (
-
-
-
- );
- }
- return value;
-}
-
-class Label extends PureComponent<{
- label: string;
- value: string;
- onClickLabel?: (label: string, value: string) => void;
-}> {
- onClickLabel = () => {
- const { onClickLabel, label, value } = this.props;
- if (onClickLabel) {
- onClickLabel(label, value);
- }
- };
-
- render() {
- const { label, value } = this.props;
- const tooltip = `${label}: ${value}`;
- return (
-
- {value}
-
- );
- }
-}
-class Labels extends PureComponent<{
- labels: LogsStreamLabels;
- onClickLabel?: (label: string, value: string) => void;
-}> {
- render() {
- const { labels, onClickLabel } = this.props;
- return Object.keys(labels).map(key => (
-
- ));
- }
-}
-
interface RowProps {
+ allRows: LogRow[];
row: LogRow;
showLabels: boolean | null; // Tristate: null means auto
showLocalTime: boolean;
@@ -88,7 +44,7 @@ interface RowProps {
onClickLabel?: (label: string, value: string) => void;
}
-function Row({ onClickLabel, row, showLabels, showLocalTime, showUtc }: RowProps) {
+function Row({ allRows, onClickLabel, row, showLabels, showLocalTime, showUtc }: RowProps) {
const needsHighlighter = row.searchWords && row.searchWords.length > 0;
return (
<>
@@ -113,7 +69,7 @@ function Row({ onClickLabel, row, showLabels, showLocalTime, showUtc }: RowProps
)}
{showLabels && (
-
+
)}
@@ -132,6 +88,17 @@ function Row({ onClickLabel, row, showLabels, showLocalTime, showUtc }: RowProps
);
}
+function renderMetaItem(value: any, kind: LogsMetaKind) {
+ if (kind === LogsMetaKind.LabelsMap) {
+ return (
+
+
+
+ );
+ }
+ return value;
+}
+
interface LogsProps {
className?: string;
data: LogsModel;
@@ -258,8 +225,9 @@ export default class Logs extends PureComponent {
}
// Staged rendering
- const firstRows = dedupedData.rows.slice(0, PREVIEW_LIMIT);
- const lastRows = dedupedData.rows.slice(PREVIEW_LIMIT);
+ const processedRows = dedupedData.rows;
+ const firstRows = processedRows.slice(0, PREVIEW_LIMIT);
+ const lastRows = processedRows.slice(PREVIEW_LIMIT);
// Check for labels
if (showLabels === null) {
@@ -351,6 +319,7 @@ export default class Logs extends PureComponent {
firstRows.map(row => (
{
lastRows.map(row => (
|