From cedf08c9ce95883d7e0f84bd0a6c34147ee4f656 Mon Sep 17 00:00:00 2001 From: Victor Marin Date: Thu, 11 Dec 2025 09:03:38 +0200 Subject: [PATCH] DashboardDS: Fix datasource annotations not hiding on toggle hide (#115024) * fix dashboard datasource annotations not hiding on toggle hide * cleanup --- .../datasource/dashboard/datasource.test.ts | 117 ++++++++++++++++++ .../datasource/dashboard/datasource.ts | 57 ++++----- 2 files changed, 146 insertions(+), 28 deletions(-) diff --git a/public/app/plugins/datasource/dashboard/datasource.test.ts b/public/app/plugins/datasource/dashboard/datasource.test.ts index 2dcb17c63db..0491dfcd798 100644 --- a/public/app/plugins/datasource/dashboard/datasource.test.ts +++ b/public/app/plugins/datasource/dashboard/datasource.test.ts @@ -10,6 +10,7 @@ import { FieldType, DataFrame, AdHocVariableFilter, + DataTopic, } from '@grafana/data'; import { getPanelPlugin } from '@grafana/data/test'; import { setPluginImportUtils } from '@grafana/runtime'; @@ -774,8 +775,124 @@ describe('DashboardDatasource', () => { }); }); }); + + describe('Annotation Handling', () => { + it('should NOT include annotations from source panel in regular query response', async () => { + const { observable } = setupWithAnnotations({ refId: 'A', panelId: 1 }); + + let rsp: DataQueryResponse | undefined; + observable.subscribe({ next: (data) => (rsp = data) }); + + // Should only have series data, no annotations + expect(rsp?.data.length).toBe(1); + expect(rsp?.data[0].fields[0].values).toEqual([1, 2, 3]); + + // Verify no annotation frames are included + const annotationFrames = rsp?.data.filter((frame) => frame.meta?.dataTopic === DataTopic.Annotations); + expect(annotationFrames?.length).toBe(0); + }); + + it('should return annotations as series when query topic is DataTopic.Annotations', async () => { + const { observable } = setupWithAnnotations({ refId: 'A', panelId: 1, topic: DataTopic.Annotations }); + + let rsp: DataQueryResponse | undefined; + observable.subscribe({ next: (data) => (rsp = data) }); + + // Should return annotation data as series (with dataTopic changed to Series) + expect(rsp?.data.length).toBe(1); + expect(rsp?.data[0].name).toBe('Test Annotation'); + // The dataTopic should be changed to Series when querying for annotations + expect(rsp?.data[0].meta?.dataTopic).toBe(DataTopic.Series); + }); + + it('should not leak annotations when source panel has annotations and toggle is off', async () => { + // This test ensures that when annotations are toggled off at the dashboard level, + // DashboardDS panels don't continue showing them from the source panel's cached data + const { observable } = setupWithAnnotations({ refId: 'A', panelId: 1 }); + + let rsp: DataQueryResponse | undefined; + observable.subscribe({ next: (data) => (rsp = data) }); + + // Verify that annotations from source panel are NOT included in response + // This is critical for annotation toggle to work correctly on DashboardDS panels + const hasAnnotations = rsp?.data.some( + (frame) => frame.meta?.dataTopic === DataTopic.Annotations || frame.name === 'Test Annotation' + ); + expect(hasAnnotations).toBe(false); + }); + + it('should only return series data even when source has both series and annotations', async () => { + const { observable } = setupWithAnnotations({ refId: 'A', panelId: 1 }); + + let rsp: DataQueryResponse | undefined; + observable.subscribe({ next: (data) => (rsp = data) }); + + // All returned frames should be series data, not annotations + rsp?.data.forEach((frame) => { + expect(frame.meta?.dataTopic).not.toBe(DataTopic.Annotations); + }); + + // Should have the series data from the source panel + expect(rsp?.data[0].fields[0].values).toEqual([1, 2, 3]); + }); + }); }); +function setupWithAnnotations(query: DashboardQuery, requestId?: string) { + const annotationFrame: DataFrame = { + name: 'Test Annotation', + fields: [ + { name: 'time', type: FieldType.time, values: [1000, 2000], config: {} }, + { name: 'text', type: FieldType.string, values: ['Annotation 1', 'Annotation 2'], config: {} }, + ], + length: 2, + meta: { + dataTopic: DataTopic.Annotations, + }, + }; + + const sourceData = new SceneDataTransformer({ + $data: new SceneDataNode({ + data: { + series: [arrayToDataFrame([1, 2, 3])], + annotations: [annotationFrame], + state: LoadingState.Done, + timeRange: getDefaultTimeRange(), + }, + }), + transformations: [], + }); + + const scene = new SceneFlexLayout({ + children: [ + new SceneFlexItem({ + body: new VizPanel({ + key: getVizPanelKeyForPanelId(1), + $data: sourceData, + }), + }), + ], + }); + + const ds = new DashboardDatasource({} as DataSourceInstanceSettings); + + const observable = ds.query({ + timezone: 'utc', + targets: [query], + requestId: requestId ?? '', + interval: '', + intervalMs: 0, + range: getDefaultTimeRange(), + scopedVars: { + __sceneObject: new SafeSerializableSceneObject(scene), + }, + app: '', + startTime: 0, + }); + + return { observable, sourceData }; +} + function setup(query: DashboardQuery, requestId?: string) { const sourceData = new SceneDataTransformer({ $data: new SceneDataNode({ diff --git a/public/app/plugins/datasource/dashboard/datasource.ts b/public/app/plugins/datasource/dashboard/datasource.ts index 34a1781623f..63fefd10bb1 100644 --- a/public/app/plugins/datasource/dashboard/datasource.ts +++ b/public/app/plugins/datasource/dashboard/datasource.ts @@ -122,8 +122,9 @@ export class DashboardDatasource extends DataSourceApi { query: DashboardQuery, filters: AdHocVariableFilter[] ): DataFrame[] { - const annotations = data.annotations ?? []; + // When querying for annotations topic, return the source panel's annotations as series data if (query.topic === DataTopic.Annotations) { + const annotations = data.annotations ?? []; return annotations.map((frame) => ({ ...frame, meta: { @@ -131,34 +132,34 @@ export class DashboardDatasource extends DataSourceApi { dataTopic: DataTopic.Series, }, })); - } else { - const series = data.series.map((s) => { - return { - ...s, - fields: s.fields.map((field: Field) => ({ - ...field, - config: { - ...field.config, - // Enable AdHoc filtering for string and numeric fields only when per-panel setting is enabled - filterable: query.adHocFiltersEnabled - ? field.type === FieldType.string || field.type === FieldType.number - : field.config.filterable, - }, - state: { - ...field.state, - }, - })), - }; - }); - - if (!query.adHocFiltersEnabled || filters.length === 0) { - return [...series, ...annotations]; - } - - // Apply AdHoc filters to series data - const filteredSeries = series.map((frame) => this.applyAdHocFilters(frame, filters)); - return [...filteredSeries, ...annotations]; } + + // For regular queries, only return series data + const series = data.series.map((s) => { + return { + ...s, + fields: s.fields.map((field: Field) => ({ + ...field, + config: { + ...field.config, + // Enable AdHoc filtering for string and numeric fields only when per-panel setting is enabled + filterable: query.adHocFiltersEnabled + ? field.type === FieldType.string || field.type === FieldType.number + : field.config.filterable, + }, + state: { + ...field.state, + }, + })), + }; + }); + + if (!query.adHocFiltersEnabled || filters.length === 0) { + return series; + } + + // Apply AdHoc filters to series data + return series.map((frame) => this.applyAdHocFilters(frame, filters)); } /**