Prometheus: Fix exemplar fill color to match series color in time series. (#59908)

* Implementing new optional color property to ExemplarMarker component 
* Refactor some changes from #59743
This commit is contained in:
Galen Kistler 2022-12-08 14:15:05 -06:00 committed by GitHub
parent 76601f3ae7
commit dfc15163da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 101 additions and 31 deletions

View File

@ -133,7 +133,7 @@ export const TimeSeriesPanel: React.FC<TimeSeriesPanelProps> = ({
)} )}
{data.annotations && ( {data.annotations && (
<ExemplarsPlugin <ExemplarsPlugin
visibleLabels={getVisibleLabels(config, frames)} visibleSeries={getVisibleLabels(config, frames)}
config={config} config={config}
exemplars={data.annotations} exemplars={data.annotations}
timeZone={timeZone} timeZone={timeZone}

View File

@ -22,6 +22,7 @@ interface ExemplarMarkerProps {
dataFrameFieldIndex: DataFrameFieldIndex; dataFrameFieldIndex: DataFrameFieldIndex;
config: UPlotConfigBuilder; config: UPlotConfigBuilder;
getFieldLinks: (field: Field, rowIndex: number) => Array<LinkModel<Field>>; getFieldLinks: (field: Field, rowIndex: number) => Array<LinkModel<Field>>;
exemplarColor?: string;
} }
export const ExemplarMarker: React.FC<ExemplarMarkerProps> = ({ export const ExemplarMarker: React.FC<ExemplarMarkerProps> = ({
@ -30,6 +31,7 @@ export const ExemplarMarker: React.FC<ExemplarMarkerProps> = ({
dataFrameFieldIndex, dataFrameFieldIndex,
config, config,
getFieldLinks, getFieldLinks,
exemplarColor,
}) => { }) => {
const styles = useStyles2(getExemplarMarkerStyles); const styles = useStyles2(getExemplarMarkerStyles);
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
@ -40,19 +42,33 @@ export const ExemplarMarker: React.FC<ExemplarMarkerProps> = ({
const getSymbol = () => { const getSymbol = () => {
const symbols = [ const symbols = [
<rect key="diamond" x="3.38672" width="4.78985" height="4.78985" transform="rotate(45 3.38672 0)" />, <rect
fill={exemplarColor}
key="diamond"
x="3.38672"
width="4.78985"
height="4.78985"
transform="rotate(45 3.38672 0)"
/>,
<path <path
fill={exemplarColor}
key="x" key="x"
d="M1.94444 3.49988L0 5.44432L1.55552 6.99984L3.49996 5.05539L5.4444 6.99983L6.99992 5.44431L5.05548 3.49988L6.99983 1.55552L5.44431 0L3.49996 1.94436L1.5556 0L8.42584e-05 1.55552L1.94444 3.49988Z" d="M1.94444 3.49988L0 5.44432L1.55552 6.99984L3.49996 5.05539L5.4444 6.99983L6.99992 5.44431L5.05548 3.49988L6.99983 1.55552L5.44431 0L3.49996 1.94436L1.5556 0L8.42584e-05 1.55552L1.94444 3.49988Z"
/>, />,
<path key="triangle" d="M4 0L7.4641 6H0.535898L4 0Z" />, <path fill={exemplarColor} key="triangle" d="M4 0L7.4641 6H0.535898L4 0Z" />,
<rect key="rectangle" width="5" height="5" />, <rect fill={exemplarColor} key="rectangle" width="5" height="5" />,
<path key="pentagon" d="M3 0.5L5.85317 2.57295L4.76336 5.92705H1.23664L0.146831 2.57295L3 0.5Z" />,
<path <path
fill={exemplarColor}
key="pentagon"
d="M3 0.5L5.85317 2.57295L4.76336 5.92705H1.23664L0.146831 2.57295L3 0.5Z"
/>,
<path
fill={exemplarColor}
key="plus" key="plus"
d="m2.35672,4.2425l0,2.357l1.88558,0l0,-2.357l2.3572,0l0,-1.88558l-2.3572,0l0,-2.35692l-1.88558,0l0,2.35692l-2.35672,0l0,1.88558l2.35672,0z" d="m2.35672,4.2425l0,2.357l1.88558,0l0,-2.357l2.3572,0l0,-1.88558l-2.3572,0l0,-2.35692l-1.88558,0l0,2.35692l-2.35672,0l0,1.88558l2.35672,0z"
/>, />,
]; ];
return symbols[dataFrameFieldIndex.frameIndex % symbols.length]; return symbols[dataFrameFieldIndex.frameIndex % symbols.length];
}; };

View File

@ -1,7 +1,7 @@
import { Field, Labels, MutableDataFrame } from '@grafana/data/src'; import { MutableDataFrame, Field } from '@grafana/data/src';
import { UPlotConfigBuilder } from '@grafana/ui/src'; import { UPlotConfigBuilder } from '@grafana/ui/src';
import { getVisibleLabels } from './ExemplarsPlugin'; import { getVisibleLabels, VisibleExemplarLabels } from './ExemplarsPlugin';
describe('getVisibleLabels()', () => { describe('getVisibleLabels()', () => {
const dataFrameSeries1 = new MutableDataFrame({ const dataFrameSeries1 = new MutableDataFrame({
@ -84,9 +84,22 @@ describe('getVisibleLabels()', () => {
} as UPlotConfigBuilder; } as UPlotConfigBuilder;
it('function should only return labels associated with actively visible series', () => { it('function should only return labels associated with actively visible series', () => {
const expected: { labels: Labels[]; totalSeriesCount: number } = { const expected: VisibleExemplarLabels = {
totalSeriesCount: 3, totalSeriesCount: 3,
labels: [{ job: 'tns/app' }, { job: 'tns/db' }], labels: [
{
color: '',
labels: {
job: 'tns/app',
},
},
{
color: '',
labels: {
job: 'tns/db',
},
},
],
}; };
// Base case // Base case

View File

@ -20,7 +20,7 @@ interface ExemplarsPluginProps {
exemplars: DataFrame[]; exemplars: DataFrame[];
timeZone: TimeZone; timeZone: TimeZone;
getFieldLinks: (field: Field, rowIndex: number) => Array<LinkModel<Field>>; getFieldLinks: (field: Field, rowIndex: number) => Array<LinkModel<Field>>;
visibleLabels?: { labels: Labels[]; totalSeriesCount: number }; visibleSeries?: VisibleExemplarLabels;
} }
export const ExemplarsPlugin: React.FC<ExemplarsPluginProps> = ({ export const ExemplarsPlugin: React.FC<ExemplarsPluginProps> = ({
@ -28,7 +28,7 @@ export const ExemplarsPlugin: React.FC<ExemplarsPluginProps> = ({
timeZone, timeZone,
getFieldLinks, getFieldLinks,
config, config,
visibleLabels, visibleSeries,
}) => { }) => {
const plotInstance = useRef<uPlot>(); const plotInstance = useRef<uPlot>();
@ -71,9 +71,11 @@ export const ExemplarsPlugin: React.FC<ExemplarsPluginProps> = ({
const renderMarker = useCallback( const renderMarker = useCallback(
(dataFrame: DataFrame, dataFrameFieldIndex: DataFrameFieldIndex) => { (dataFrame: DataFrame, dataFrameFieldIndex: DataFrameFieldIndex) => {
// If the parent provided series/labels: filter the exemplars, otherwise default to show all exemplars const showMarker =
let showMarker = visibleSeries !== undefined ? showExemplarMarker(visibleSeries, dataFrame, dataFrameFieldIndex) : true;
visibleLabels !== undefined ? showExemplarMarker(visibleLabels, dataFrame, dataFrameFieldIndex) : true;
const markerColor =
visibleSeries !== undefined ? getExemplarColor(dataFrame, dataFrameFieldIndex, visibleSeries) : undefined;
if (!showMarker) { if (!showMarker) {
return <></>; return <></>;
@ -86,10 +88,11 @@ export const ExemplarsPlugin: React.FC<ExemplarsPluginProps> = ({
dataFrame={dataFrame} dataFrame={dataFrame}
dataFrameFieldIndex={dataFrameFieldIndex} dataFrameFieldIndex={dataFrameFieldIndex}
config={config} config={config}
exemplarColor={markerColor}
/> />
); );
}, },
[config, timeZone, getFieldLinks, visibleLabels] [config, timeZone, getFieldLinks, visibleSeries]
); );
return ( return (
@ -103,15 +106,13 @@ export const ExemplarsPlugin: React.FC<ExemplarsPluginProps> = ({
); );
}; };
export type VisibleExemplarLabels = { labels: LabelWithExemplarUIData[]; totalSeriesCount: number };
/** /**
* Function to get labels that are currently displayed in the legend * Get labels that are currently visible/active in the legend
*/ */
export const getVisibleLabels = ( export const getVisibleLabels = (config: UPlotConfigBuilder, frames: DataFrame[] | null): VisibleExemplarLabels => {
config: UPlotConfigBuilder,
frames: DataFrame[] | null
): { labels: Labels[]; totalSeriesCount: number } => {
const visibleSeries = config.series.filter((series) => series.props.show); const visibleSeries = config.series.filter((series) => series.props.show);
const visibleLabels: Labels[] = []; const visibleLabels: LabelWithExemplarUIData[] = [];
if (frames?.length) { if (frames?.length) {
visibleSeries.forEach((plotInstance) => { visibleSeries.forEach((plotInstance) => {
const frameIndex = plotInstance.props?.dataFrameFieldIndex?.frameIndex; const frameIndex = plotInstance.props?.dataFrameFieldIndex?.frameIndex;
@ -121,7 +122,10 @@ export const getVisibleLabels = (
const field = frames[frameIndex].fields[fieldIndex]; const field = frames[frameIndex].fields[fieldIndex];
if (field.labels) { if (field.labels) {
// Note that this may be an empty object in the case of a metric being rendered with no labels // Note that this may be an empty object in the case of a metric being rendered with no labels
visibleLabels.push(field.labels); visibleLabels.push({
labels: field.labels,
color: plotInstance.props?.lineColor ?? '',
});
} }
} }
}); });
@ -130,22 +134,59 @@ export const getVisibleLabels = (
return { labels: visibleLabels, totalSeriesCount: config.series.length }; return { labels: visibleLabels, totalSeriesCount: config.series.length };
}; };
interface LabelWithExemplarUIData {
labels: Labels;
color?: string;
}
/**
* Get color of active series in legend
*/
const getExemplarColor = (
dataFrame: DataFrame,
dataFrameFieldIndex: DataFrameFieldIndex,
visibleLabels: VisibleExemplarLabels
) => {
let exemplarColor;
visibleLabels.labels.some((visibleLabel) => {
const labelKeys = Object.keys(visibleLabel.labels);
const fields = dataFrame.fields.filter((field) => {
return labelKeys.find((labelKey) => labelKey === field.name);
});
if (fields.length) {
const hasMatch = fields.every((field, index, fields) => {
const value = field.values.get(dataFrameFieldIndex.fieldIndex);
return visibleLabel.labels[field.name] === value;
});
if (hasMatch) {
exemplarColor = visibleLabel.color;
return true;
}
}
return false;
});
return exemplarColor;
};
/** /**
* Determine if the current exemplar marker is filtered by what series are selected in the legend UI * Determine if the current exemplar marker is filtered by what series are selected in the legend UI
*/ */
const showExemplarMarker = ( const showExemplarMarker = (
visibleLabels: { labels: Labels[]; totalSeriesCount: number }, visibleSeries: VisibleExemplarLabels,
dataFrame: DataFrame, dataFrame: DataFrame,
dataFrameFieldIndex: DataFrameFieldIndex dataFrameFieldIndex: DataFrameFieldIndex
) => { ) => {
let showMarker = false; let showMarker = false;
if (visibleLabels.labels.length === visibleLabels.totalSeriesCount) { // If all series are visible, don't filter any exemplars
if (visibleSeries.labels.length === visibleSeries.totalSeriesCount) {
showMarker = true; showMarker = true;
} else { } else {
visibleLabels.labels.forEach((visibleLabel) => { visibleSeries.labels.forEach((visibleLabel) => {
const labelKeys = Object.keys(visibleLabel); // Get the label names
const labelKeys = Object.keys(visibleLabel.labels);
// 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 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) { if (Object.keys(visibleLabel.labels).length === 0) {
showMarker = true; showMarker = true;
} else { } else {
// If there are labels, lets only show the exemplars with labels associated with series that are currently visible // If there are labels, lets only show the exemplars with labels associated with series that are currently visible
@ -153,11 +194,11 @@ const showExemplarMarker = (
return labelKeys.find((labelKey) => labelKey === field.name); return labelKeys.find((labelKey) => labelKey === field.name);
}); });
// Check to see if at least one value matches each field
if (fields.length) { if (fields.length) {
showMarker = visibleLabels.labels.some((series) => { // Check to see if at least one value matches each field
return Object.keys(series).every((label) => { showMarker = visibleSeries.labels.some((series) => {
const value = series[label]; return Object.keys(series.labels).every((label) => {
const value = series.labels[label];
return fields.find((field) => field.values.get(dataFrameFieldIndex.fieldIndex) === value); return fields.find((field) => field.values.get(dataFrameFieldIndex.fieldIndex) === value);
}); });
}); });