Transformations: Add substring matcher to the 'Filter by Value' transformation (#83548)

This commit is contained in:
Tim Levett 2024-02-29 16:59:40 -06:00 committed by GitHub
parent 74115f1f08
commit 88ebef5cba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 189 additions and 0 deletions

View File

@ -21,6 +21,7 @@ import { getNullValueMatchers } from './matchers/valueMatchers/nullMatchers';
import { getNumericValueMatchers } from './matchers/valueMatchers/numericMatchers';
import { getRangeValueMatchers } from './matchers/valueMatchers/rangeMatchers';
import { getRegexValueMatcher } from './matchers/valueMatchers/regexMatchers';
import { getSubstringValueMatchers } from './matchers/valueMatchers/substringMatchers';
export { type FieldValueMatcherConfig } from './matchers/fieldValueMatcher';
@ -59,6 +60,7 @@ export const valueMatchers = new Registry<ValueMatcherInfo>(() => {
...getNullValueMatchers(),
...getNumericValueMatchers(),
...getEqualValueMatchers(),
...getSubstringValueMatchers(),
...getRangeValueMatchers(),
...getRegexValueMatcher(),
];

View File

@ -52,5 +52,7 @@ export enum ValueMatcherID {
lowerOrEqual = 'lowerOrEqual',
equal = 'equal',
notEqual = 'notEqual',
substring = 'substring',
notSubstring = 'notSubstring',
between = 'between',
}

View File

@ -0,0 +1,130 @@
import { toDataFrame } from '../../../dataframe';
import { DataFrame } from '../../../types/dataFrame';
import { getValueMatcher } from '../../matchers';
import { ValueMatcherID } from '../ids';
describe('value substring to matcher', () => {
const data: DataFrame[] = [
toDataFrame({
fields: [
{
name: 'temp',
values: ['24', null, '10', 'asd', '42'],
},
],
}),
];
const matcher = getValueMatcher({
id: ValueMatcherID.substring,
options: {
value: '2',
},
});
it('should match when option value is a substring', () => {
const frame = data[0];
const field = frame.fields[0];
const valueIndex = 0;
expect(matcher(valueIndex, field, frame, data)).toBeTruthy();
});
// Added for https://github.com/grafana/grafana/pull/83548#pullrequestreview-1904931540 where the matcher was not handling null values
it('should be a mismatch if the option is null and should not cause errors', () => {
const frame = data[0];
const field = frame.fields[0];
const valueIndex = 1;
expect(matcher(valueIndex, field, frame, data)).toBeFalsy();
});
it('should not match when option value is different', () => {
const frame = data[0];
const field = frame.fields[0];
const valueIndex = 2;
expect(matcher(valueIndex, field, frame, data)).toBeFalsy();
});
it('should match when option value is a substring', () => {
const frame = data[0];
const field = frame.fields[0];
const valueIndex = 4;
expect(matcher(valueIndex, field, frame, data)).toBeTruthy();
});
});
describe('value not substring matcher', () => {
const data: DataFrame[] = [
toDataFrame({
fields: [
{
name: 'temp',
values: ['24', null, '050', 'asd', '42', '0'],
},
],
}),
];
const matcher = getValueMatcher({
id: ValueMatcherID.notSubstring,
options: {
value: '5',
},
});
it('should not match if the value is "0" and the option value is "0"', () => {
const frame = data[0];
const field = frame.fields[0];
const valueIndex = 5;
const zeroMatcher = getValueMatcher({
id: ValueMatcherID.notSubstring,
options: {
value: '0',
},
});
expect(zeroMatcher(valueIndex, field, frame, data)).toBeFalsy();
});
it('should match when option value is a substring', () => {
const frame = data[0];
const field = frame.fields[0];
const valueIndex = 0;
expect(matcher(valueIndex, field, frame, data)).toBeTruthy();
});
it('should not match when option value is different', () => {
const frame = data[0];
const field = frame.fields[0];
const valueIndex = 2;
expect(matcher(valueIndex, field, frame, data)).toBeFalsy();
});
it('should match when value is null because null its not a substring', () => {
const frame = data[0];
const field = frame.fields[0];
const valueIndex = 4;
expect(matcher(valueIndex, field, frame, data)).toBeTruthy();
});
it('it should not match if the option value is empty string', () => {
const frame = data[0];
const field = frame.fields[0];
const valueIndex = 0;
const emptyMatcher = getValueMatcher({
id: ValueMatcherID.notSubstring,
options: {
value: '',
},
});
expect(emptyMatcher(valueIndex, field, frame, data)).toBeFalsy();
});
});

View File

@ -0,0 +1,41 @@
import { Field, FieldType } from '../../../types/dataFrame';
import { ValueMatcherInfo } from '../../../types/transformations';
import { ValueMatcherID } from '../ids';
import { BasicValueMatcherOptions } from './types';
const isSubstringMatcher: ValueMatcherInfo<BasicValueMatcherOptions> = {
id: ValueMatcherID.substring,
name: 'Contains Substring',
description: 'Match where value for given field is a substring to options value.',
get: (options) => {
return (valueIndex: number, field: Field) => {
const value = field.values[valueIndex];
return (value && value.includes(options.value)) || options.value === '';
};
},
getOptionsDisplayText: () => {
return `Matches all rows where field is similar to the value.`;
},
isApplicable: (field) => field.type === FieldType.string,
getDefaultOptions: () => ({ value: '' }),
};
const isNotSubstringValueMatcher: ValueMatcherInfo<BasicValueMatcherOptions> = {
id: ValueMatcherID.notSubstring,
name: 'Does not contain substring',
description: 'Match where value for given field is not a substring to options value.',
get: (options) => {
return (valueIndex: number, field: Field) => {
const value = field.values[valueIndex];
return typeof value === 'string' && options.value !== '' && !value.includes(options.value);
};
},
getOptionsDisplayText: () => {
return `Matches all rows where field is not similar to the value.`;
},
isApplicable: (field) => field.type === FieldType.string,
getDefaultOptions: () => ({ value: '' }),
};
export const getSubstringValueMatchers = (): ValueMatcherInfo[] => [isSubstringMatcher, isNotSubstringValueMatcher];

View File

@ -127,5 +127,19 @@ export const getBasicValueMatchersUI = (): Array<ValueMatcherUIRegistryItem<Basi
validator: () => true,
}),
},
{
name: 'Is Substring',
id: ValueMatcherID.substring,
component: basicMatcherEditor<string | number | boolean>({
validator: () => true,
}),
},
{
name: 'Is not substring',
id: ValueMatcherID.notSubstring,
component: basicMatcherEditor<string | number | boolean>({
validator: () => true,
}),
},
];
};