DashboardDS: Fix datasource annotations not hiding on toggle hide (#115024)

* fix dashboard datasource annotations not hiding on toggle hide

* cleanup
This commit is contained in:
Victor Marin
2025-12-11 09:03:38 +02:00
committed by GitHub
parent 5ca221743f
commit cedf08c9ce
2 changed files with 146 additions and 28 deletions
@@ -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({
@@ -122,8 +122,9 @@ export class DashboardDatasource extends DataSourceApi<DashboardQuery> {
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<DashboardQuery> {
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));
}
/**