prometheus/web/ui/react-app/src/pages/graph/DataTable.tsx
Julius Volz 4ce23ce83e Start work on histogram visualization in the table view
Signed-off-by: Julius Volz <julius.volz@gmail.com>
2023-01-31 15:49:38 +01:00

213 lines
5.6 KiB
TypeScript

import React, { FC, ReactNode } from 'react';
import { Alert, Table, UncontrolledTooltip } from 'reactstrap';
import SeriesName from './SeriesName';
import { Metric, Histogram } from '../../types/types';
import moment from 'moment';
import { Tooltip as ReTooltip, Bar, BarChart, CartesianGrid, XAxis, YAxis } from 'recharts';
import { parseValue } from './GraphHelpers';
import HistogramChart from './HistogramChart';
export interface DataTableProps {
data:
| null
| {
resultType: 'vector';
result: InstantSample[];
}
| {
resultType: 'matrix';
result: RangeSamples[];
}
| {
resultType: 'scalar';
result: SampleValue;
}
| {
resultType: 'string';
result: string;
};
useLocalTime: boolean;
}
interface InstantSample {
metric: Metric;
value?: SampleValue;
histogram?: SampleHistogram;
}
interface RangeSamples {
metric: Metric;
values?: SampleValue[];
histograms?: SampleHistogram[];
}
type SampleValue = [number, string];
type SampleHistogram = [number, Histogram];
const limitSeries = <S extends InstantSample | RangeSamples>(series: S[]): S[] => {
const maxSeries = 10000;
if (series.length > maxSeries) {
return series.slice(0, maxSeries);
}
return series;
};
const DataTable: FC<DataTableProps> = ({ data, useLocalTime }) => {
if (data === null) {
return <Alert color="light">No data queried yet</Alert>;
}
if (data.result === null || data.result.length === 0) {
return <Alert color="secondary">Empty query result</Alert>;
}
const maxFormattableSize = 1000;
let rows: ReactNode[] = [];
let limited = false;
const doFormat = data.result.length <= maxFormattableSize;
switch (data.resultType) {
case 'vector':
rows = (limitSeries(data.result) as InstantSample[]).map((s: InstantSample, index: number): ReactNode => {
return (
<tr key={index}>
<td>
<SeriesName labels={s.metric} format={doFormat} />
</td>
<td>
{s.value && s.value[1]}
{s.histogram && (
<>
<HistogramChart histogram={s.histogram[1]} index={index} />
{histogramTable(s.histogram[1])}
</>
)}
</td>
</tr>
);
});
limited = rows.length !== data.result.length;
break;
case 'matrix':
rows = (limitSeries(data.result) as RangeSamples[]).map((s, seriesIdx) => {
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}>
{histogramTable(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} {histogramsAndTimes}
</td>
</tr>
);
});
limited = rows.length !== data.result.length;
break;
case 'scalar':
rows.push(
<tr key="0">
<td>scalar</td>
<td>{data.result[1]}</td>
</tr>
);
break;
case 'string':
rows.push(
<tr key="0">
<td>string</td>
<td>{data.result[1]}</td>
</tr>
);
break;
default:
return <Alert color="danger">Unsupported result value type</Alert>;
}
return (
<>
{limited && (
<Alert color="danger">
<strong>Warning:</strong> Fetched {data.result.length} metrics, only displaying first {rows.length}.
</Alert>
)}
{!doFormat && (
<Alert color="secondary">
<strong>Notice:</strong> Showing more than {maxFormattableSize} series, turning off label formatting for
performance reasons.
</Alert>
)}
<Table hover size="sm" className="data-table">
<tbody>{rows}</tbody>
</Table>
</>
);
};
const leftDelim = (br: number): string => (br === 3 || br === 1 ? '[' : '(');
const rightDelim = (br: number): string => (br === 3 || br === 0 ? ']' : ')');
export const bucketRangeString = ([boundaryRule, leftBoundary, rightBoundary, _]: [
number,
string,
string,
string
]): string => {
return `${leftDelim(boundaryRule)}${leftBoundary} 🠒 ${rightBoundary}${rightDelim(boundaryRule)}`;
};
export const histogramTable = (h: Histogram): ReactNode => (
<Table size="xs" responsive bordered>
<thead>
<tr>
<th style={{ textAlign: 'center' }} colSpan={2}>
Histogram Sample
</th>
</tr>
</thead>
<tbody>
<tr>
<th>count:</th>
<td>{h.count}</td>
</tr>
<tr>
<th>sum:</th>
<td>{h.sum}</td>
</tr>
{h.buckets?.map((b, i) => (
<tr key={i}>
<th>{bucketRangeString(b)}</th>
<td>{b[3]}</td>
</tr>
))}
</tbody>
</Table>
);
export default DataTable;