mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Prometheus: Fix exemplars not respecting corresponding series display status. (#59743)
* Exemplar filtering when series are toggled in legend UI
This commit is contained in:
parent
6f930f4836
commit
22f828300d
@ -39,7 +39,7 @@ type PrepData = (frames: DataFrame[]) => AlignedData | FacetedData;
|
||||
type PreDataStacked = (frames: DataFrame[], stackingGroups: StackingGroup[]) => AlignedData | FacetedData;
|
||||
|
||||
export class UPlotConfigBuilder {
|
||||
private series: UPlotSeriesBuilder[] = [];
|
||||
series: UPlotSeriesBuilder[] = [];
|
||||
private axes: Record<string, UPlotAxisBuilder> = {};
|
||||
private scales: UPlotScaleBuilder[] = [];
|
||||
private bands: Band[] = [];
|
||||
|
@ -4,10 +4,10 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
import { Field, getDisplayProcessor, PanelProps, getLinksSupplier } from '@grafana/data';
|
||||
import { Field, getDisplayProcessor, getLinksSupplier, PanelProps } from '@grafana/data';
|
||||
import { PanelDataErrorView } from '@grafana/runtime';
|
||||
import { TooltipDisplayMode } from '@grafana/schema';
|
||||
import { usePanelContext, TimeSeries, TooltipPlugin, ZoomPlugin, UPlotConfigBuilder, useTheme2 } from '@grafana/ui';
|
||||
import { TimeSeries, TooltipPlugin, UPlotConfigBuilder, usePanelContext, useTheme2, ZoomPlugin } from '@grafana/ui';
|
||||
import { AxisProps } from '@grafana/ui/src/components/uPlot/config/UPlotAxisBuilder';
|
||||
import { ScaleProps } from '@grafana/ui/src/components/uPlot/config/UPlotScaleBuilder';
|
||||
import { config } from 'app/core/config';
|
||||
@ -21,7 +21,7 @@ import { OutsideRangePlugin } from '../timeseries/plugins/OutsideRangePlugin';
|
||||
import { ThresholdControlsPlugin } from '../timeseries/plugins/ThresholdControlsPlugin';
|
||||
|
||||
import { prepareCandlestickFields } from './fields';
|
||||
import { defaultColors, CandlestickOptions, VizDisplayMode } from './models.gen';
|
||||
import { CandlestickOptions, defaultColors, VizDisplayMode } from './models.gen';
|
||||
import { drawMarkers, FieldIndices } from './utils';
|
||||
|
||||
interface CandlestickPanelProps extends PanelProps<CandlestickOptions> {}
|
||||
|
@ -3,14 +3,14 @@ import React, { useMemo } from 'react';
|
||||
import { Field, PanelProps } from '@grafana/data';
|
||||
import { PanelDataErrorView } from '@grafana/runtime';
|
||||
import { TooltipDisplayMode } from '@grafana/schema';
|
||||
import { usePanelContext, TimeSeries, TooltipPlugin, ZoomPlugin, KeyboardPlugin } from '@grafana/ui';
|
||||
import { KeyboardPlugin, TimeSeries, TooltipPlugin, usePanelContext, ZoomPlugin } from '@grafana/ui';
|
||||
import { config } from 'app/core/config';
|
||||
import { getFieldLinksForExplore } from 'app/features/explore/utils/links';
|
||||
|
||||
import { AnnotationEditorPlugin } from './plugins/AnnotationEditorPlugin';
|
||||
import { AnnotationsPlugin } from './plugins/AnnotationsPlugin';
|
||||
import { ContextMenuPlugin } from './plugins/ContextMenuPlugin';
|
||||
import { ExemplarsPlugin } from './plugins/ExemplarsPlugin';
|
||||
import { ExemplarsPlugin, getVisibleLabels } from './plugins/ExemplarsPlugin';
|
||||
import { OutsideRangePlugin } from './plugins/OutsideRangePlugin';
|
||||
import { ThresholdControlsPlugin } from './plugins/ThresholdControlsPlugin';
|
||||
import { TimeSeriesOptions } from './types';
|
||||
@ -133,6 +133,7 @@ export const TimeSeriesPanel: React.FC<TimeSeriesPanelProps> = ({
|
||||
)}
|
||||
{data.annotations && (
|
||||
<ExemplarsPlugin
|
||||
visibleLabels={getVisibleLabels(config, frames)}
|
||||
config={config}
|
||||
exemplars={data.annotations}
|
||||
timeZone={timeZone}
|
||||
|
@ -0,0 +1,97 @@
|
||||
import { Field, Labels, MutableDataFrame } from '@grafana/data/src';
|
||||
import { UPlotConfigBuilder } from '@grafana/ui/src';
|
||||
|
||||
import { getVisibleLabels } from './ExemplarsPlugin';
|
||||
|
||||
describe('getVisibleLabels()', () => {
|
||||
const dataFrameSeries1 = new MutableDataFrame({
|
||||
name: 'tns/app',
|
||||
fields: [
|
||||
{
|
||||
name: 'Time',
|
||||
values: [1670418750000, 1670418765000, 1670418780000, 1670418795000],
|
||||
entities: {},
|
||||
},
|
||||
{
|
||||
name: 'Value',
|
||||
labels: {
|
||||
job: 'tns/app',
|
||||
},
|
||||
values: [0.018963114754098367, 0.019140624999999974, 0.019718309859154928, 0.020064189189189167],
|
||||
},
|
||||
] as unknown as Field[],
|
||||
length: 4,
|
||||
});
|
||||
const dataFrameSeries2 = new MutableDataFrame({
|
||||
name: 'tns/db',
|
||||
fields: [
|
||||
{
|
||||
name: 'Time',
|
||||
values: [1670418750000, 1670418765000, 1670418780000, 1670418795000],
|
||||
entities: {},
|
||||
},
|
||||
{
|
||||
name: 'Value',
|
||||
labels: {
|
||||
job: 'tns/db',
|
||||
},
|
||||
values: [0.028963114754098367, 0.029140624999999974, 0.029718309859154928, 0.030064189189189167],
|
||||
},
|
||||
] as unknown as Field[],
|
||||
length: 4,
|
||||
});
|
||||
const dataFrameSeries3 = new MutableDataFrame({
|
||||
name: 'tns/loadgen',
|
||||
fields: [
|
||||
{
|
||||
name: 'Time',
|
||||
values: [1670418750000, 1670418765000, 1670418780000, 1670418795000],
|
||||
entities: {},
|
||||
},
|
||||
{
|
||||
name: 'Value',
|
||||
labels: {
|
||||
job: 'tns/loadgen',
|
||||
},
|
||||
values: [0.028963114754098367, 0.029140624999999974, 0.029718309859154928, 0.030064189189189167],
|
||||
},
|
||||
] as unknown as Field[],
|
||||
length: 4,
|
||||
});
|
||||
const frames = [dataFrameSeries1, dataFrameSeries2, dataFrameSeries3];
|
||||
const config: UPlotConfigBuilder = {
|
||||
addHook: (type, hook) => {},
|
||||
series: [
|
||||
{
|
||||
props: {
|
||||
dataFrameFieldIndex: { frameIndex: 0, fieldIndex: 1 },
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
props: {
|
||||
dataFrameFieldIndex: { frameIndex: 1, fieldIndex: 1 },
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
props: {
|
||||
dataFrameFieldIndex: { frameIndex: 2, fieldIndex: 1 },
|
||||
show: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
} as UPlotConfigBuilder;
|
||||
|
||||
it('function should only return labels associated with actively visible series', () => {
|
||||
const expected: { labels: Labels[]; totalSeriesCount: number } = {
|
||||
totalSeriesCount: 3,
|
||||
labels: [{ job: 'tns/app' }, { job: 'tns/db' }],
|
||||
};
|
||||
|
||||
// Base case
|
||||
expect(getVisibleLabels(config, [])).toEqual({ totalSeriesCount: 3, labels: [] });
|
||||
|
||||
expect(getVisibleLabels(config, frames)).toEqual(expected);
|
||||
});
|
||||
});
|
@ -5,10 +5,11 @@ import {
|
||||
DataFrame,
|
||||
DataFrameFieldIndex,
|
||||
Field,
|
||||
Labels,
|
||||
LinkModel,
|
||||
TimeZone,
|
||||
TIME_SERIES_TIME_FIELD_NAME,
|
||||
TIME_SERIES_VALUE_FIELD_NAME,
|
||||
TimeZone,
|
||||
} from '@grafana/data';
|
||||
import { EventsCanvas, FIXED_UNIT, UPlotConfigBuilder } from '@grafana/ui';
|
||||
|
||||
@ -19,9 +20,16 @@ interface ExemplarsPluginProps {
|
||||
exemplars: DataFrame[];
|
||||
timeZone: TimeZone;
|
||||
getFieldLinks: (field: Field, rowIndex: number) => Array<LinkModel<Field>>;
|
||||
visibleLabels?: { labels: Labels[]; totalSeriesCount: number };
|
||||
}
|
||||
|
||||
export const ExemplarsPlugin: React.FC<ExemplarsPluginProps> = ({ exemplars, timeZone, getFieldLinks, config }) => {
|
||||
export const ExemplarsPlugin: React.FC<ExemplarsPluginProps> = ({
|
||||
exemplars,
|
||||
timeZone,
|
||||
getFieldLinks,
|
||||
config,
|
||||
visibleLabels,
|
||||
}) => {
|
||||
const plotInstance = useRef<uPlot>();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
@ -63,6 +71,14 @@ export const ExemplarsPlugin: React.FC<ExemplarsPluginProps> = ({ exemplars, tim
|
||||
|
||||
const renderMarker = useCallback(
|
||||
(dataFrame: DataFrame, dataFrameFieldIndex: DataFrameFieldIndex) => {
|
||||
// If the parent provided series/labels: filter the exemplars, otherwise default to show all exemplars
|
||||
let showMarker =
|
||||
visibleLabels !== undefined ? showExemplarMarker(visibleLabels, dataFrame, dataFrameFieldIndex) : true;
|
||||
|
||||
if (!showMarker) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<ExemplarMarker
|
||||
timeZone={timeZone}
|
||||
@ -73,7 +89,7 @@ export const ExemplarsPlugin: React.FC<ExemplarsPluginProps> = ({ exemplars, tim
|
||||
/>
|
||||
);
|
||||
},
|
||||
[config, timeZone, getFieldLinks]
|
||||
[config, timeZone, getFieldLinks, visibleLabels]
|
||||
);
|
||||
|
||||
return (
|
||||
@ -86,3 +102,68 @@ export const ExemplarsPlugin: React.FC<ExemplarsPluginProps> = ({ exemplars, tim
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Function to get labels that are currently displayed in the legend
|
||||
*/
|
||||
export const getVisibleLabels = (
|
||||
config: UPlotConfigBuilder,
|
||||
frames: DataFrame[] | null
|
||||
): { labels: Labels[]; totalSeriesCount: number } => {
|
||||
const visibleSeries = config.series.filter((series) => series.props.show);
|
||||
const visibleLabels: Labels[] = [];
|
||||
if (frames?.length) {
|
||||
visibleSeries.forEach((plotInstance) => {
|
||||
const frameIndex = plotInstance.props?.dataFrameFieldIndex?.frameIndex;
|
||||
const fieldIndex = plotInstance.props?.dataFrameFieldIndex?.fieldIndex;
|
||||
|
||||
if (frameIndex !== undefined && fieldIndex !== undefined) {
|
||||
const field = frames[frameIndex].fields[fieldIndex];
|
||||
if (field.labels) {
|
||||
// Note that this may be an empty object in the case of a metric being rendered with no labels
|
||||
visibleLabels.push(field.labels);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return { labels: visibleLabels, totalSeriesCount: config.series.length };
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine if the current exemplar marker is filtered by what series are selected in the legend UI
|
||||
*/
|
||||
const showExemplarMarker = (
|
||||
visibleLabels: { labels: Labels[]; totalSeriesCount: number },
|
||||
dataFrame: DataFrame,
|
||||
dataFrameFieldIndex: DataFrameFieldIndex
|
||||
) => {
|
||||
let showMarker = false;
|
||||
if (visibleLabels.labels.length === visibleLabels.totalSeriesCount) {
|
||||
showMarker = true;
|
||||
} else {
|
||||
visibleLabels.labels.forEach((visibleLabel) => {
|
||||
const labelKeys = Object.keys(visibleLabel);
|
||||
// If there aren't any labels, the graph is only displaying a single series with exemplars, let's show all exemplars in this case as well
|
||||
if (Object.keys(visibleLabel).length === 0) {
|
||||
showMarker = true;
|
||||
} else {
|
||||
// If there are labels, lets only show the exemplars with labels associated with series that are currently visible
|
||||
const fields = dataFrame.fields.filter((field) => {
|
||||
return labelKeys.find((labelKey) => labelKey === field.name);
|
||||
});
|
||||
|
||||
// Check to see if at least one value matches each field
|
||||
if (fields.length) {
|
||||
showMarker = visibleLabels.labels.some((series) => {
|
||||
return Object.keys(series).every((label) => {
|
||||
const value = series[label];
|
||||
return fields.find((field) => field.values.get(dataFrameFieldIndex.fieldIndex) === value);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return showMarker;
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user