Skip to content

Commit

Permalink
[Scopes]: Pass formatted scope filters to adhoc (grafana#101217)
Browse files Browse the repository at this point in the history
* pass formatted scope filters to adhoc

* fix

* fix

* fix scenario where we have equals and not-equals filters with the same key

* add canary packages for testing

* WIP

* refactor to pass all filter values

* rename property

* refactor

* update canary scenes

* update scenes version

* fix tests

* fix arg startProfile bug that arised with scenes update
  • Loading branch information
mdvictor authored Feb 27, 2025
1 parent 2372508 commit 7730532
Show file tree
Hide file tree
Showing 6 changed files with 426 additions and 1 deletion.
3 changes: 3 additions & 0 deletions .betterer.results
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,9 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
],
"packages/grafana-data/src/types/scopes.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"packages/grafana-data/src/types/select.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
Expand Down
2 changes: 2 additions & 0 deletions packages/grafana-data/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,8 @@ export {
type ScopeNodeSpec,
type ScopeNode,
scopeFilterOperatorMap,
reverseScopeFilterOperatorMap,
isEqualityOrMultiOperator,
} from './types/scopes';
export {
PluginState,
Expand Down
11 changes: 11 additions & 0 deletions packages/grafana-data/src/types/scopes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ export interface ScopeDashboardBinding {
}

export type ScopeFilterOperator = 'equals' | 'not-equals' | 'regex-match' | 'regex-not-match' | 'one-of' | 'not-one-of';
export type EqualityOrMultiOperator = Extract<ScopeFilterOperator, 'equals' | 'not-equals' | 'one-of' | 'not-one-of'>;

export function isEqualityOrMultiOperator(value: string): value is EqualityOrMultiOperator {
const operators = new Set(['equals', 'not-equals', 'one-of', 'not-one-of']);
return operators.has(value);
}

export const scopeFilterOperatorMap: Record<string, ScopeFilterOperator> = {
'=': 'equals',
Expand All @@ -28,6 +34,11 @@ export const scopeFilterOperatorMap: Record<string, ScopeFilterOperator> = {
'!=|': 'not-one-of',
};

export const reverseScopeFilterOperatorMap: Record<ScopeFilterOperator, string> = Object.fromEntries(
Object.entries(scopeFilterOperatorMap).map(([symbol, operator]) => [operator, symbol])
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
) as Record<ScopeFilterOperator, string>;

export interface ScopeSpecFilter {
key: string;
value: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { sceneGraph } from '@grafana/scenes';
import { AdHocFiltersVariable, sceneGraph } from '@grafana/scenes';
import { ScopesFacade } from 'app/features/scopes';

import { getDashboardSceneFor } from '../utils/utils';

import { convertScopesToAdHocFilters } from './convertScopesToAdHocFilters';

export interface DashboardScopesFacadeState {
reloadOnParamsChange?: boolean;
uid?: string;
Expand All @@ -13,7 +17,32 @@ export class DashboardScopesFacade extends ScopesFacade {
if (!reloadOnParamsChange || !uid) {
sceneGraph.getTimeRange(facade).onRefresh();
}

// push filters as soon as they come
this.pushScopeFiltersToAdHocVariable();
},
});

this.addActivationHandler(() => {
// also try to push filters on activation, for
// when the dashboard is changed
this.pushScopeFiltersToAdHocVariable();
});
}

private pushScopeFiltersToAdHocVariable() {
const dashboard = getDashboardSceneFor(this);

const adhoc = dashboard.state.$variables?.state.variables.find((v) => v instanceof AdHocFiltersVariable);

if (!adhoc) {
return;
}

const filters = convertScopesToAdHocFilters(this.value);

adhoc.setState({
baseFilters: filters,
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
import { Scope, ScopeSpecFilter } from '@grafana/data';
import { FilterOrigin } from '@grafana/scenes';

import { convertScopesToAdHocFilters } from './convertScopesToAdHocFilters';

describe('convertScopesToAdHocFilters', () => {
it('should return empty filters when no scopes are provided', () => {
let scopes = generateScopes([]);

expect(scopes).toEqual([]);
expect(convertScopesToAdHocFilters(scopes)).toEqual([]);

scopes = generateScopes([[], []]);

expect(convertScopesToAdHocFilters(scopes)).toEqual([]);
});

it('should return filters formatted for adHoc from a single scope', () => {
let scopes = generateScopes([
[
{ key: 'key1', value: 'value1', operator: 'equals' },
{ key: 'key2', value: 'value2', operator: 'not-equals' },
{ key: 'key3', value: 'value3', operator: 'regex-not-match' },
],
]);

expect(convertScopesToAdHocFilters(scopes)).toEqual([
{ key: 'key1', value: 'value1', operator: '=', origin: FilterOrigin.Scopes, values: ['value1'] },
{ key: 'key2', value: 'value2', operator: '!=', origin: FilterOrigin.Scopes, values: ['value2'] },
{ key: 'key3', value: 'value3', operator: '!~', origin: FilterOrigin.Scopes, values: ['value3'] },
]);

scopes = generateScopes([[{ key: 'key3', value: 'value3', operator: 'regex-match' }]]);

expect(convertScopesToAdHocFilters(scopes)).toEqual([
{ key: 'key3', value: 'value3', operator: '=~', origin: FilterOrigin.Scopes, values: ['value3'] },
]);
});

it('should return filters formatted for adHoc from multiple scopes with single values', () => {
let scopes = generateScopes([
[{ key: 'key1', value: 'value1', operator: 'equals' }],
[{ key: 'key2', value: 'value2', operator: 'regex-match' }],
]);

expect(convertScopesToAdHocFilters(scopes)).toEqual([
{ key: 'key1', value: 'value1', operator: '=', origin: FilterOrigin.Scopes, values: ['value1'] },
{ key: 'key2', value: 'value2', operator: '=~', origin: FilterOrigin.Scopes, values: ['value2'] },
]);
});

it('should return filters formatted for adHoc from multiple scopes with multiple values', () => {
let scopes = generateScopes([
[
{ key: 'key1', value: 'value1', operator: 'equals' },
{ key: 'key2', value: 'value2', operator: 'not-equals' },
],
[
{ key: 'key3', value: 'value3', operator: 'regex-match' },
{ key: 'key4', value: 'value4', operator: 'regex-match' },
],
]);

expect(convertScopesToAdHocFilters(scopes)).toEqual([
{ key: 'key1', value: 'value1', operator: '=', origin: FilterOrigin.Scopes, values: ['value1'] },
{ key: 'key2', value: 'value2', operator: '!=', origin: FilterOrigin.Scopes, values: ['value2'] },
{ key: 'key3', value: 'value3', operator: '=~', origin: FilterOrigin.Scopes, values: ['value3'] },
{ key: 'key4', value: 'value4', operator: '=~', origin: FilterOrigin.Scopes, values: ['value4'] },
]);
});

it('should return formatted filters and concat values of the same key, coming from different scopes, if operator supports multi-value', () => {
let scopes = generateScopes([
[
{ key: 'key1', value: 'value1', operator: 'equals' },
{ key: 'key2', value: 'value2', operator: 'not-equals' },
],
[
{ key: 'key1', value: 'value3', operator: 'equals' },
{ key: 'key2', value: 'value4', operator: 'not-equals' },
],
[{ key: 'key1', value: 'value5', operator: 'equals' }],
]);

expect(convertScopesToAdHocFilters(scopes)).toEqual([
{
key: 'key1',
value: 'value1',
operator: '=|',
origin: FilterOrigin.Scopes,
values: ['value1', 'value3', 'value5'],
},
{ key: 'key2', value: 'value2', operator: '!=|', origin: FilterOrigin.Scopes, values: ['value2', 'value4'] },
]);
});

it('should ignore the rest of the duplicate filters, if they are a combination of equals and not-equals', () => {
let scopes = generateScopes([
[{ key: 'key1', value: 'value1', operator: 'equals' }],
[{ key: 'key1', value: 'value2', operator: 'not-equals' }],
[{ key: 'key1', value: 'value3', operator: 'equals' }],
]);

expect(convertScopesToAdHocFilters(scopes)).toEqual([
{
key: 'key1',
value: 'value1',
operator: '=|',
origin: FilterOrigin.Scopes,
values: ['value1', 'value3'],
},
{
key: 'key1',
value: 'value2',
operator: '!=',
origin: FilterOrigin.Scopes,
values: ['value2'],
},
]);
});

it('should return formatted filters and keep only the first filter of the same key if operator is not multi-value', () => {
let scopes = generateScopes([
[
{ key: 'key1', value: 'value1', operator: 'regex-match' },
{ key: 'key2', value: 'value2', operator: 'not-equals' },
],
[
{ key: 'key1', value: 'value3', operator: 'regex-match' },
{ key: 'key2', value: 'value4', operator: 'not-equals' },
],
[{ key: 'key1', value: 'value5', operator: 'equals' }],
]);

expect(convertScopesToAdHocFilters(scopes)).toEqual([
{
key: 'key1',
value: 'value1',
operator: '=~',
origin: FilterOrigin.Scopes,
values: ['value1'],
},
{ key: 'key2', value: 'value2', operator: '!=|', origin: FilterOrigin.Scopes, values: ['value2', 'value4'] },
{
key: 'key1',
value: 'value3',
operator: '=~',
origin: FilterOrigin.Scopes,
values: ['value3'],
},
{
key: 'key1',
value: 'value5',
operator: '=',
origin: FilterOrigin.Scopes,
values: ['value5'],
},
]);

scopes = generateScopes([
[{ key: 'key1', value: 'value1', operator: 'regex-match' }],
[{ key: 'key1', value: 'value5', operator: 'equals' }],
[{ key: 'key1', value: 'value3', operator: 'regex-match' }],
]);

expect(convertScopesToAdHocFilters(scopes)).toEqual([
{
key: 'key1',
value: 'value1',
operator: '=~',
origin: FilterOrigin.Scopes,
values: ['value1'],
},
{
key: 'key1',
value: 'value5',
operator: '=',
origin: FilterOrigin.Scopes,
values: ['value5'],
},
{
key: 'key1',
value: 'value3',
operator: '=~',
origin: FilterOrigin.Scopes,
values: ['value3'],
},
]);
});

it('should return formatted filters and concat values that are multi-value and drop duplicates with non multi-value operator', () => {
let scopes = generateScopes([
[{ key: 'key1', value: 'value1', operator: 'equals' }],
[{ key: 'key1', value: 'value2', operator: 'regex-match' }],
[{ key: 'key1', value: 'value3', operator: 'equals' }],
]);

expect(convertScopesToAdHocFilters(scopes)).toEqual([
{
key: 'key1',
value: 'value1',
operator: '=|',
origin: FilterOrigin.Scopes,
values: ['value1', 'value3'],
},
{
key: 'key1',
value: 'value2',
operator: '=~',
origin: FilterOrigin.Scopes,
values: ['value2'],
},
]);

scopes = generateScopes([
[
{ key: 'key1', value: 'value1', operator: 'equals' },
{ key: 'key2', value: 'value2', operator: 'equals' },
],
[
{ key: 'key1', value: 'value3', operator: 'equals' },
{ key: 'key2', value: 'value4', operator: 'equals' },
],
[
{ key: 'key1', value: 'value5', operator: 'regex-match' },
{ key: 'key2', value: 'value6', operator: 'equals' },
],
[
{ key: 'key1', value: 'value7', operator: 'equals' },
{ key: 'key2', value: 'value8', operator: 'regex-match' },
],
[
{ key: 'key1', value: 'value9', operator: 'equals' },
{ key: 'key2', value: 'value10', operator: 'equals' },
],
]);

expect(convertScopesToAdHocFilters(scopes)).toEqual([
{
key: 'key1',
value: 'value1',
operator: '=|',
origin: FilterOrigin.Scopes,
values: ['value1', 'value3', 'value7', 'value9'],
},
{
key: 'key2',
value: 'value2',
operator: '=|',
origin: FilterOrigin.Scopes,
values: ['value2', 'value4', 'value6', 'value10'],
},
{
key: 'key1',
value: 'value5',
operator: '=~',
origin: FilterOrigin.Scopes,
values: ['value5'],
},
{
key: 'key2',
value: 'value8',
operator: '=~',
origin: FilterOrigin.Scopes,
values: ['value8'],
},
]);
});
});

function generateScopes(filtersSpec: ScopeSpecFilter[][]) {
const scopes: Scope[] = [];

for (let i = 0; i < filtersSpec.length; i++) {
scopes.push({
metadata: { name: `name-${i}` },
spec: {
title: `scope-${i}`,
type: '',
description: 'desc',
category: '',
filters: filtersSpec[i],
},
});
}

return scopes;
}
Loading

0 comments on commit 7730532

Please sign in to comment.