[--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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user