From f0213a7308a94de35f40867ec92a199d6964be08 Mon Sep 17 00:00:00 2001 From: Galen Date: Fri, 2 Dec 2022 11:37:38 -0600 Subject: [PATCH 01/27] PoC: Working exemplar filtering when series are toggled in legend UI --- .../components/Graph/GraphSeriesToggler.tsx | 2 + .../uPlot/config/UPlotConfigBuilder.ts | 2 +- .../panel/candlestick/CandlestickPanel.tsx | 3 +- .../panel/timeseries/TimeSeriesPanel.tsx | 5 +- .../timeseries/plugins/ExemplarsPlugin.tsx | 52 ++++++++++++++++++- 5 files changed, 58 insertions(+), 6 deletions(-) diff --git a/packages/grafana-ui/src/components/Graph/GraphSeriesToggler.tsx b/packages/grafana-ui/src/components/Graph/GraphSeriesToggler.tsx index 693cf9e9758f..57a6e0338790 100644 --- a/packages/grafana-ui/src/components/Graph/GraphSeriesToggler.tsx +++ b/packages/grafana-ui/src/components/Graph/GraphSeriesToggler.tsx @@ -41,6 +41,8 @@ export class GraphSeriesToggler extends Component) { const { series, onHiddenSeriesChanged } = this.props; const { hiddenSeries } = this.state; + console.log('onSeriesToggle series', series); + console.log('onSeriesToggle hidden series', hiddenSeries); if (event.ctrlKey || event.metaKey || event.shiftKey) { // Toggling series with key makes the series itself to toggle diff --git a/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts b/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts index 839935cd1e5c..f3c8a3824d4f 100644 --- a/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts +++ b/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts @@ -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 = {}; private scales: UPlotScaleBuilder[] = []; private bands: Band[] = []; diff --git a/public/app/plugins/panel/candlestick/CandlestickPanel.tsx b/public/app/plugins/panel/candlestick/CandlestickPanel.tsx index 67f5d264c89b..b2ebc43d5e9d 100644 --- a/public/app/plugins/panel/candlestick/CandlestickPanel.tsx +++ b/public/app/plugins/panel/candlestick/CandlestickPanel.tsx @@ -16,7 +16,7 @@ import { getFieldLinksForExplore } from 'app/features/explore/utils/links'; import { AnnotationEditorPlugin } from '../timeseries/plugins/AnnotationEditorPlugin'; import { AnnotationsPlugin } from '../timeseries/plugins/AnnotationsPlugin'; import { ContextMenuPlugin } from '../timeseries/plugins/ContextMenuPlugin'; -import { ExemplarsPlugin } from '../timeseries/plugins/ExemplarsPlugin'; +import { ExemplarsPlugin, getVisibleLabels } from '../timeseries/plugins/ExemplarsPlugin'; import { OutsideRangePlugin } from '../timeseries/plugins/OutsideRangePlugin'; import { ThresholdControlsPlugin } from '../timeseries/plugins/ThresholdControlsPlugin'; @@ -313,6 +313,7 @@ export const CandlestickPanel: React.FC = ({ )} {data.annotations && ( = ({ )} {data.annotations && ( Array>; + visibleLabels: Labels[]; } -export const ExemplarsPlugin: React.FC = ({ exemplars, timeZone, getFieldLinks, config }) => { +export const getVisibleLabels = (config: UPlotConfigBuilder, frames: DataFrame[] | null) => { + 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) { + visibleLabels.push(field.labels); + } + } + }); + } + + return visibleLabels; +}; + +export const ExemplarsPlugin: React.FC = ({ + exemplars, + timeZone, + getFieldLinks, + config, + visibleLabels, +}) => { const plotInstance = useRef(); useLayoutEffect(() => { @@ -63,6 +91,26 @@ export const ExemplarsPlugin: React.FC = ({ exemplars, tim const renderMarker = useCallback( (dataFrame: DataFrame, dataFrameFieldIndex: DataFrameFieldIndex) => { + let showMarker = false; + + visibleLabels.forEach((visibleLabel) => { + const labelKeys = Object.keys(visibleLabel); + const labelValues = Object.values(visibleLabel); + + const field = dataFrame.fields.find((field) => labelKeys.find((labelKey) => labelKey === field.name)); + + if (field) { + const value = field.values.get(dataFrameFieldIndex.fieldIndex); + if (labelValues.includes(value)) { + showMarker = true; + } + } + }); + + if (!showMarker) { + return <>; + } + return ( = ({ exemplars, tim /> ); }, - [config, timeZone, getFieldLinks] + [config, timeZone, getFieldLinks, visibleLabels] ); return ( From f589afc1d6da0ad0b2b2f024c55e2b05d5069ffd Mon Sep 17 00:00:00 2001 From: Galen Date: Fri, 2 Dec 2022 11:42:23 -0600 Subject: [PATCH 02/27] remove console.logs --- packages/grafana-ui/src/components/Graph/GraphSeriesToggler.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/grafana-ui/src/components/Graph/GraphSeriesToggler.tsx b/packages/grafana-ui/src/components/Graph/GraphSeriesToggler.tsx index 57a6e0338790..693cf9e9758f 100644 --- a/packages/grafana-ui/src/components/Graph/GraphSeriesToggler.tsx +++ b/packages/grafana-ui/src/components/Graph/GraphSeriesToggler.tsx @@ -41,8 +41,6 @@ export class GraphSeriesToggler extends Component) { const { series, onHiddenSeriesChanged } = this.props; const { hiddenSeries } = this.state; - console.log('onSeriesToggle series', series); - console.log('onSeriesToggle hidden series', hiddenSeries); if (event.ctrlKey || event.metaKey || event.shiftKey) { // Toggling series with key makes the series itself to toggle From 9ab0aff532cfacb8f384e0f87ccaf1ba3d7ecf63 Mon Sep 17 00:00:00 2001 From: Galen Date: Mon, 5 Dec 2022 10:43:06 -0600 Subject: [PATCH 03/27] fix bug in which single series exemplar graphs wouldn't display exemplars --- .../timeseries/plugins/ExemplarsPlugin.tsx | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx index 498781d0672e..c1f9a31caa3f 100644 --- a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx +++ b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx @@ -34,12 +34,15 @@ export const getVisibleLabels = (config: UPlotConfigBuilder, frames: DataFrame[] if (frameIndex !== undefined && fieldIndex !== undefined) { const field = frames[frameIndex].fields[fieldIndex]; if (field.labels) { + // Note that this may be an empty object visibleLabels.push(field.labels); } } }); } + console.log('getVisibleLabels', visibleLabels); + return visibleLabels; }; @@ -97,12 +100,18 @@ export const ExemplarsPlugin: React.FC = ({ const labelKeys = Object.keys(visibleLabel); const labelValues = Object.values(visibleLabel); - const field = dataFrame.fields.find((field) => labelKeys.find((labelKey) => labelKey === field.name)); - - if (field) { - const value = field.values.get(dataFrameFieldIndex.fieldIndex); - if (labelValues.includes(value)) { - showMarker = true; + // If there aren't any labels, the graph is only displaying a single source of exemplars, let's show them + if (Object.keys(visibleLabel).length === 0) { + showMarker = true; + } else { + // If there are labels, lets only show the labels associated with series that are currently visible + const field = dataFrame.fields.find((field) => labelKeys.find((labelKey) => labelKey === field.name)); + + if (field) { + const value = field.values.get(dataFrameFieldIndex.fieldIndex); + if (labelValues.includes(value)) { + showMarker = true; + } } } }); From 3bc429c2f7116cef13042df7c3b2a3c8251c2e97 Mon Sep 17 00:00:00 2001 From: Galen Date: Mon, 5 Dec 2022 10:43:54 -0600 Subject: [PATCH 04/27] remove console.log --- public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx index c1f9a31caa3f..b0ffffdeebf9 100644 --- a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx +++ b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx @@ -41,8 +41,6 @@ export const getVisibleLabels = (config: UPlotConfigBuilder, frames: DataFrame[] }); } - console.log('getVisibleLabels', visibleLabels); - return visibleLabels; }; From 5926f6d2aac4851aba603bc0cd962436ee4a2f62 Mon Sep 17 00:00:00 2001 From: Galen Date: Mon, 5 Dec 2022 11:54:35 -0600 Subject: [PATCH 05/27] fix edge cases breaking exemplar filtering --- .../timeseries/plugins/ExemplarsPlugin.tsx | 51 +++++++++++-------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx index b0ffffdeebf9..662d31a17a26 100644 --- a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx +++ b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx @@ -20,10 +20,13 @@ interface ExemplarsPluginProps { exemplars: DataFrame[]; timeZone: TimeZone; getFieldLinks: (field: Field, rowIndex: number) => Array>; - visibleLabels: Labels[]; + visibleLabels: { labels: Labels[]; totalSeriesCount: number }; } -export const getVisibleLabels = (config: UPlotConfigBuilder, frames: DataFrame[] | null) => { +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) { @@ -41,7 +44,7 @@ export const getVisibleLabels = (config: UPlotConfigBuilder, frames: DataFrame[] }); } - return visibleLabels; + return { labels: visibleLabels, totalSeriesCount: config.series.length }; }; export const ExemplarsPlugin: React.FC = ({ @@ -94,25 +97,33 @@ export const ExemplarsPlugin: React.FC = ({ (dataFrame: DataFrame, dataFrameFieldIndex: DataFrameFieldIndex) => { let showMarker = false; - visibleLabels.forEach((visibleLabel) => { - const labelKeys = Object.keys(visibleLabel); - const labelValues = Object.values(visibleLabel); - - // If there aren't any labels, the graph is only displaying a single source of exemplars, let's show them - if (Object.keys(visibleLabel).length === 0) { - showMarker = true; - } else { - // If there are labels, lets only show the labels associated with series that are currently visible - const field = dataFrame.fields.find((field) => labelKeys.find((labelKey) => labelKey === field.name)); - - if (field) { - const value = field.values.get(dataFrameFieldIndex.fieldIndex); - if (labelValues.includes(value)) { - showMarker = true; + // If all series are visible, don't filter any exemplars + if (visibleLabels.labels.length === visibleLabels.totalSeriesCount) { + showMarker = true; + } else { + visibleLabels.labels.forEach((visibleLabel) => { + const labelKeys = Object.keys(visibleLabel); + const labelValues = Object.values(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) => labelKeys.find((labelKey) => labelKey === field.name)); + + if (fields.length > 1) { + showMarker = fields.every((field) => { + const value = field.values.get(dataFrameFieldIndex.fieldIndex); + if (labelValues.includes(value)) { + return true; + } + return false; + }); } } - } - }); + }); + } if (!showMarker) { return <>; From 6fed0cc4b56707bd5961675e34246c6b5c275af8 Mon Sep 17 00:00:00 2001 From: Galen Date: Mon, 5 Dec 2022 13:48:54 -0600 Subject: [PATCH 06/27] fix off by one bug --- public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx index 662d31a17a26..d01c23131dda 100644 --- a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx +++ b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx @@ -112,7 +112,7 @@ export const ExemplarsPlugin: React.FC = ({ // If there are labels, lets only show the exemplars with labels associated with series that are currently visible const fields = dataFrame.fields.filter((field) => labelKeys.find((labelKey) => labelKey === field.name)); - if (fields.length > 1) { + if (fields.length) { showMarker = fields.every((field) => { const value = field.values.get(dataFrameFieldIndex.fieldIndex); if (labelValues.includes(value)) { From daaea0d547aad47a3352b29fa904670fd385c332 Mon Sep 17 00:00:00 2001 From: Galen Date: Tue, 6 Dec 2022 09:16:58 -0600 Subject: [PATCH 07/27] fix bug in feature when multiple series are selected in legend, exemplars wouldn't filter --- .../timeseries/plugins/ExemplarsPlugin.tsx | 36 ++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx index d01c23131dda..2fe4f8b0c627 100644 --- a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx +++ b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx @@ -7,9 +7,9 @@ import { 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'; @@ -37,7 +37,7 @@ export const getVisibleLabels = ( if (frameIndex !== undefined && fieldIndex !== undefined) { const field = frames[frameIndex].fields[fieldIndex]; if (field.labels) { - // Note that this may be an empty object + // Note that this may be an empty object in the case of a metric being rendered with no labels visibleLabels.push(field.labels); } } @@ -93,6 +93,24 @@ export const ExemplarsPlugin: React.FC = ({ }; }, []); + // Merge values from objects with same props + const getUniqueValuesFromLabels = (labels: Labels[]) => { + const labelToSet: { [index: string]: Set } = {}; + + Object.values(labels).forEach((labelObject) => { + return Object.keys(labelObject).forEach((labelName) => { + const labelValue: string = labelObject[labelName]; + if (typeof labelToSet[labelName] === 'undefined') { + labelToSet[labelName] = new Set().add(labelValue); + } else { + labelToSet[labelName].add(labelValue); + } + }); + }); + + return labelToSet; + }; + const renderMarker = useCallback( (dataFrame: DataFrame, dataFrameFieldIndex: DataFrameFieldIndex) => { let showMarker = false; @@ -103,22 +121,24 @@ export const ExemplarsPlugin: React.FC = ({ } else { visibleLabels.labels.forEach((visibleLabel) => { const labelKeys = Object.keys(visibleLabel); - const labelValues = Object.values(visibleLabel); + const labelsAndUniqueValuesFromActiveFilters = getUniqueValuesFromLabels(visibleLabels.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 (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) => labelKeys.find((labelKey) => labelKey === field.name)); + 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 = fields.every((field) => { const value = field.values.get(dataFrameFieldIndex.fieldIndex); - if (labelValues.includes(value)) { - return true; - } - return false; + const allValues = labelsAndUniqueValuesFromActiveFilters[field.name]; + + return [...allValues].includes(value); }); } } From 4edb81e4691f2d0e51eecb916e6bfe85e9235ce3 Mon Sep 17 00:00:00 2001 From: Galen Date: Tue, 6 Dec 2022 11:23:31 -0600 Subject: [PATCH 08/27] add exemplar color coding --- .../timeseries/plugins/ExemplarMarker.tsx | 24 ++++++++++--- .../timeseries/plugins/ExemplarsPlugin.tsx | 34 +++++++++++++++++-- 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/public/app/plugins/panel/timeseries/plugins/ExemplarMarker.tsx b/public/app/plugins/panel/timeseries/plugins/ExemplarMarker.tsx index 6d2e18a582b6..ea4dfc95f1af 100644 --- a/public/app/plugins/panel/timeseries/plugins/ExemplarMarker.tsx +++ b/public/app/plugins/panel/timeseries/plugins/ExemplarMarker.tsx @@ -22,6 +22,7 @@ interface ExemplarMarkerProps { dataFrameFieldIndex: DataFrameFieldIndex; config: UPlotConfigBuilder; getFieldLinks: (field: Field, rowIndex: number) => Array>; + exemplarColor?: string; } export const ExemplarMarker: React.FC = ({ @@ -30,6 +31,7 @@ export const ExemplarMarker: React.FC = ({ dataFrameFieldIndex, config, getFieldLinks, + exemplarColor, }) => { const styles = useStyles2(getExemplarMarkerStyles); const [isOpen, setIsOpen] = useState(false); @@ -40,19 +42,33 @@ export const ExemplarMarker: React.FC = ({ const getSymbol = () => { const symbols = [ - , + , , - , - , - , + , + , + , , ]; + return symbols[dataFrameFieldIndex.frameIndex % symbols.length]; }; diff --git a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx index 2fe4f8b0c627..f7b9f84d6d02 100644 --- a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx +++ b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx @@ -38,7 +38,7 @@ export const getVisibleLabels = ( 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); + visibleLabels.push({ ...field.labels, __color__: plotInstance.props?.lineColor ?? '' }); } } }); @@ -47,6 +47,33 @@ export const getVisibleLabels = ( return { labels: visibleLabels, totalSeriesCount: config.series.length }; }; +// Get color of active series in legend +const getExemplarColor = ( + dataFrame: DataFrame, + dataFrameFieldIndex: DataFrameFieldIndex, + visibleLabels: { labels: Labels[]; totalSeriesCount: number } +) => { + let exemplarColor; + visibleLabels.labels.forEach((visibleLabel) => { + const labelKeys = Object.keys(visibleLabel); + const fields = dataFrame.fields.filter((field) => { + return labelKeys.find((labelKey) => labelKey === field.name); + }); + if (fields.length) { + const hasMatch = fields.every((field) => { + const value = field.values.get(dataFrameFieldIndex.fieldIndex); + return visibleLabel[field.name] === value; + }); + + if (hasMatch) { + exemplarColor = visibleLabel['__color__']; + return; + } + } + }); + return exemplarColor; +}; + export const ExemplarsPlugin: React.FC = ({ exemplars, timeZone, @@ -114,6 +141,9 @@ export const ExemplarsPlugin: React.FC = ({ const renderMarker = useCallback( (dataFrame: DataFrame, dataFrameFieldIndex: DataFrameFieldIndex) => { let showMarker = false; + const labelsAndUniqueValuesFromActiveFilters = getUniqueValuesFromLabels(visibleLabels.labels); + + const markerColor = getExemplarColor(dataFrame, dataFrameFieldIndex, visibleLabels); // If all series are visible, don't filter any exemplars if (visibleLabels.labels.length === visibleLabels.totalSeriesCount) { @@ -121,7 +151,6 @@ export const ExemplarsPlugin: React.FC = ({ } else { visibleLabels.labels.forEach((visibleLabel) => { const labelKeys = Object.keys(visibleLabel); - const labelsAndUniqueValuesFromActiveFilters = getUniqueValuesFromLabels(visibleLabels.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 (Object.keys(visibleLabel).length === 0) { @@ -156,6 +185,7 @@ export const ExemplarsPlugin: React.FC = ({ dataFrame={dataFrame} dataFrameFieldIndex={dataFrameFieldIndex} config={config} + exemplarColor={markerColor} /> ); }, From 165b956c27fb724939599af7a4ab46715b2a2d39 Mon Sep 17 00:00:00 2001 From: Galen Date: Tue, 6 Dec 2022 11:33:38 -0600 Subject: [PATCH 09/27] refactor --- .../timeseries/plugins/ExemplarsPlugin.tsx | 127 +++++++++++------- 1 file changed, 75 insertions(+), 52 deletions(-) diff --git a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx index 2fe4f8b0c627..21e65ca0d2be 100644 --- a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx +++ b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx @@ -23,30 +23,6 @@ interface ExemplarsPluginProps { visibleLabels: { labels: Labels[]; totalSeriesCount: number }; } -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 }; -}; - export const ExemplarsPlugin: React.FC = ({ exemplars, timeZone, @@ -116,34 +92,13 @@ export const ExemplarsPlugin: React.FC = ({ let showMarker = false; // If all series are visible, don't filter any exemplars - if (visibleLabels.labels.length === visibleLabels.totalSeriesCount) { - showMarker = true; - } else { - visibleLabels.labels.forEach((visibleLabel) => { - const labelKeys = Object.keys(visibleLabel); - const labelsAndUniqueValuesFromActiveFilters = getUniqueValuesFromLabels(visibleLabels.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 (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 = fields.every((field) => { - const value = field.values.get(dataFrameFieldIndex.fieldIndex); - const allValues = labelsAndUniqueValuesFromActiveFilters[field.name]; - - return [...allValues].includes(value); - }); - } - } - }); - } + showMarker = showExemplarMarker( + visibleLabels, + showMarker, + getUniqueValuesFromLabels, + dataFrame, + dataFrameFieldIndex + ); if (!showMarker) { return <>; @@ -172,3 +127,71 @@ export const ExemplarsPlugin: React.FC = ({ /> ); }; + +/** + * 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 }, + showMarker: boolean, + getUniqueValuesFromLabels: (labels: Labels[]) => { [p: string]: Set }, + dataFrame: DataFrame, + dataFrameFieldIndex: DataFrameFieldIndex +) => { + if (visibleLabels.labels.length === visibleLabels.totalSeriesCount) { + showMarker = true; + } else { + visibleLabels.labels.forEach((visibleLabel) => { + const labelKeys = Object.keys(visibleLabel); + const labelsAndUniqueValuesFromActiveFilters = getUniqueValuesFromLabels(visibleLabels.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 (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 = fields.every((field) => { + const value = field.values.get(dataFrameFieldIndex.fieldIndex); + const allValues = labelsAndUniqueValuesFromActiveFilters[field.name]; + + return [...allValues].includes(value); + }); + } + } + }); + } + return showMarker; +}; From ca7102fcc894177395723a8f0856784dbe23de2a Mon Sep 17 00:00:00 2001 From: Galen Date: Tue, 6 Dec 2022 11:59:44 -0600 Subject: [PATCH 10/27] clean up comments after merge --- .../app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx index ae8ca7cd54bd..d0d19d4cdaf5 100644 --- a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx +++ b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx @@ -125,7 +125,7 @@ export const ExemplarsPlugin: React.FC = ({ }; /** - * + * Function to get labels that are currently displayed in the legend */ export const getVisibleLabels = ( config: UPlotConfigBuilder, @@ -181,7 +181,7 @@ const getExemplarColor = ( }; /** - * + * Determine if the current exemplar marker is filtered by what series are selected in the legend UI */ function showExemplar( getUniqueValuesFromLabels: (labels: Labels[]) => { [p: string]: Set }, From c52333392be33ff8f255e56787cf4efb479fe799 Mon Sep 17 00:00:00 2001 From: Galen Date: Tue, 6 Dec 2022 14:45:59 -0600 Subject: [PATCH 11/27] refactor and clean up --- .../panel/candlestick/CandlestickPanel.tsx | 2 +- .../panel/timeseries/TimeSeriesPanel.tsx | 2 +- .../timeseries/plugins/ExemplarsPlugin.tsx | 93 ++++++++++--------- 3 files changed, 52 insertions(+), 45 deletions(-) diff --git a/public/app/plugins/panel/candlestick/CandlestickPanel.tsx b/public/app/plugins/panel/candlestick/CandlestickPanel.tsx index b2ebc43d5e9d..a770dc5fedb3 100644 --- a/public/app/plugins/panel/candlestick/CandlestickPanel.tsx +++ b/public/app/plugins/panel/candlestick/CandlestickPanel.tsx @@ -313,7 +313,7 @@ export const CandlestickPanel: React.FC = ({ )} {data.annotations && ( = ({ )} {data.annotations && ( Array>; - visibleLabels: { labels: Labels[]; totalSeriesCount: number }; + visibleSeries: VisibleExemplarLabels; } export const ExemplarsPlugin: React.FC = ({ @@ -28,7 +28,7 @@ export const ExemplarsPlugin: React.FC = ({ timeZone, getFieldLinks, config, - visibleLabels, + visibleSeries, }) => { const plotInstance = useRef(); @@ -69,31 +69,13 @@ export const ExemplarsPlugin: React.FC = ({ }; }, []); - // Merge values from objects with same props - const getUniqueValuesFromLabels = (labels: Labels[]) => { - const labelToSet: { [index: string]: Set } = {}; - - Object.values(labels).forEach((labelObject) => { - return Object.keys(labelObject).forEach((labelName) => { - const labelValue: string = labelObject[labelName]; - if (typeof labelToSet[labelName] === 'undefined') { - labelToSet[labelName] = new Set().add(labelValue); - } else { - labelToSet[labelName].add(labelValue); - } - }); - }); - - return labelToSet; - }; - const renderMarker = useCallback( (dataFrame: DataFrame, dataFrameFieldIndex: DataFrameFieldIndex) => { let showMarker = false; - const markerColor = getExemplarColor(dataFrame, dataFrameFieldIndex, visibleLabels); + const markerColor = getExemplarColor(dataFrame, dataFrameFieldIndex, visibleSeries); - showMarker = showExemplar(getUniqueValuesFromLabels, visibleLabels, showMarker, dataFrame, dataFrameFieldIndex); + showMarker = showExemplar(visibleSeries, showMarker, dataFrame, dataFrameFieldIndex); if (!showMarker) { return <>; @@ -110,7 +92,7 @@ export const ExemplarsPlugin: React.FC = ({ /> ); }, - [config, timeZone, getFieldLinks, visibleLabels] + [config, timeZone, getFieldLinks, visibleSeries] ); return ( @@ -124,15 +106,13 @@ export const ExemplarsPlugin: React.FC = ({ ); }; +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 = ( - config: UPlotConfigBuilder, - frames: DataFrame[] | null -): { labels: Labels[]; totalSeriesCount: number } => { +export const getVisibleLabels = (config: UPlotConfigBuilder, frames: DataFrame[] | null): VisibleExemplarLabels => { const visibleSeries = config.series.filter((series) => series.props.show); - const visibleLabels: Labels[] = []; + const visibleLabels: LabelWithExemplarUIData[] = []; if (frames?.length) { visibleSeries.forEach((plotInstance) => { const frameIndex = plotInstance.props?.dataFrameFieldIndex?.frameIndex; @@ -142,7 +122,10 @@ export const getVisibleLabels = ( 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, __color__: plotInstance.props?.lineColor ?? '' }); + visibleLabels.push({ + labels: field.labels, + color: plotInstance.props?.lineColor ?? '', + }); } } }); @@ -151,28 +134,50 @@ export const getVisibleLabels = ( return { labels: visibleLabels, totalSeriesCount: config.series.length }; }; +// Merge values from objects with same props +const getUniqueValuesFromLabels = (labels: LabelWithExemplarUIData[]) => { + const labelToSet: { [index: string]: Set } = {}; + + Object.values(labels).forEach((labelObject) => { + return Object.keys(labelObject.labels).forEach((labelName) => { + const labelValue: string = labelObject.labels[labelName]; + if (typeof labelToSet[labelName] === 'undefined') { + labelToSet[labelName] = new Set().add(labelValue); + } else { + labelToSet[labelName].add(labelValue); + } + }); + }); + + return labelToSet; +}; + +interface LabelWithExemplarUIData { + labels: Labels; + color?: string; +} /** * Get color of active series in legend */ const getExemplarColor = ( dataFrame: DataFrame, dataFrameFieldIndex: DataFrameFieldIndex, - visibleLabels: { labels: Labels[]; totalSeriesCount: number } + visibleLabels: VisibleExemplarLabels ) => { let exemplarColor; visibleLabels.labels.forEach((visibleLabel) => { - const labelKeys = Object.keys(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) => { + const hasMatch = fields.every((field, index, fields) => { const value = field.values.get(dataFrameFieldIndex.fieldIndex); - return visibleLabel[field.name] === value; + return visibleLabel.labels[field.name] === value; }); if (hasMatch) { - exemplarColor = visibleLabel['__color__']; + exemplarColor = visibleLabel.color; return; } } @@ -184,22 +189,24 @@ const getExemplarColor = ( * Determine if the current exemplar marker is filtered by what series are selected in the legend UI */ function showExemplar( - getUniqueValuesFromLabels: (labels: Labels[]) => { [p: string]: Set }, - visibleLabels: { labels: Labels[]; totalSeriesCount: number }, + visibleSeries: VisibleExemplarLabels, showMarker: boolean, dataFrame: DataFrame, dataFrameFieldIndex: DataFrameFieldIndex ) { - const labelsAndUniqueValuesFromActiveFilters = getUniqueValuesFromLabels(visibleLabels.labels); + const labelsAndUniqueValuesFromActiveFilters = getUniqueValuesFromLabels(visibleSeries.labels); // If all series are visible, don't filter any exemplars - if (visibleLabels.labels.length === visibleLabels.totalSeriesCount) { + + if (visibleSeries.labels.length === visibleSeries.totalSeriesCount) { showMarker = true; } else { - visibleLabels.labels.forEach((visibleLabel) => { - const labelKeys = Object.keys(visibleLabel); + // Iterate through all of the visible series + visibleSeries.labels.forEach((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 (Object.keys(visibleLabel).length === 0) { + if (Object.keys(visibleLabel.labels).length === 0) { showMarker = true; } else { // If there are labels, lets only show the exemplars with labels associated with series that are currently visible @@ -207,8 +214,8 @@ function showExemplar( return labelKeys.find((labelKey) => labelKey === field.name); }); - // Check to see if at least one value matches each field if (fields.length) { + // Check to see if at least one value matches each field showMarker = fields.every((field) => { const value = field.values.get(dataFrameFieldIndex.fieldIndex); const allValues = labelsAndUniqueValuesFromActiveFilters[field.name]; From 57321ee962e333851ee158cd7927d6b219a9749e Mon Sep 17 00:00:00 2001 From: Galen Date: Tue, 6 Dec 2022 16:03:23 -0600 Subject: [PATCH 12/27] fix bug in exemplar filtering letting through exemplars that matched any label name, label value combination --- .../panel/timeseries/plugins/ExemplarsPlugin.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx index 21e65ca0d2be..9840f86749d5 100644 --- a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx +++ b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx @@ -170,8 +170,6 @@ const showExemplarMarker = ( } else { visibleLabels.labels.forEach((visibleLabel) => { const labelKeys = Object.keys(visibleLabel); - const labelsAndUniqueValuesFromActiveFilters = getUniqueValuesFromLabels(visibleLabels.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 (Object.keys(visibleLabel).length === 0) { showMarker = true; @@ -183,11 +181,11 @@ const showExemplarMarker = ( // Check to see if at least one value matches each field if (fields.length) { - showMarker = fields.every((field) => { - const value = field.values.get(dataFrameFieldIndex.fieldIndex); - const allValues = labelsAndUniqueValuesFromActiveFilters[field.name]; - - return [...allValues].includes(value); + 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); + }); }); } } From 2c59d856b214f973ea772dbe4c80399c82ba74c1 Mon Sep 17 00:00:00 2001 From: Galen Date: Tue, 6 Dec 2022 16:11:14 -0600 Subject: [PATCH 13/27] fix bugs introduced by merge conflicts --- public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx index bd670d91f846..4343e9320bef 100644 --- a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx +++ b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx @@ -200,7 +200,7 @@ function showExemplar( } else { visibleSeries.labels.forEach((visibleLabel) => { // Get the label names - const labelKeys = Object.keys(visibleLabel); + 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 (Object.keys(visibleLabel.labels).length === 0) { From 9da3f0009f7922717b621995385827cc0e40c7cb Mon Sep 17 00:00:00 2001 From: Galen Date: Wed, 7 Dec 2022 07:35:52 -0600 Subject: [PATCH 14/27] remove unused function --- .../timeseries/plugins/ExemplarsPlugin.tsx | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx index 4343e9320bef..3573a9264b99 100644 --- a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx +++ b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx @@ -134,24 +134,6 @@ export const getVisibleLabels = (config: UPlotConfigBuilder, frames: DataFrame[] return { labels: visibleLabels, totalSeriesCount: config.series.length }; }; -// Merge values from objects with same props -const getUniqueValuesFromLabels = (labels: LabelWithExemplarUIData[]) => { - const labelToSet: { [index: string]: Set } = {}; - - Object.values(labels).forEach((labelObject) => { - return Object.keys(labelObject.labels).forEach((labelName) => { - const labelValue: string = labelObject.labels[labelName]; - if (typeof labelToSet[labelName] === 'undefined') { - labelToSet[labelName] = new Set().add(labelValue); - } else { - labelToSet[labelName].add(labelValue); - } - }); - }); - - return labelToSet; -}; - interface LabelWithExemplarUIData { labels: Labels; color?: string; From e5dfa27c6540666dc41d934e5b48054820cee8d8 Mon Sep 17 00:00:00 2001 From: Galen Date: Wed, 7 Dec 2022 08:25:58 -0600 Subject: [PATCH 15/27] update changelog --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f819093c881..6f22fc607282 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ + + +# 9.3.2 (2022-12-13) + +### Bug fixes + +- **Prometheus:** Enable exemplars to be filtered when series visibility is toggled in the time series legend UI. [#59678](https://github.com/grafana/grafana/issues/59678), [@gtk-grafana](https://github.com/gtk-grafana) + + # 9.3.0 (2022-11-30) From a54be2cc581b804f6b3b4c09fae51c638d546a0b Mon Sep 17 00:00:00 2001 From: Galen Kistler <109082771+gtk-grafana@users.noreply.github.com> Date: Wed, 7 Dec 2022 11:58:51 -0600 Subject: [PATCH 16/27] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fa112896adc..22177a34c140 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Bug fixes -- **Prometheus:** Enable exemplars to be filtered when series visibility is toggled in the time series legend UI. [#59678](https://github.com/grafana/grafana/issues/59678), [@gtk-grafana](https://github.com/gtk-grafana) +- **Prometheus:** Fix exemplars not respecting corresponding series display status. [#59678](https://github.com/grafana/grafana/issues/59678), [@gtk-grafana](https://github.com/gtk-grafana) From 08b557018469a8013f0c7cffc917472b93482a74 Mon Sep 17 00:00:00 2001 From: Galen Date: Wed, 7 Dec 2022 12:41:15 -0600 Subject: [PATCH 17/27] make property optional, set default behavior --- .../panel/candlestick/CandlestickPanel.tsx | 9 ++++----- .../timeseries/plugins/ExemplarsPlugin.tsx | 17 ++++++----------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/public/app/plugins/panel/candlestick/CandlestickPanel.tsx b/public/app/plugins/panel/candlestick/CandlestickPanel.tsx index b2ebc43d5e9d..5a9d216cb7d1 100644 --- a/public/app/plugins/panel/candlestick/CandlestickPanel.tsx +++ b/public/app/plugins/panel/candlestick/CandlestickPanel.tsx @@ -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'; @@ -16,12 +16,12 @@ import { getFieldLinksForExplore } from 'app/features/explore/utils/links'; import { AnnotationEditorPlugin } from '../timeseries/plugins/AnnotationEditorPlugin'; import { AnnotationsPlugin } from '../timeseries/plugins/AnnotationsPlugin'; import { ContextMenuPlugin } from '../timeseries/plugins/ContextMenuPlugin'; -import { ExemplarsPlugin, getVisibleLabels } from '../timeseries/plugins/ExemplarsPlugin'; +import { ExemplarsPlugin } from '../timeseries/plugins/ExemplarsPlugin'; 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 {} @@ -313,7 +313,6 @@ export const CandlestickPanel: React.FC = ({ )} {data.annotations && ( Array>; - visibleLabels: { labels: Labels[]; totalSeriesCount: number }; + visibleLabels?: { labels: Labels[]; totalSeriesCount: number }; } export const ExemplarsPlugin: React.FC = ({ @@ -89,17 +89,12 @@ export const ExemplarsPlugin: React.FC = ({ const renderMarker = useCallback( (dataFrame: DataFrame, dataFrameFieldIndex: DataFrameFieldIndex) => { - let showMarker = false; + let showMarker = + visibleLabels !== undefined + ? showExemplarMarker(visibleLabels, getUniqueValuesFromLabels, dataFrame, dataFrameFieldIndex) + : true; // If all series are visible, don't filter any exemplars - showMarker = showExemplarMarker( - visibleLabels, - showMarker, - getUniqueValuesFromLabels, - dataFrame, - dataFrameFieldIndex - ); - if (!showMarker) { return <>; } @@ -160,11 +155,11 @@ export const getVisibleLabels = ( */ const showExemplarMarker = ( visibleLabels: { labels: Labels[]; totalSeriesCount: number }, - showMarker: boolean, getUniqueValuesFromLabels: (labels: Labels[]) => { [p: string]: Set }, dataFrame: DataFrame, dataFrameFieldIndex: DataFrameFieldIndex ) => { + let showMarker = false; if (visibleLabels.labels.length === visibleLabels.totalSeriesCount) { showMarker = true; } else { From cac009bf7d679404a99b433935a379d956755a42 Mon Sep 17 00:00:00 2001 From: Galen Date: Wed, 7 Dec 2022 12:44:07 -0600 Subject: [PATCH 18/27] update comment --- public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx index d497207b6d14..501fef6b9193 100644 --- a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx +++ b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx @@ -89,12 +89,12 @@ export const ExemplarsPlugin: React.FC = ({ 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, getUniqueValuesFromLabels, dataFrame, dataFrameFieldIndex) : true; - // If all series are visible, don't filter any exemplars if (!showMarker) { return <>; } From df71569e6332f8392d2f8ce78345ea7d53806629 Mon Sep 17 00:00:00 2001 From: Galen Date: Wed, 7 Dec 2022 12:50:55 -0600 Subject: [PATCH 19/27] remove uncalled function --- .../timeseries/plugins/ExemplarsPlugin.tsx | 23 +------------------ 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx index 501fef6b9193..1952863eb3c2 100644 --- a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx +++ b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx @@ -69,31 +69,11 @@ export const ExemplarsPlugin: React.FC = ({ }; }, []); - // Merge values from objects with same props - const getUniqueValuesFromLabels = (labels: Labels[]) => { - const labelToSet: { [index: string]: Set } = {}; - - Object.values(labels).forEach((labelObject) => { - return Object.keys(labelObject).forEach((labelName) => { - const labelValue: string = labelObject[labelName]; - if (typeof labelToSet[labelName] === 'undefined') { - labelToSet[labelName] = new Set().add(labelValue); - } else { - labelToSet[labelName].add(labelValue); - } - }); - }); - - return labelToSet; - }; - 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, getUniqueValuesFromLabels, dataFrame, dataFrameFieldIndex) - : true; + visibleLabels !== undefined ? showExemplarMarker(visibleLabels, dataFrame, dataFrameFieldIndex) : true; if (!showMarker) { return <>; @@ -155,7 +135,6 @@ export const getVisibleLabels = ( */ const showExemplarMarker = ( visibleLabels: { labels: Labels[]; totalSeriesCount: number }, - getUniqueValuesFromLabels: (labels: Labels[]) => { [p: string]: Set }, dataFrame: DataFrame, dataFrameFieldIndex: DataFrameFieldIndex ) => { From 099d2bf1e6314857624c3a212022333b79040928 Mon Sep 17 00:00:00 2001 From: Galen Date: Wed, 7 Dec 2022 13:53:59 -0600 Subject: [PATCH 20/27] add unit test for getVisibleLabels --- .../plugins/ExemplarsPlugin.test.tsx | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.test.tsx diff --git a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.test.tsx b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.test.tsx new file mode 100644 index 000000000000..e580258b7218 --- /dev/null +++ b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.test.tsx @@ -0,0 +1,95 @@ +import { Labels, MutableDataFrame } from '@grafana/data/src'; +import { UPlotConfigBuilder } from '@grafana/ui/src'; + +import { getVisibleLabels } from './ExemplarsPlugin'; + +describe('getVisibleLabels()', () => { + 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' }], + }; + + const config: UPlotConfigBuilder = { + 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; + 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], + }, + ], + 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], + }, + ], + 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], + }, + ], + length: 4, + }); + + // Base case + expect(getVisibleLabels(config, [])).toEqual({ totalSeriesCount: 3, labels: [] }); + + expect(getVisibleLabels(config, [dataFrameSeries1, dataFrameSeries2, dataFrameSeries3])).toEqual(expected); + }); +}); From a32b1735e6919a6bd64bba247bd6401e965ec860 Mon Sep 17 00:00:00 2001 From: Galen Date: Wed, 7 Dec 2022 14:15:34 -0600 Subject: [PATCH 21/27] clean up test --- .../plugins/ExemplarsPlugin.test.tsx | 156 +++++++++--------- .../timeseries/plugins/ExemplarsPlugin.tsx | 1 + 2 files changed, 80 insertions(+), 77 deletions(-) diff --git a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.test.tsx b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.test.tsx index e580258b7218..92c1b3ce1397 100644 --- a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.test.tsx +++ b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.test.tsx @@ -4,92 +4,94 @@ import { UPlotConfigBuilder } from '@grafana/ui/src'; import { getVisibleLabels } from './ExemplarsPlugin'; describe('getVisibleLabels()', () => { - 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' }], - }; - - const config: UPlotConfigBuilder = { - 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; - const dataFrameSeries1 = new MutableDataFrame({ - name: 'tns/app', - fields: [ - { - name: 'Time', - values: [1670418750000, 1670418765000, 1670418780000, 1670418795000], - entities: {}, + const dataFrameSeries1 = new MutableDataFrame({ + name: 'tns/app', + fields: [ + { + name: 'Time', + values: [1670418750000, 1670418765000, 1670418780000, 1670418795000], + entities: {}, + }, + { + name: 'Value', + labels: { + job: 'tns/app', }, - { - name: 'Value', - labels: { - job: 'tns/app', - }, - values: [0.018963114754098367, 0.019140624999999974, 0.019718309859154928, 0.020064189189189167], + values: [0.018963114754098367, 0.019140624999999974, 0.019718309859154928, 0.020064189189189167], + }, + ], + length: 4, + }); + const dataFrameSeries2 = new MutableDataFrame({ + name: 'tns/db', + fields: [ + { + name: 'Time', + values: [1670418750000, 1670418765000, 1670418780000, 1670418795000], + entities: {}, + }, + { + name: 'Value', + labels: { + job: 'tns/db', }, - ], - length: 4, - }); - const dataFrameSeries2 = new MutableDataFrame({ - name: 'tns/db', - fields: [ - { - name: 'Time', - values: [1670418750000, 1670418765000, 1670418780000, 1670418795000], - entities: {}, + values: [0.028963114754098367, 0.029140624999999974, 0.029718309859154928, 0.030064189189189167], + }, + ], + length: 4, + }); + const dataFrameSeries3 = new MutableDataFrame({ + name: 'tns/loadgen', + fields: [ + { + name: 'Time', + values: [1670418750000, 1670418765000, 1670418780000, 1670418795000], + entities: {}, + }, + { + name: 'Value', + labels: { + job: 'tns/loadgen', }, - { - name: 'Value', - labels: { - job: 'tns/db', - }, - values: [0.028963114754098367, 0.029140624999999974, 0.029718309859154928, 0.030064189189189167], + values: [0.028963114754098367, 0.029140624999999974, 0.029718309859154928, 0.030064189189189167], + }, + ], + length: 4, + }); + const frames = [dataFrameSeries1, dataFrameSeries2, dataFrameSeries3]; + const config: UPlotConfigBuilder = { + addHook: (type, hook) => {}, + series: [ + { + props: { + dataFrameFieldIndex: { frameIndex: 0, fieldIndex: 1 }, + show: true, }, - ], - length: 4, - }); - const dataFrameSeries3 = new MutableDataFrame({ - name: 'tns/loadgen', - fields: [ - { - name: 'Time', - values: [1670418750000, 1670418765000, 1670418780000, 1670418795000], - entities: {}, + }, + { + props: { + dataFrameFieldIndex: { frameIndex: 1, fieldIndex: 1 }, + show: true, }, - { - name: 'Value', - labels: { - job: 'tns/loadgen', - }, - values: [0.028963114754098367, 0.029140624999999974, 0.029718309859154928, 0.030064189189189167], + }, + { + props: { + dataFrameFieldIndex: { frameIndex: 2, fieldIndex: 1 }, + show: false, }, - ], - length: 4, - }); + }, + ], + } 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, [dataFrameSeries1, dataFrameSeries2, dataFrameSeries3])).toEqual(expected); + expect(getVisibleLabels(config, frames)).toEqual(expected); }); }); diff --git a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx index 1952863eb3c2..99ea4fcc6315 100644 --- a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx +++ b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx @@ -30,6 +30,7 @@ export const ExemplarsPlugin: React.FC = ({ config, visibleLabels, }) => { + console.log('exemplars', exemplars); const plotInstance = useRef(); useLayoutEffect(() => { From d00be4c6bcd6c0193f042d8c44601ab49eb66045 Mon Sep 17 00:00:00 2001 From: Galen Date: Wed, 7 Dec 2022 14:20:02 -0600 Subject: [PATCH 22/27] remove console.log --- public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx index 99ea4fcc6315..1952863eb3c2 100644 --- a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx +++ b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx @@ -30,7 +30,6 @@ export const ExemplarsPlugin: React.FC = ({ config, visibleLabels, }) => { - console.log('exemplars', exemplars); const plotInstance = useRef(); useLayoutEffect(() => { From 7a716ea2d09a7ba11de59661c986a09d68188379 Mon Sep 17 00:00:00 2001 From: Galen Date: Wed, 7 Dec 2022 14:52:16 -0600 Subject: [PATCH 23/27] fix unit test --- .../plugins/ExemplarsPlugin.test.tsx | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.test.tsx b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.test.tsx index 92c1b3ce1397..e0ba99f59829 100644 --- a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.test.tsx +++ b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.test.tsx @@ -1,7 +1,7 @@ -import { Labels, MutableDataFrame } from '@grafana/data/src'; +import { Field, MutableDataFrame } from '@grafana/data/src'; import { UPlotConfigBuilder } from '@grafana/ui/src'; -import { getVisibleLabels } from './ExemplarsPlugin'; +import { getVisibleLabels, VisibleExemplarLabels } from './ExemplarsPlugin'; describe('getVisibleLabels()', () => { const dataFrameSeries1 = new MutableDataFrame({ @@ -19,7 +19,7 @@ describe('getVisibleLabels()', () => { }, values: [0.018963114754098367, 0.019140624999999974, 0.019718309859154928, 0.020064189189189167], }, - ], + ] as unknown as Field[], length: 4, }); const dataFrameSeries2 = new MutableDataFrame({ @@ -37,7 +37,7 @@ describe('getVisibleLabels()', () => { }, values: [0.028963114754098367, 0.029140624999999974, 0.029718309859154928, 0.030064189189189167], }, - ], + ] as unknown as Field[], length: 4, }); const dataFrameSeries3 = new MutableDataFrame({ @@ -55,7 +55,7 @@ describe('getVisibleLabels()', () => { }, values: [0.028963114754098367, 0.029140624999999974, 0.029718309859154928, 0.030064189189189167], }, - ], + ] as unknown as Field[], length: 4, }); const frames = [dataFrameSeries1, dataFrameSeries2, dataFrameSeries3]; @@ -84,9 +84,22 @@ describe('getVisibleLabels()', () => { } as UPlotConfigBuilder; it('function should only return labels associated with actively visible series', () => { - const expected: { labels: Labels[]; totalSeriesCount: number } = { + const expected: VisibleExemplarLabels = { totalSeriesCount: 3, - labels: [{ job: 'tns/app' }, { job: 'tns/db' }], + labels: [ + { + color: '', + labels: { + job: 'tns/app', + }, + }, + { + color: '', + labels: { + job: 'tns/db', + }, + }, + ], }; // Base case From 39f7425777f3de2f87d2a58d0706342acffb89cc Mon Sep 17 00:00:00 2001 From: Galen Date: Wed, 7 Dec 2022 14:53:42 -0600 Subject: [PATCH 24/27] update unit test --- .../panel/timeseries/plugins/ExemplarsPlugin.test.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.test.tsx b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.test.tsx index 92c1b3ce1397..6e24c04d8fe0 100644 --- a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.test.tsx +++ b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.test.tsx @@ -1,4 +1,4 @@ -import { Labels, MutableDataFrame } from '@grafana/data/src'; +import { Field, Labels, MutableDataFrame } from '@grafana/data/src'; import { UPlotConfigBuilder } from '@grafana/ui/src'; import { getVisibleLabels } from './ExemplarsPlugin'; @@ -19,7 +19,7 @@ describe('getVisibleLabels()', () => { }, values: [0.018963114754098367, 0.019140624999999974, 0.019718309859154928, 0.020064189189189167], }, - ], + ] as unknown as Field[], length: 4, }); const dataFrameSeries2 = new MutableDataFrame({ @@ -37,7 +37,7 @@ describe('getVisibleLabels()', () => { }, values: [0.028963114754098367, 0.029140624999999974, 0.029718309859154928, 0.030064189189189167], }, - ], + ] as unknown as Field[], length: 4, }); const dataFrameSeries3 = new MutableDataFrame({ @@ -55,7 +55,7 @@ describe('getVisibleLabels()', () => { }, values: [0.028963114754098367, 0.029140624999999974, 0.029718309859154928, 0.030064189189189167], }, - ], + ] as unknown as Field[], length: 4, }); const frames = [dataFrameSeries1, dataFrameSeries2, dataFrameSeries3]; From d1416df0a86b22e6716330e39783d605c4b716b4 Mon Sep 17 00:00:00 2001 From: Galen Date: Wed, 7 Dec 2022 15:31:33 -0600 Subject: [PATCH 25/27] changelog update --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22177a34c140..cd45c4f5dae1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Bug fixes - **Prometheus:** Fix exemplars not respecting corresponding series display status. [#59678](https://github.com/grafana/grafana/issues/59678), [@gtk-grafana](https://github.com/gtk-grafana) +- **Prometheus:** Match exemplar fill color with series color in time series. [#59908](https://github.com/grafana/grafana/issues/59499), [@gtk-grafana](https://github.com/gtk-grafana) From e88971c24fe14441aa805c0945d04d32af156800 Mon Sep 17 00:00:00 2001 From: Galen Date: Thu, 8 Dec 2022 10:56:35 -0600 Subject: [PATCH 26/27] revert manual changelog --- CHANGELOG.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22177a34c140..24b2b1f87853 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,3 @@ - - -# 9.3.2 (2022-12-13) - -### Bug fixes - -- **Prometheus:** Fix exemplars not respecting corresponding series display status. [#59678](https://github.com/grafana/grafana/issues/59678), [@gtk-grafana](https://github.com/gtk-grafana) - - # 9.3.1 (2022-11-30) From dd6fc277ae04d54e97924e02e14f137486a69f4f Mon Sep 17 00:00:00 2001 From: Galen Date: Thu, 8 Dec 2022 13:24:00 -0600 Subject: [PATCH 27/27] rewrite inefficient forEach --- .../app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx index d55871674b0f..91d3647cae8b 100644 --- a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx +++ b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx @@ -147,7 +147,7 @@ const getExemplarColor = ( visibleLabels: VisibleExemplarLabels ) => { let exemplarColor; - visibleLabels.labels.forEach((visibleLabel) => { + visibleLabels.labels.some((visibleLabel) => { const labelKeys = Object.keys(visibleLabel.labels); const fields = dataFrame.fields.filter((field) => { return labelKeys.find((labelKey) => labelKey === field.name); @@ -160,9 +160,10 @@ const getExemplarColor = ( if (hasMatch) { exemplarColor = visibleLabel.color; - return; + return true; } } + return false; }); return exemplarColor; };