diff --git a/public/app/plugins/datasource/dashboard/DashboardQueryEditor.test.tsx b/public/app/plugins/datasource/dashboard/DashboardQueryEditor.test.tsx
index 0b794a48061..2cbfa311d7b 100644
--- a/public/app/plugins/datasource/dashboard/DashboardQueryEditor.test.tsx
+++ b/public/app/plugins/datasource/dashboard/DashboardQueryEditor.test.tsx
@@ -1,7 +1,8 @@
-import { render, screen } from '@testing-library/react';
+import { act, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { getDefaultTimeRange, LoadingState } from '@grafana/data';
+import config from 'app/core/config';
import { mockDataSource } from 'app/features/alerting/unified/mocks';
import { setupDataSources } from 'app/features/alerting/unified/testSetup/datasources';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
@@ -16,6 +17,7 @@ import { MIXED_DATASOURCE_NAME } from '../mixed/MixedDataSource';
import { DashboardQueryEditor, INVALID_PANEL_DESCRIPTION } from './DashboardQueryEditor';
import { SHARED_DASHBOARD_QUERY } from './constants';
import { DashboardDatasource } from './datasource';
+import { DashboardQuery } from './types';
jest.mock('app/core/config', () => ({
...jest.requireActual('app/core/config'),
@@ -28,6 +30,9 @@ jest.mock('app/core/config', () => ({
},
},
},
+ featureToggles: {
+ dashboardDsAdHocFiltering: false, // Default to false, can be overridden in tests
+ },
}));
setupDataSources(mockDataSource({ isDefault: true }));
@@ -164,4 +169,62 @@ describe('DashboardQueryEditor', () => {
INVALID_PANEL_DESCRIPTION
);
});
+
+ describe('AdHoc Filters Toggle', () => {
+ beforeEach(() => {
+ // Reset only the specific mocks we need, not all mocks
+ mockOnChange.mockClear();
+ mockOnRunQueries.mockClear();
+ // Re-establish the dashboard mock in case it was cleared
+ jest.spyOn(getDashboardSrv(), 'getCurrent').mockImplementation(() => mockDashboard);
+ });
+
+ it('shows the AdHoc Filters toggle when feature toggle is enabled', async () => {
+ await act(async () => {
+ config.featureToggles.dashboardDsAdHocFiltering = true;
+ });
+
+ const query: DashboardQuery = { refId: 'A', panelId: 1, adHocFiltersEnabled: false };
+
+ await act(async () => {
+ render(
+
+ );
+ });
+
+ const adhocFiltersToggle = await screen.findByText('AdHoc Filters');
+ expect(adhocFiltersToggle).toBeInTheDocument();
+ });
+
+ it('does not show the AdHoc Filters toggle when feature toggle is disabled', async () => {
+ await act(async () => {
+ config.featureToggles.dashboardDsAdHocFiltering = false;
+ });
+
+ const query: DashboardQuery = { refId: 'A', panelId: 1, adHocFiltersEnabled: false };
+
+ await act(async () => {
+ render(
+
+ );
+ });
+
+ // Wait for any async operations to complete
+ await waitFor(() => {
+ expect(screen.queryByText('AdHoc Filters')).not.toBeInTheDocument();
+ });
+ });
+ });
});
diff --git a/public/app/plugins/datasource/dashboard/DashboardQueryEditor.tsx b/public/app/plugins/datasource/dashboard/DashboardQueryEditor.tsx
index 4ce823746e2..e137069219d 100644
--- a/public/app/plugins/datasource/dashboard/DashboardQueryEditor.tsx
+++ b/public/app/plugins/datasource/dashboard/DashboardQueryEditor.tsx
@@ -108,6 +108,13 @@ export function DashboardQueryEditor({ data, query, onChange, onRunQuery }: Prop
[query, onUpdateQuery]
);
+ const onAdHocFiltersToggle = useCallback(() => {
+ onUpdateQuery({
+ ...query,
+ adHocFiltersEnabled: !query.adHocFiltersEnabled,
+ });
+ }, [query, onUpdateQuery]);
+
const isMixedDSWithDashboardQueries = (panel: PanelModel) => {
return (
panel.datasource?.uid === MIXED_DATASOURCE_NAME &&
@@ -197,6 +204,16 @@ export function DashboardQueryEditor({ data, query, onChange, onRunQuery }: Prop
)}
+
+ {config.featureToggles.dashboardDsAdHocFiltering && (
+
+
+
+ )}
{loadingResults ? (
diff --git a/public/app/plugins/datasource/dashboard/datasource.test.ts b/public/app/plugins/datasource/dashboard/datasource.test.ts
index 3ecde52e9b6..55d7a12968d 100644
--- a/public/app/plugins/datasource/dashboard/datasource.test.ts
+++ b/public/app/plugins/datasource/dashboard/datasource.test.ts
@@ -159,10 +159,10 @@ describe('DashboardDatasource', () => {
};
}
- function createQueryRequest(filters: AdHocVariableFilter[], scene: SceneObject) {
+ function createQueryRequest(filters: AdHocVariableFilter[], scene: SceneObject, adHocFiltersEnabled?: boolean) {
return {
timezone: 'utc',
- targets: [{ refId: 'A', panelId: 1 }],
+ targets: [{ refId: 'A', panelId: 1, adHocFiltersEnabled }],
requestId: '',
interval: '',
intervalMs: 0,
@@ -214,7 +214,7 @@ describe('DashboardDatasource', () => {
const ds = new DashboardDatasource({} as DataSourceInstanceSettings);
const filters: AdHocVariableFilter[] = [{ key: 'name', operator: '=', value: 'John' }];
- const observable = ds.query(createQueryRequest(filters, scene));
+ const observable = ds.query(createQueryRequest(filters, scene, true));
let result: DataQueryResponse | undefined;
observable.subscribe({ next: (data) => (result = data) });
@@ -264,6 +264,158 @@ describe('DashboardDatasource', () => {
expect(result?.data[0].length).toBe(3);
});
+ it('should respect per-panel adHocFiltersEnabled setting and not filter when disabled', async () => {
+ const testFrame = createTestFrame([
+ { name: 'name', type: FieldType.string, values: ['John', 'Jane', 'Bob'] },
+ { name: 'age', type: FieldType.number, values: [25, 30, 35] },
+ ]);
+
+ const scene = new SceneFlexLayout({
+ children: [
+ new SceneFlexItem({
+ body: new VizPanel({
+ key: getVizPanelKeyForPanelId(1),
+ $data: new SceneDataNode({
+ data: {
+ series: [testFrame],
+ state: LoadingState.Done,
+ timeRange: getDefaultTimeRange(),
+ },
+ }),
+ }),
+ }),
+ ],
+ });
+
+ const ds = new DashboardDatasource({} as DataSourceInstanceSettings);
+ const filters: AdHocVariableFilter[] = [{ key: 'name', operator: '=', value: 'John' }];
+
+ // Test with adHocFiltersEnabled explicitly set to false
+ const observable = ds.query(createQueryRequest(filters, scene, false));
+
+ let result: DataQueryResponse | undefined;
+ observable.subscribe({ next: (data) => (result = data) });
+
+ // Should return unfiltered data since per-panel setting is disabled
+ expect(result?.data[0].fields[0].values).toEqual(['John', 'Jane', 'Bob']);
+ expect(result?.data[0].fields[1].values).toEqual([25, 30, 35]);
+ expect(result?.data[0].length).toBe(3);
+ });
+
+ it('should not filter when adHocFiltersEnabled is undefined (default behavior)', async () => {
+ const testFrame = createTestFrame([
+ { name: 'name', type: FieldType.string, values: ['John', 'Jane', 'Bob'] },
+ { name: 'age', type: FieldType.number, values: [25, 30, 35] },
+ ]);
+
+ const scene = new SceneFlexLayout({
+ children: [
+ new SceneFlexItem({
+ body: new VizPanel({
+ key: getVizPanelKeyForPanelId(1),
+ $data: new SceneDataNode({
+ data: {
+ series: [testFrame],
+ state: LoadingState.Done,
+ timeRange: getDefaultTimeRange(),
+ },
+ }),
+ }),
+ }),
+ ],
+ });
+
+ const ds = new DashboardDatasource({} as DataSourceInstanceSettings);
+ const filters: AdHocVariableFilter[] = [{ key: 'name', operator: '=', value: 'John' }];
+
+ // Test with adHocFiltersEnabled undefined (should default to not filtering)
+ const observable = ds.query(createQueryRequest(filters, scene));
+
+ let result: DataQueryResponse | undefined;
+ observable.subscribe({ next: (data) => (result = data) });
+
+ // Should return unfiltered data since adHocFiltersEnabled is not set
+ expect(result?.data[0].fields[0].values).toEqual(['John', 'Jane', 'Bob']);
+ expect(result?.data[0].fields[1].values).toEqual([25, 30, 35]);
+ expect(result?.data[0].length).toBe(3);
+ });
+
+ it('should apply filtering when adHocFiltersEnabled is explicitly enabled', async () => {
+ const testFrame = createTestFrame([
+ { name: 'name', type: FieldType.string, values: ['John', 'Jane', 'Bob'] },
+ { name: 'age', type: FieldType.number, values: [25, 30, 35] },
+ ]);
+
+ const scene = new SceneFlexLayout({
+ children: [
+ new SceneFlexItem({
+ body: new VizPanel({
+ key: getVizPanelKeyForPanelId(1),
+ $data: new SceneDataNode({
+ data: {
+ series: [testFrame],
+ state: LoadingState.Done,
+ timeRange: getDefaultTimeRange(),
+ },
+ }),
+ }),
+ }),
+ ],
+ });
+
+ const ds = new DashboardDatasource({} as DataSourceInstanceSettings);
+ const filters: AdHocVariableFilter[] = [{ key: 'name', operator: '=', value: 'John' }];
+
+ // Test with adHocFiltersEnabled explicitly set to true
+ const observable = ds.query(createQueryRequest(filters, scene, true));
+
+ let result: DataQueryResponse | undefined;
+ observable.subscribe({ next: (data) => (result = data) });
+
+ // Should return filtered data since adHocFiltersEnabled is enabled
+ expect(result?.data[0].fields[0].values).toEqual(['John']);
+ expect(result?.data[0].fields[1].values).toEqual([25]);
+ expect(result?.data[0].length).toBe(1);
+ });
+
+ it('should apply not-equal filtering when adHocFiltersEnabled is enabled', async () => {
+ const testFrame = createTestFrame([
+ { name: 'name', type: FieldType.string, values: ['John', 'Jane', 'Bob'] },
+ { name: 'age', type: FieldType.number, values: [25, 30, 35] },
+ ]);
+
+ const scene = new SceneFlexLayout({
+ children: [
+ new SceneFlexItem({
+ body: new VizPanel({
+ key: getVizPanelKeyForPanelId(1),
+ $data: new SceneDataNode({
+ data: {
+ series: [testFrame],
+ state: LoadingState.Done,
+ timeRange: getDefaultTimeRange(),
+ },
+ }),
+ }),
+ }),
+ ],
+ });
+
+ const ds = new DashboardDatasource({} as DataSourceInstanceSettings);
+ const filters: AdHocVariableFilter[] = [{ key: 'name', operator: '!=', value: 'John' }];
+
+ // Test with adHocFiltersEnabled explicitly set to true
+ const observable = ds.query(createQueryRequest(filters, scene, true));
+
+ let result: DataQueryResponse | undefined;
+ observable.subscribe({ next: (data) => (result = data) });
+
+ // Should return filtered data excluding 'John'
+ expect(result?.data[0].fields[0].values).toEqual(['Jane', 'Bob']);
+ expect(result?.data[0].fields[1].values).toEqual([30, 35]);
+ expect(result?.data[0].length).toBe(2);
+ });
+
it('should apply multiple filters with AND logic through public API', async () => {
const testFrame = createTestFrame([
{ name: 'name', type: FieldType.string, values: ['John', 'Jane', 'Bob'] },
@@ -293,7 +445,7 @@ describe('DashboardDatasource', () => {
{ key: 'status', operator: '=', value: 'active' },
];
- const observable = ds.query(createQueryRequest(filters, scene));
+ const observable = ds.query(createQueryRequest(filters, scene, true));
let result: DataQueryResponse | undefined;
observable.subscribe({ next: (data) => (result = data) });
@@ -598,6 +750,7 @@ describe('DashboardDatasource', () => {
{ key: 'name', operator: '=', value: 'John' },
{ key: 'age', operator: '!=', value: '25' },
],
+ queries: [{ refId: 'A', panelId: 1, adHocFiltersEnabled: true }],
});
expect(result).toEqual([
@@ -606,6 +759,29 @@ describe('DashboardDatasource', () => {
]);
});
+ it('should return empty array when no query has adHocFiltersEnabled enabled', async () => {
+ const result = await ds.getDrilldownsApplicability({
+ filters: [
+ { key: 'name', operator: '=', value: 'John' },
+ { key: 'age', operator: '!=', value: '25' },
+ ],
+ queries: [{ refId: 'A', panelId: 1, adHocFiltersEnabled: false }],
+ });
+
+ expect(result).toEqual([]);
+ });
+
+ it('should return empty array when queries is undefined', async () => {
+ const result = await ds.getDrilldownsApplicability({
+ filters: [
+ { key: 'name', operator: '=', value: 'John' },
+ { key: 'age', operator: '!=', value: '25' },
+ ],
+ });
+
+ expect(result).toEqual([]);
+ });
+
it('should mark unsupported operators as not applicable with reason', async () => {
const result = await ds.getDrilldownsApplicability({
filters: [
@@ -613,6 +789,7 @@ describe('DashboardDatasource', () => {
{ key: 'age', operator: '<', value: '25' },
{ key: 'score', operator: '=~', value: 'pattern' },
],
+ queries: [{ refId: 'A', panelId: 1, adHocFiltersEnabled: true }],
});
expect(result).toEqual([
@@ -641,6 +818,7 @@ describe('DashboardDatasource', () => {
{ key: 'age', operator: '>', value: '25' },
{ key: 'status', operator: '!=', value: 'active' },
],
+ queries: [{ refId: 'A', panelId: 1, adHocFiltersEnabled: true }],
});
expect(result).toEqual([
diff --git a/public/app/plugins/datasource/dashboard/datasource.ts b/public/app/plugins/datasource/dashboard/datasource.ts
index c5efa221fc4..2386418231a 100644
--- a/public/app/plugins/datasource/dashboard/datasource.ts
+++ b/public/app/plugins/datasource/dashboard/datasource.ts
@@ -128,10 +128,11 @@ export class DashboardDatasource extends DataSourceApi {
...field,
config: {
...field.config,
- // Enable AdHoc filtering for string and numeric fields only when feature toggle is enabled
- filterable: config.featureToggles.dashboardDsAdHocFiltering
- ? field.type === FieldType.string || field.type === FieldType.number
- : field.config.filterable,
+ // Enable AdHoc filtering for string and numeric fields only when feature toggle and per-panel setting are enabled
+ filterable:
+ config.featureToggles.dashboardDsAdHocFiltering && query.adHocFiltersEnabled
+ ? field.type === FieldType.string || field.type === FieldType.number
+ : field.config.filterable,
},
state: {
...field.state,
@@ -140,7 +141,7 @@ export class DashboardDatasource extends DataSourceApi {
};
});
- if (!config.featureToggles.dashboardDsAdHocFiltering || filters.length === 0) {
+ if (!config.featureToggles.dashboardDsAdHocFiltering || !query.adHocFiltersEnabled || filters.length === 0) {
return [...series, ...annotations];
}
@@ -348,6 +349,13 @@ export class DashboardDatasource extends DataSourceApi {
return [];
}
+ // Check if any query has adhoc filters enabled
+ const hasAdHocFiltersEnabled = options?.queries?.some((query) => query.adHocFiltersEnabled);
+
+ if (!hasAdHocFiltersEnabled) {
+ return [];
+ }
+
const filters = options?.filters || [];
return filters.map((filter): DrilldownsApplicability => {
diff --git a/public/app/plugins/datasource/dashboard/types.ts b/public/app/plugins/datasource/dashboard/types.ts
index 867605b1d07..d55141c183a 100644
--- a/public/app/plugins/datasource/dashboard/types.ts
+++ b/public/app/plugins/datasource/dashboard/types.ts
@@ -4,6 +4,7 @@ export interface DashboardQuery extends DataQuery {
panelId?: number;
withTransforms?: boolean;
topic?: DataTopic;
+ adHocFiltersEnabled?: boolean;
}
export type ResultInfo = {