Dashboards: Fix adhoc filter click when panel has no panel-level datasource (#115576)

* V2: Panel datasource is defined only for mixed ds

* if getDatasourceFromQueryRunner only returns ds.type, resolve to full ds ref throgh ds service

---------

Co-authored-by: Haris Rozajac <haris.rozajac12@gmail.com>
This commit is contained in:
Ivan Ortega Alba
2025-12-29 10:29:50 +01:00
committed by GitHub
co-authored by Haris Rozajac
parent 0b58cd3900
commit 30ad61e0e9
5 changed files with 95 additions and 12 deletions
@@ -1,10 +1,10 @@
import { AdHocVariableModel, EventBusSrv, GroupByVariableModel, VariableModel } from '@grafana/data';
import { BackendSrv, config, setBackendSrv } from '@grafana/runtime';
import { GroupByVariable, sceneGraph } from '@grafana/scenes';
import { GroupByVariable, sceneGraph, SceneQueryRunner } from '@grafana/scenes';
import { AdHocFilterItem, PanelContext } from '@grafana/ui';
import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene';
import { findVizPanelByKey } from '../utils/utils';
import { findVizPanelByKey, getQueryRunnerFor } from '../utils/utils';
import { getAdHocFilterVariableFor, setDashboardPanelContext } from './setDashboardPanelContext';
@@ -159,6 +159,23 @@ describe('setDashboardPanelContext', () => {
// Verify existing filter value updated
expect(variable.state.filters[1].operator).toBe('!=');
});
it('Should use existing adhoc filter when panel has no panel-level datasource because queries have all the same datasources (v2 behavior)', () => {
const { scene, context } = buildTestScene({ existingFilterVariable: true, panelDatasourceUndefined: true });
const variable = getAdHocFilterVariableFor(scene, { uid: 'my-ds-uid' });
variable.setState({ filters: [] });
context.onAddAdHocFilter!({ key: 'hello', value: 'world', operator: '=' });
// Should use the existing adhoc filter variable, not create a new one
expect(variable.state.filters).toEqual([{ key: 'hello', value: 'world', operator: '=' }]);
// Verify no new adhoc variables were created
const variables = sceneGraph.getVariables(scene);
const adhocVars = variables.state.variables.filter((v) => v.state.type === 'adhoc');
expect(adhocVars.length).toBe(1);
});
});
describe('getFiltersBasedOnGrouping', () => {
@@ -312,6 +329,7 @@ interface SceneOptions {
existingFilterVariable?: boolean;
existingGroupByVariable?: boolean;
groupByDatasourceUid?: string;
panelDatasourceUndefined?: boolean;
}
function buildTestScene(options: SceneOptions) {
@@ -385,6 +403,19 @@ function buildTestScene(options: SceneOptions) {
});
const vizPanel = findVizPanelByKey(scene, 'panel-4')!;
// Simulate v2 dashboard behavior where non-mixed panels don't have panel-level datasource
// but the queries have their own datasources
if (options.panelDatasourceUndefined) {
const queryRunner = getQueryRunnerFor(vizPanel);
if (queryRunner instanceof SceneQueryRunner) {
queryRunner.setState({
datasource: undefined,
queries: [{ refId: 'A', datasource: { uid: 'my-ds-uid', type: 'prometheus' } }],
});
}
}
const context: PanelContext = {
eventBus: new EventBusSrv(),
eventsScope: 'global',
@@ -6,7 +6,12 @@ import { AdHocFilterItem, PanelContext } from '@grafana/ui';
import { annotationServer } from 'app/features/annotations/api';
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
import { getDashboardSceneFor, getPanelIdForVizPanel, getQueryRunnerFor } from '../utils/utils';
import {
getDashboardSceneFor,
getDatasourceFromQueryRunner,
getPanelIdForVizPanel,
getQueryRunnerFor,
} from '../utils/utils';
import { DashboardScene } from './DashboardScene';
@@ -121,7 +126,7 @@ export function setDashboardPanelContext(vizPanel: VizPanel, context: PanelConte
context.eventBus.publish(new AnnotationChangeEvent({ id }));
};
context.onAddAdHocFilter = (newFilter: AdHocFilterItem) => {
context.onAddAdHocFilter = async (newFilter: AdHocFilterItem) => {
const dashboard = getDashboardSceneFor(vizPanel);
const queryRunner = getQueryRunnerFor(vizPanel);
@@ -129,7 +134,19 @@ export function setDashboardPanelContext(vizPanel: VizPanel, context: PanelConte
return;
}
const filterVar = getAdHocFilterVariableFor(dashboard, queryRunner.state.datasource);
let datasource = getDatasourceFromQueryRunner(queryRunner);
// If the datasource is type-only (e.g. it's possible that only group is set in V2 schema queries)
// we need to resolve it to a full datasource
if (datasource && !datasource.uid) {
const datasourceToLoad = await getDataSourceSrv().get(datasource);
datasource = {
uid: datasourceToLoad.uid,
type: datasourceToLoad.type,
};
}
const filterVar = getAdHocFilterVariableFor(dashboard, datasource);
updateAdHocFilterVariable(filterVar, newFilter);
};
@@ -141,7 +158,8 @@ export function setDashboardPanelContext(vizPanel: VizPanel, context: PanelConte
return [];
}
const groupByVar = getGroupByVariableFor(dashboard, queryRunner.state.datasource);
const datasource = getDatasourceFromQueryRunner(queryRunner);
const groupByVar = getGroupByVariableFor(dashboard, datasource);
if (!groupByVar) {
return [];
@@ -158,7 +176,7 @@ export function setDashboardPanelContext(vizPanel: VizPanel, context: PanelConte
.filter((item) => item !== undefined);
};
context.onAddAdHocFilters = (items: AdHocFilterItem[]) => {
context.onAddAdHocFilters = async (items: AdHocFilterItem[]) => {
const dashboard = getDashboardSceneFor(vizPanel);
const queryRunner = getQueryRunnerFor(vizPanel);
@@ -166,7 +184,18 @@ export function setDashboardPanelContext(vizPanel: VizPanel, context: PanelConte
return;
}
const filterVar = getAdHocFilterVariableFor(dashboard, queryRunner.state.datasource);
let datasource = getDatasourceFromQueryRunner(queryRunner);
// If the datasource is type-only (e.g. it's possible that only group is set in V2 schema queries)
// we need to resolve it to a full datasource
if (datasource && !datasource.uid) {
const datasourceToLoad = await getDataSourceSrv().get(datasource);
datasource = {
uid: datasourceToLoad.uid,
type: datasourceToLoad.type,
};
}
const filterVar = getAdHocFilterVariableFor(dashboard, datasource);
bulkUpdateAdHocFiltersVariable(filterVar, items);
};
@@ -3,6 +3,8 @@ import { getDataSourceSrv } from '@grafana/runtime';
import { AdHocFiltersVariable, GroupByVariable, sceneGraph, SceneObject, SceneQueryRunner } from '@grafana/scenes';
import { DataSourceRef } from '@grafana/schema';
import { getDatasourceFromQueryRunner } from './utils';
export function verifyDrilldownApplicability(
sourceObject: SceneObject,
queriesDataSource: DataSourceRef | undefined,
@@ -26,7 +28,7 @@ export async function getDrilldownApplicability(
return;
}
const datasource = queryRunner.state.datasource;
const datasource = getDatasourceFromQueryRunner(queryRunner);
const queries = queryRunner.state.data?.request?.targets;
const ds = await getDataSourceSrv().get(datasource?.uid);
@@ -4,7 +4,7 @@ import { sceneGraph, VizPanel } from '@grafana/scenes';
import { contextSrv } from 'app/core/services/context_srv';
import { getExploreUrl } from 'app/core/utils/explore';
import { getQueryRunnerFor } from './utils';
import { getDatasourceFromQueryRunner, getQueryRunnerFor } from './utils';
export function getViewPanelUrl(vizPanel: VizPanel) {
return locationUtil.getUrlForPartial(locationService.getLocation(), {
@@ -27,10 +27,11 @@ export function tryGetExploreUrlForPanel(vizPanel: VizPanel): Promise<string | u
}
const timeRange = sceneGraph.getTimeRange(vizPanel);
const datasource = getDatasourceFromQueryRunner(queryRunner);
return getExploreUrl({
queries: queryRunner.state.queries,
dsRef: queryRunner.state.datasource,
dsRef: datasource,
timeRange: timeRange.state.value,
scopedVars: { __sceneObject: { value: vizPanel } },
adhocFilters: queryRunner.state.data?.request?.filters,
@@ -1,4 +1,4 @@
import { getDataSourceRef, IntervalVariableModel } from '@grafana/data';
import { DataSourceRef, getDataSourceRef, IntervalVariableModel } from '@grafana/data';
import { t } from '@grafana/i18n';
import { config, getDataSourceSrv } from '@grafana/runtime';
import {
@@ -237,6 +237,26 @@ export function getQueryRunnerFor(sceneObject: SceneObject | undefined): SceneQu
return undefined;
}
/**
* Gets the datasource from a query runner.
* When no panel-level datasource is set, it means all queries use the same datasource,
* so we extract the datasource from the first query.
*/
export function getDatasourceFromQueryRunner(queryRunner: SceneQueryRunner): DataSourceRef | null | undefined {
// Panel-level datasource is set for mixed datasource panels
if (queryRunner.state.datasource) {
return queryRunner.state.datasource;
}
// No panel-level datasource means all queries share the same datasource
const firstQuery = queryRunner.state.queries?.[0];
if (firstQuery?.datasource) {
return firstQuery.datasource;
}
return undefined;
}
export function getDashboardSceneFor(sceneObject: SceneObject): DashboardScene {
const root = sceneObject.getRoot();