Histogram support in table view

Signed-off-by: beorn7 <beorn@grafana.com>
This commit is contained in:
beorn7 2022-04-27 17:25:06 +02:00
parent bcc919cb19
commit 77a362b771
5 changed files with 152 additions and 23 deletions

View file

@ -1,6 +1,7 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import DataTable, { DataTableProps } from './DataTable';
import HistogramString, { HistogramStringProps } from './DataTable';
import { Alert, Table } from 'reactstrap';
import SeriesName from './SeriesName';
@ -71,7 +72,81 @@ describe('DataTable', () => {
const table = dataTable.find(Table);
table.find('tr').forEach((row, idx) => {
expect(row.find(SeriesName)).toHaveLength(1);
expect(row.find('td').at(1).text()).toEqual(`${idx}`);
expect(row.find('td').at(1).text()).toEqual(`${idx} <HistogramString />`);
});
});
});
describe('when resultType is a vector with histograms', () => {
const dataTableProps: DataTableProps = {
data: {
resultType: 'vector',
result: [
{
metric: {
__name__: 'metric_name_1',
label1: 'value_1',
labeln: 'value_n',
},
histogram: [
1572098246.599,
{
"count": "10",
"sum": "3.3",
"buckets": [
[ 1, "-1", "-0.5", "2"],
[ 3, "-0.5", "0.5", "3"],
[ 0, "0.5", "1", "5"],
]
}
],
},
{
metric: {
__name__: 'metric_name_2',
label1: 'value_1',
labeln: 'value_n',
},
histogram: [
1572098247.599,
{
"count": "5",
"sum": "1.11",
"buckets": [
[ 0, "0.5", "1", "2"],
[ 0, "1", "2", "3"],
]
}
],
},
{
metric: {
__name__: 'metric_name_2',
label1: 'value_1',
labeln: 'value_n',
},
},
],
},
useLocalTime: false,
};
const dataTable = shallow(<DataTable {...dataTableProps} />);
it('renders a table', () => {
const table = dataTable.find(Table);
expect(table.prop('hover')).toBe(true);
expect(table.prop('size')).toEqual('sm');
expect(table.prop('className')).toEqual('data-table');
expect(table.find('tbody')).toHaveLength(1);
});
it('renders rows', () => {
const table = dataTable.find(Table);
table.find('tr').forEach((row, idx) => {
expect(row.find(SeriesName)).toHaveLength(1);
// TODO(beorn7): This doesn't actually test the rendoring yet. Need to trigger it somehow.
expect(row.find('td').at(1).text()).toEqual(` <HistogramString />`);
});
});
});
@ -239,7 +314,7 @@ describe('DataTable', () => {
expect(table.find('tr')).toHaveLength(3);
const row = rows.at(0);
expect(row.text()).toEqual(
`<SeriesName />9 @1572097950.9310 @1572097965.93111 @1572097980.92912 @1572097995.93113 @1572098010.93214 @1572098025.93315 @1572098040.9316 @1572098055.9317 @1572098070.9318 @1572098085.93619 @1572098100.93620 @1572098115.93321 @1572098130.93222 @1572098145.93223 @1572098160.93324 @1572098175.93425 @1572098190.93726 @1572098205.93427 @1572098220.93328 @1572098235.934`
`<SeriesName />9 @1572097950.9310 @1572097965.93111 @1572097980.92912 @1572097995.93113 @1572098010.93214 @1572098025.93315 @1572098040.9316 @1572098055.9317 @1572098070.9318 @1572098085.93619 @1572098100.93620 @1572098115.93321 @1572098130.93222 @1572098145.93223 @1572098160.93324 @1572098175.93425 @1572098190.93726 @1572098205.93427 @1572098220.93328 @1572098235.934 `
);
});
});

View file

