mirror of
https://github.com/prometheus/prometheus.git
synced 2025-08-06 22:27:17 +02:00
* Added exemplar support Signed-off-by: Levi Harrison <git@leviharrison.dev> * Modified tests Signed-off-by: Levi Harrison <git@leviharrison.dev> * Fix eslint suggestions Signed-off-by: Levi Harrison <git@leviharrison.dev> * Address review comments Signed-off-by: Levi Harrison <git@leviharrison.dev> * Fixed undefined data property error Signed-off-by: Levi Harrison <git@leviharrison.dev> * Added series label section to tooltip Signed-off-by: Levi Harrison <git@leviharrison.dev> * Fixed spacing Signed-off-by: GitHub <noreply@github.com> Signed-off-by: Levi Harrison <git@leviharrison.dev> * Fixed tests Signed-off-by: Levi Harrison <git@leviharrison.dev> * Added exemplar info Signed-off-by: Levi Harrison <git@leviharrison.dev> * Changed exemplar symbol Signed-off-by: Levi Harrison <git@leviharrison.dev> Co-authored-by: Julius Volz <julius.volz@gmail.com> Signed-off-by: Levi Harrison <git@leviharrison.dev> * Hide selected exemplar info when 'Show Exemplars' is unchecked Signed-off-by: Levi Harrison <git@leviharrison.dev> * Include series labels in exemplar info Signed-off-by: Levi Harrison <git@leviharrison.dev> * De-densify exemplars Signed-off-by: Levi Harrison <git@leviharrison.dev> * Moved showExemplars to per-panel control Signed-off-by: Levi Harrison <git@leviharrison.dev> * Eslint fixes Signed-off-by: Levi Harrison <git@leviharrison.dev> * Address review comments Signed-off-by: Levi Harrison <git@leviharrison.dev> * Fixed tests Signed-off-by: Levi Harrison <git@leviharrison.dev> * Fix state bug Signed-off-by: Levi Harrison <git@leviharrison.dev> * Removed unused object Signed-off-by: Levi Harrison <git@leviharrison.dev> * Fix eslint Signed-off-by: Levi Harrison <git@leviharrison.dev> * Encoded 'show_exemplars' in url Signed-off-by: Levi Harrison <git@leviharrison.dev> Co-authored-by: Julius Volz <julius.volz@gmail.com>
238 lines
7.3 KiB
TypeScript
238 lines
7.3 KiB
TypeScript
import $ from 'jquery';
|
|
import React, { PureComponent } from 'react';
|
|
import ReactResizeDetector from 'react-resize-detector';
|
|
|
|
import { Legend } from './Legend';
|
|
import { Metric, ExemplarData, QueryParams } from '../../types/types';
|
|
import { isPresent } from '../../utils';
|
|
import { normalizeData, getOptions, toHoverColor } from './GraphHelpers';
|
|
|
|
require('../../vendor/flot/jquery.flot');
|
|
require('../../vendor/flot/jquery.flot.stack');
|
|
require('../../vendor/flot/jquery.flot.time');
|
|
require('../../vendor/flot/jquery.flot.crosshair');
|
|
require('jquery.flot.tooltip');
|
|
|
|
export interface GraphProps {
|
|
data: {
|
|
resultType: string;
|
|
result: Array<{ metric: Metric; values: [number, string][] }>;
|
|
};
|
|
exemplars: ExemplarData;
|
|
stacked: boolean;
|
|
useLocalTime: boolean;
|
|
showExemplars: boolean;
|
|
queryParams: QueryParams | null;
|
|
id: string;
|
|
}
|
|
|
|
export interface GraphSeries {
|
|
labels: { [key: string]: string };
|
|
color: string;
|
|
data: (number | null)[][]; // [x,y][]
|
|
index: number;
|
|
}
|
|
|
|
export interface GraphExemplar {
|
|
seriesLabels: { [key: string]: string };
|
|
labels: { [key: string]: string };
|
|
data: number[][];
|
|
points: any; // This is used to specify the symbol.
|
|
color: string;
|
|
}
|
|
|
|
export interface GraphData {
|
|
series: GraphSeries[];
|
|
exemplars: GraphExemplar[];
|
|
}
|
|
|
|
interface GraphState {
|
|
chartData: GraphData;
|
|
selectedExemplarLabels: { exemplar: { [key: string]: string }; series: { [key: string]: string } };
|
|
}
|
|
|
|
class Graph extends PureComponent<GraphProps, GraphState> {
|
|
private chartRef = React.createRef<HTMLDivElement>();
|
|
private $chart?: jquery.flot.plot;
|
|
private rafID = 0;
|
|
private selectedSeriesIndexes: number[] = [];
|
|
|
|
state = {
|
|
chartData: normalizeData(this.props),
|
|
selectedExemplarLabels: { exemplar: {}, series: {} },
|
|
};
|
|
|
|
componentDidUpdate(prevProps: GraphProps) {
|
|
const { data, stacked, useLocalTime, showExemplars } = this.props;
|
|
if (prevProps.data !== data) {
|
|
this.selectedSeriesIndexes = [];
|
|
this.setState({ chartData: normalizeData(this.props) }, this.plot);
|
|
} else if (prevProps.stacked !== stacked) {
|
|
this.setState({ chartData: normalizeData(this.props) }, () => {
|
|
if (this.selectedSeriesIndexes.length === 0) {
|
|
this.plot();
|
|
} else {
|
|
this.plot([
|
|
...this.state.chartData.series.filter((_, i) => this.selectedSeriesIndexes.includes(i)),
|
|
...this.state.chartData.exemplars,
|
|
]);
|
|
}
|
|
});
|
|
}
|
|
|
|
if (prevProps.useLocalTime !== useLocalTime) {
|
|
this.plot();
|
|
}
|
|
|
|
if (prevProps.showExemplars !== showExemplars && !showExemplars) {
|
|
this.setState(
|
|
{
|
|
chartData: { series: this.state.chartData.series, exemplars: [] },
|
|
selectedExemplarLabels: { exemplar: {}, series: {} },
|
|
},
|
|
() => {
|
|
this.plot();
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
componentDidMount() {
|
|
this.plot();
|
|
|
|
$(`.graph-${this.props.id}`).bind('plotclick', (event, pos, item) => {
|
|
// If an item has the series label property that means it's an exemplar.
|
|
if (item && 'seriesLabels' in item.series) {
|
|
this.setState({
|
|
selectedExemplarLabels: { exemplar: item.series.labels, series: item.series.seriesLabels },
|
|
chartData: this.state.chartData,
|
|
});
|
|
} else {
|
|
this.setState({
|
|
chartData: this.state.chartData,
|
|
selectedExemplarLabels: { exemplar: {}, series: {} },
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
this.destroyPlot();
|
|
}
|
|
|
|
plot = (data: (GraphSeries | GraphExemplar)[] = [...this.state.chartData.series, ...this.state.chartData.exemplars]) => {
|
|
if (!this.chartRef.current) {
|
|
return;
|
|
}
|
|
this.destroyPlot();
|
|
|
|
this.$chart = $.plot($(this.chartRef.current), data, getOptions(this.props.stacked, this.props.useLocalTime));
|
|
};
|
|
|
|
destroyPlot = () => {
|
|
if (isPresent(this.$chart)) {
|
|
this.$chart.destroy();
|
|
}
|
|
};
|
|
|
|
plotSetAndDraw(
|
|
data: (GraphSeries | GraphExemplar)[] = [...this.state.chartData.series, ...this.state.chartData.exemplars]
|
|
) {
|
|
if (isPresent(this.$chart)) {
|
|
this.$chart.setData(data);
|
|
this.$chart.draw();
|
|
}
|
|
}
|
|
|
|
handleSeriesSelect = (selected: number[], selectedIndex: number) => {
|
|
const { chartData } = this.state;
|
|
this.plot(
|
|
this.selectedSeriesIndexes.length === 1 && this.selectedSeriesIndexes.includes(selectedIndex)
|
|
? [...chartData.series.map(toHoverColor(selectedIndex, this.props.stacked)), ...chartData.exemplars]
|
|
: [
|
|
...chartData.series.filter((_, i) => selected.includes(i)),
|
|
...chartData.exemplars.filter(exemplar => {
|
|
series: for (const i in selected) {
|
|
for (const name in chartData.series[selected[i]].labels) {
|
|
if (exemplar.seriesLabels[name] !== chartData.series[selected[i]].labels[name]) {
|
|
continue series;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}),
|
|
] // draw only selected
|
|
);
|
|
this.selectedSeriesIndexes = selected;
|
|
};
|
|
|
|
handleSeriesHover = (index: number) => () => {
|
|
if (this.rafID) {
|
|
cancelAnimationFrame(this.rafID);
|
|
}
|
|
this.rafID = requestAnimationFrame(() => {
|
|
this.plotSetAndDraw([
|
|
...this.state.chartData.series.map(toHoverColor(index, this.props.stacked)),
|
|
...this.state.chartData.exemplars,
|
|
]);
|
|
});
|
|
};
|
|
|
|
handleLegendMouseOut = () => {
|
|
cancelAnimationFrame(this.rafID);
|
|
this.plotSetAndDraw();
|
|
};
|
|
|
|
handleResize = () => {
|
|
if (isPresent(this.$chart)) {
|
|
this.plot(this.$chart.getData() as (GraphSeries | GraphExemplar)[]);
|
|
}
|
|
};
|
|
|
|
render() {
|
|
const { chartData, selectedExemplarLabels } = this.state;
|
|
const selectedLabels = selectedExemplarLabels as {
|
|
exemplar: { [key: string]: string };
|
|
series: { [key: string]: string };
|
|
};
|
|
return (
|
|
<div className={`graph-${this.props.id}`}>
|
|
<ReactResizeDetector handleWidth onResize={this.handleResize} skipOnMount />
|
|
<div className="graph-chart" ref={this.chartRef} />
|
|
{Object.keys(selectedLabels.exemplar).length > 0 ? (
|
|
<div className="float-right">
|
|
<span style={{ fontSize: '17px' }}>Selected exemplar:</span>
|
|
<div className="labels mt-1">
|
|
{Object.keys(selectedLabels.exemplar).map((k, i) => (
|
|
<div key={i} style={{ fontSize: '15px' }}>
|
|
<strong>{k}</strong>: {selectedLabels.exemplar[k]}
|
|
</div>
|
|
))}
|
|
</div>
|
|
<span style={{ fontSize: '16px' }}>Series labels:</span>
|
|
<div className="labels mt-1">
|
|
{Object.keys(selectedLabels.series).map((k, i) => (
|
|
<div key={i} style={{ fontSize: '15px' }}>
|
|
<strong>{k}</strong>: {selectedLabels.series[k]}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
<Legend
|
|
shouldReset={this.selectedSeriesIndexes.length === 0}
|
|
chartData={chartData.series}
|
|
onHover={this.handleSeriesHover}
|
|
onLegendMouseOut={this.handleLegendMouseOut}
|
|
onSeriesToggle={this.handleSeriesSelect}
|
|
/>
|
|
{/* This is to make sure the graph box expands when the selected exemplar info pops up. */}
|
|
<br style={{ clear: 'both' }} />
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
|
|
export default Graph;
|