[--Dashboard-- data source] Implement getFiltersApplicability() (#108517)

**What is this feature?**

Implement `getFiltersApplicability()` on the `--Dashboard--` data source

Also refactor some existing code to use this new approach to naming

This follows some previous work to implement AdHoc filtering on the `--Dashboard--` data source in PR #108011 

**Why do we need this feature?**

See https://github.com/grafana/grafana/pull/107775 and https://github.com/grafana/grafana/pull/106756

> We want to see, when carrying filters from one dashboard to another, which of them are still applicable and which are not

Additionally I think @mdvictor is hoping to see more consistency in our approach across the codebase. But maybe he can confirm this.

**Who is this feature for?**

Everyone
This commit is contained in:
Sam Jewell
2025-08-05 10:27:09 +01:00
committed by GitHub
parent e4be1042d0
commit ff10f6da67
2 changed files with 163 additions and 22 deletions
@@ -569,6 +569,101 @@ describe('DashboardDatasource', () => {
expect(result.length).toBe(2);
});
});
describe('getFiltersApplicability', () => {
const originalToggleValue = config.featureToggles.dashboardDsAdHocFiltering;
const ds = new DashboardDatasource({} as DataSourceInstanceSettings);
beforeEach(() => {
config.featureToggles.dashboardDsAdHocFiltering = true;
});
afterEach(() => {
config.featureToggles.dashboardDsAdHocFiltering = originalToggleValue;
});
it('should return empty array when feature toggle is disabled', async () => {
config.featureToggles.dashboardDsAdHocFiltering = false;
const result = await ds.getFiltersApplicability({
filters: [{ key: 'name', operator: '=', value: 'test' }],
});
expect(result).toEqual([]);
});
it('should mark supported operators as applicable', async () => {
const result = await ds.getFiltersApplicability({
filters: [
{ key: 'name', operator: '=', value: 'John' },
{ key: 'age', operator: '!=', value: '25' },
],
});
expect(result).toEqual([
{ key: 'name', applicable: true },
{ key: 'age', applicable: true },
]);
});
it('should mark unsupported operators as not applicable with reason', async () => {
const result = await ds.getFiltersApplicability({
filters: [
{ key: 'name', operator: '>', value: 'John' },
{ key: 'age', operator: '<', value: '25' },
{ key: 'score', operator: '=~', value: 'pattern' },
],
});
expect(result).toEqual([
{
key: 'name',
applicable: false,
reason: "Operator '>' is not supported. Only '=' and '!=' operators are supported.",
},
{
key: 'age',
applicable: false,
reason: "Operator '<' is not supported. Only '=' and '!=' operators are supported.",
},
{
key: 'score',
applicable: false,
reason: "Operator '=~' is not supported. Only '=' and '!=' operators are supported.",
},
]);
});
it('should handle mixed applicable and non-applicable filters', async () => {
const result = await ds.getFiltersApplicability({
filters: [
{ key: 'name', operator: '=', value: 'John' },
{ key: 'age', operator: '>', value: '25' },
{ key: 'status', operator: '!=', value: 'active' },
],
});
expect(result).toEqual([
{ key: 'name', applicable: true },
{
key: 'age',
applicable: false,
reason: "Operator '>' is not supported. Only '=' and '!=' operators are supported.",
},
{ key: 'status', applicable: true },
]);
});
it('should handle empty filters array', async () => {
const result = await ds.getFiltersApplicability({ filters: [] });
expect(result).toEqual([]);
});
it('should handle missing options', async () => {
const result = await ds.getFiltersApplicability();
expect(result).toEqual([]);
});
});
});
});
@@ -17,6 +17,8 @@ import {
MetricFindValue,
getValueMatcher,
ValueMatcherID,
FiltersApplicability,
DataSourceGetTagKeysOptions,
} from '@grafana/data';
import { config } from '@grafana/runtime';
import { SceneDataProvider, SceneDataTransformer, SceneObject } from '@grafana/scenes';
@@ -157,30 +159,18 @@ export class DashboardDatasource extends DataSourceApi<DashboardQuery> {
return frame;
}
// Pre-compute field indices and value matchers for better performance
const filterFieldIndices = filters
.map((filter) => {
const fieldIndex = frame.fields.findIndex((f) => f.name === filter.key);
return { filter, fieldIndex, matcher: this.createValueMatcher(filter, fieldIndex, frame) };
})
.filter(({ filter, fieldIndex, matcher }) => {
// If field is not present:
// - Keep filters with '=' operator (will always be false - reject rows)
// - Remove filters with '!=' operator (will always be true - no effect)
if (fieldIndex === -1) {
return filter.operator === '=';
}
// Only keep filters with valid matchers
return matcher !== null;
});
// Filter out non-applicable filters for this specific DataFrame
const applicableFilters = this.getApplicableFiltersForFrame(frame, filters);
// If no filters remain after optimization, return original frame
if (filterFieldIndices.length === 0) {
// If no filters remain after filtering, return original frame
if (applicableFilters.length === 0) {
return frame;
}
// Short-circuit: if any filter has '=' operator with missing field, reject all rows
const hasImpossibleFilter = filterFieldIndices.some(({ fieldIndex }) => fieldIndex === -1);
// Check for impossible filters (missing field with '=' operator)
const hasImpossibleFilter = applicableFilters.some(
({ fieldIndex, filter }) => fieldIndex === -1 && filter.operator === '='
);
if (hasImpossibleFilter) {
return this.reconstructDataFrame(frame);
}
@@ -189,7 +179,7 @@ export class DashboardDatasource extends DataSourceApi<DashboardQuery> {
// Check each row to see if it matches all filters (AND logic)
for (let rowIndex = 0; rowIndex < frame.length; rowIndex++) {
const rowMatches = filterFieldIndices.every(({ matcher, fieldIndex }) => {
const rowMatches = applicableFilters.every(({ matcher, fieldIndex }) => {
const field = frame.fields[fieldIndex];
// Use Grafana's value matcher system
@@ -210,7 +200,31 @@ export class DashboardDatasource extends DataSourceApi<DashboardQuery> {
}
/**
* Create a value matcher from an AdHoc filter
* Get applicable filters for a specific DataFrame, considering field existence and type compatibility.
*/
private getApplicableFiltersForFrame(
frame: DataFrame,
filters: AdHocVariableFilter[]
): Array<{ filter: AdHocVariableFilter; fieldIndex: number; matcher: ReturnType<typeof getValueMatcher> | null }> {
return filters
.map((filter) => {
const fieldIndex = frame.fields.findIndex((f) => f.name === filter.key);
return { filter, fieldIndex, matcher: this.createValueMatcher(filter, fieldIndex, frame) };
})
.filter(({ filter, fieldIndex, matcher }) => {
// If field is not present:
// - Keep filters with '=' operator (will always be false - reject rows)
// - Remove filters with '!=' operator (will always be true - no effect)
if (fieldIndex === -1) {
return filter.operator === '=';
}
// Only keep filters with valid matchers
return matcher !== null;
});
}
/**
* Create a value matcher from an AdHoc filter.
*/
private createValueMatcher(filter: AdHocVariableFilter, fieldIndex: number, frame: DataFrame) {
// Return null for missing fields - they are handled separately
@@ -329,6 +343,38 @@ export class DashboardDatasource extends DataSourceApi<DashboardQuery> {
return Promise.resolve({ message: '', status: '' });
}
/**
* Check which AdHoc filters are applicable based on operator and field type support
*/
async getFiltersApplicability(
options?: DataSourceGetTagKeysOptions<DashboardQuery>
): Promise<FiltersApplicability[]> {
if (!config.featureToggles.dashboardDsAdHocFiltering) {
return [];
}
const filters = options?.filters || [];
return filters.map((filter): FiltersApplicability => {
// Check operator support
if (filter.operator !== '=' && filter.operator !== '!=') {
return {
key: filter.key,
applicable: false,
reason: `Operator '${filter.operator}' is not supported. Only '=' and '!=' operators are supported.`,
};
}
// For dashboard datasource, we can't determine field existence/type
// without the actual DataFrame context, so we assume applicable here
// and let the actual filtering logic handle field-specific checks
return {
key: filter.key,
applicable: true,
};
});
}
getTagKeys(): Promise<MetricFindValue[]> {
// Stub implementation to indicate AdHoc filter support
// Full implementation will be added in future PRs