@ -3,7 +3,7 @@ import React, { FC, ReactNode } from 'react';
import { Alert, Table } from 'reactstrap';
import SeriesName from './SeriesName';
import { Metric } from '../../types/types';
import { Metric, Histogram } from '../../types/types';
import moment from 'moment';
@ -31,15 +31,18 @@ export interface DataTableProps {
interface InstantSample {
metric: Metric;
value: SampleValue;
value?: SampleValue;
histogram?: SampleHistogram;
}
interface RangeSamples {
metric: Metric;
values: SampleValue[];
values?: SampleValue[];
histograms?: SampleHistogram[];
}
type SampleValue = [number, string];
type SampleHistogram = [number, Histogram];
const limitSeries = <S extends InstantSample | RangeSamples>(series: S[]): S[] => {
const maxSeries = 10000;
@ -71,7 +74,9 @@ const DataTable: FC<DataTableProps> = ({ data, useLocalTime }) => {
<td>
<SeriesName labels={s.metric} format={doFormat} />
</td>
<td>{s.value[1]}</td>
<td>
{s.value && s.value[1]} <HistogramString h={s.histogram && s.histogram[1]} />
</td>
</tr>
);
});
@ -79,21 +84,36 @@ const DataTable: FC<DataTableProps> = ({ data, useLocalTime }) => {
break;
case 'matrix':
rows = (limitSeries(data.result) as RangeSamples[]).map((s, seriesIdx) => {
const valuesAndTimes = s.values.map((v, valIdx) => {
const printedDatetime = moment.unix(v[0]).toISOString(useLocalTime);
return (
<React.Fragment key={valIdx}>
{v[1]} @{<span title={printedDatetime}>{v[0]}</span>}
<br />
</React.Fragment>
);
});
const valuesAndTimes = s.values
? s.values.map((v, valIdx) => {
const printedDatetime = moment.unix(v[0]).toISOString(useLocalTime);
return (
<React.Fragment key={valIdx}>
{v[1]} @{<span title={printedDatetime}>{v[0]}</span>}
<br />
</React.Fragment>
);
})
: [];
const histogramsAndTimes = s.histograms
? s.histograms.map((h, hisIdx) => {
const printedDatetime = moment.unix(h[0]).toISOString(useLocalTime);
return (
<React.Fragment key={-hisIdx}>
<HistogramString h={h[1]} /> @{<span title={printedDatetime}>{h[0]}</span>}
<br />
</React.Fragment>
);
})
: [];
return (
<tr style={{ whiteSpace: 'pre' }} key={seriesIdx}>
<td>
<SeriesName labels={s.metric} format={doFormat} />
</td>
<td>{valuesAndTimes}</td>
<td>
{valuesAndTimes} {histogramsAndTimes}
</td>
</tr>
);
});
@ -139,4 +159,27 @@ const DataTable: FC<DataTableProps> = ({ data, useLocalTime }) => {
);
};
export interface HistogramStringProps {
h?: Histogram;
}
export const HistogramString: FC<HistogramStringProps> = ({ h }) => {
if (!h) {
return <></>;
}
const buckets: string[] = [];
for (const bucket of h.buckets) {
const left = bucket[0] === 3 || bucket[0] === 1 ? '[' : '(';
const right = bucket[0] === 3 || bucket[0] === 0 ? ']' : ')';
buckets.push(left + bucket[1] + ',' + bucket[2] + right + ':' + bucket[3] + ' ');
}
return (
<>
{'{'} count:{h.count} sum:{h.sum} {buckets} {'}'}
</>
);
};
export default DataTable;

View file

@ -3,7 +3,7 @@ import React, { PureComponent } from 'react';
import ReactResizeDetector from 'react-resize-detector';
import { Legend } from './Legend';
import { Metric, ExemplarData, QueryParams } from '../../types/types';
import { Metric, Histogram, ExemplarData, QueryParams } from '../../types/types';
import { isPresent } from '../../utils';
import { normalizeData, getOptions, toHoverColor } from './GraphHelpers';
import { Button } from 'reactstrap';
@ -20,7 +20,7 @@ require('jquery.flot.tooltip');
export interface GraphProps {
data: {
resultType: string;
result: Array<{ metric: Metric; values: [number, string][] }>;
result: Array<{ metric: Metric; values?: [number, string][]; histograms?: [number, Histogram][] }>;
};
exemplars: ExemplarData;
stacked: boolean;

View file

@ -189,17 +189,22 @@ export const normalizeData = ({ queryParams, data, exemplars, stacked }: GraphPr
const deviation = stdDeviation(sum, values);
return {
series: data.result.map(({ values, metric }, index) => {
series: data.result.map(({ values, histograms, metric }, index) => {
// Insert nulls for all missing steps.
const data = [];
let pos = 0;
let valuePos = 0;
let histogramPos = 0;
for (let t = startTime; t <= endTime; t += resolution) {
// Allow for floating point inaccuracy.
const currentValue = values[pos];
if (values.length > pos && currentValue[0] < t + resolution / 100) {
const currentValue = values && values[valuePos];
const currentHistogram = histograms && histograms[histogramPos];
if (currentValue && values.length > valuePos && currentValue[0] < t + resolution / 100) {
data.push([currentValue[0] * 1000, parseValue(currentValue[1])]);
pos++;
valuePos++;
} else if (currentHistogram && histograms.length > histogramPos && currentHistogram[0] < t + resolution / 100) {
data.push([currentHistogram[0] * 1000, parseValue(currentHistogram[1].sum)]);
histogramPos++;
} else {
data.push([t * 1000, null]);
}

View file

@ -4,6 +4,12 @@ export interface Metric {
[key: string]: string;
}
export interface Histogram {
count: string;
sum: string;
buckets: [number, string, string, string][];
}
export interface Exemplar {
labels: { [key: string]: string };
value: string;