mirror of https://github.com/grafana/grafana
[Scopes]: Pass formatted scope filters to adhoc (#101217)
* 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 updatepull/101414/head
parent
2372508e9e
commit
77305325c2
@ -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; |
||||||
|
} |
@ -0,0 +1,92 @@ |
|||||||
|
import { |
||||||
|
Scope, |
||||||
|
ScopeSpecFilter, |
||||||
|
isEqualityOrMultiOperator, |
||||||
|
reverseScopeFilterOperatorMap, |
||||||
|
scopeFilterOperatorMap, |
||||||
|
} from '@grafana/data'; |
||||||
|
import { AdHocFilterWithLabels, FilterOrigin } from '@grafana/scenes'; |
||||||
|
|
||||||
|
export function convertScopesToAdHocFilters(scopes: Scope[]): AdHocFilterWithLabels[] { |
||||||
|
const formattedFilters: Map<string, AdHocFilterWithLabels> = new Map(); |
||||||
|
// duplicated filters that could not be processed in any way are just appended to the list
|
||||||
|
const duplicatedFilters: AdHocFilterWithLabels[] = []; |
||||||
|
const allFilters = scopes.flatMap((scope) => scope.spec.filters); |
||||||
|
|
||||||
|
for (const filter of allFilters) { |
||||||
|
processFilter(formattedFilters, duplicatedFilters, filter); |
||||||
|
} |
||||||
|
|
||||||
|
return [...formattedFilters.values(), ...duplicatedFilters]; |
||||||
|
} |
||||||
|
|
||||||
|
function processFilter( |
||||||
|
formattedFilters: Map<string, AdHocFilterWithLabels>, |
||||||
|
duplicatedFilters: AdHocFilterWithLabels[], |
||||||
|
filter: ScopeSpecFilter |
||||||
|
) { |
||||||
|
const existingFilter = formattedFilters.get(filter.key); |
||||||
|
|
||||||
|
if (existingFilter && canValueBeMerged(existingFilter.operator, filter.operator)) { |
||||||
|
mergeFilterValues(existingFilter, filter); |
||||||
|
} else if (!existingFilter) { |
||||||
|
// Add filter to map either only if it is new.
|
||||||
|
// Otherwise it is an existing filter that cannot be converted to multi-value
|
||||||
|
// and thus will be moved to the duplicatedFilters list
|
||||||
|
formattedFilters.set(filter.key, { |
||||||
|
key: filter.key, |
||||||
|
operator: reverseScopeFilterOperatorMap[filter.operator], |
||||||
|
value: filter.value, |
||||||
|
values: filter.values ?? [filter.value], |
||||||
|
origin: FilterOrigin.Scopes, |
||||||
|
}); |
||||||
|
} else { |
||||||
|
duplicatedFilters.push({ |
||||||
|
key: filter.key, |
||||||
|
operator: reverseScopeFilterOperatorMap[filter.operator], |
||||||
|
value: filter.value, |
||||||
|
values: filter.values ?? [filter.value], |
||||||
|
origin: FilterOrigin.Scopes, |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function mergeFilterValues(adHocFilter: AdHocFilterWithLabels, filter: ScopeSpecFilter) { |
||||||
|
const values = filter.values ?? [filter.value]; |
||||||
|
|
||||||
|
for (const value of values) { |
||||||
|
if (!adHocFilter.values?.includes(value)) { |
||||||
|
adHocFilter.values?.push(value); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// If there's only one value, there's no need to update the
|
||||||
|
// operator to its multi-value equivalent
|
||||||
|
if (adHocFilter.values?.length === 1) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
// Otherwise update it to the equivalent multi-value operator
|
||||||
|
if (filter.operator === 'equals' && adHocFilter.operator === reverseScopeFilterOperatorMap['equals']) { |
||||||
|
adHocFilter.operator = reverseScopeFilterOperatorMap['one-of']; |
||||||
|
} else if (filter.operator === 'not-equals' && adHocFilter.operator === reverseScopeFilterOperatorMap['not-equals']) { |
||||||
|
adHocFilter.operator = reverseScopeFilterOperatorMap['not-one-of']; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function canValueBeMerged(adHocFilterOperator: string, filterOperator: string) { |
||||||
|
const scopeConvertedOperator = scopeFilterOperatorMap[adHocFilterOperator]; |
||||||
|
|
||||||
|
if (!isEqualityOrMultiOperator(scopeConvertedOperator) || !isEqualityOrMultiOperator(filterOperator)) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
if ( |
||||||
|
(scopeConvertedOperator.includes('not') && !filterOperator.includes('not')) || |
||||||
|
(!scopeConvertedOperator.includes('not') && filterOperator.includes('not')) |
||||||
|
) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
return true; |
||||||
|
} |
Loading…
Reference in new issue