From 88ebef5cba2529634357092595613226691e9f81 Mon Sep 17 00:00:00 2001 From: Tim Levett Date: Thu, 29 Feb 2024 16:59:40 -0600 Subject: [PATCH] Transformations: Add substring matcher to the 'Filter by Value' transformation (#83548) --- .../src/transformations/matchers.ts | 2 + .../src/transformations/matchers/ids.ts | 2 + .../valueMatchers/substringMatchers.test.ts | 130 ++++++++++++++++++ .../valueMatchers/substringMatchers.ts | 41 ++++++ .../ValueMatchers/BasicMatcherEditor.tsx | 14 ++ 5 files changed, 189 insertions(+) create mode 100644 packages/grafana-data/src/transformations/matchers/valueMatchers/substringMatchers.test.ts create mode 100644 packages/grafana-data/src/transformations/matchers/valueMatchers/substringMatchers.ts diff --git a/packages/grafana-data/src/transformations/matchers.ts b/packages/grafana-data/src/transformations/matchers.ts index 3ad60ca07cc..21ec33dda24 100644 --- a/packages/grafana-data/src/transformations/matchers.ts +++ b/packages/grafana-data/src/transformations/matchers.ts @@ -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(() => { ...getNullValueMatchers(), ...getNumericValueMatchers(), ...getEqualValueMatchers(), + ...getSubstringValueMatchers(), ...getRangeValueMatchers(), ...getRegexValueMatcher(), ]; diff --git a/packages/grafana-data/src/transformations/matchers/ids.ts b/packages/grafana-data/src/transformations/matchers/ids.ts index 5830e62f5fb..415e4fe21a8 100644 --- a/packages/grafana-data/src/transformations/matchers/ids.ts +++ b/packages/grafana-data/src/transformations/matchers/ids.ts @@ -52,5 +52,7 @@ export enum ValueMatcherID { lowerOrEqual = 'lowerOrEqual', equal = 'equal', notEqual = 'notEqual', + substring = 'substring', + notSubstring = 'notSubstring', between = 'between', } diff --git a/packages/grafana-data/src/transformations/matchers/valueMatchers/substringMatchers.test.ts b/packages/grafana-data/src/transformations/matchers/valueMatchers/substringMatchers.test.ts new file mode 100644 index 00000000000..f9f7789cd66 --- /dev/null +++ b/packages/grafana-data/src/transformations/matchers/valueMatchers/substringMatchers.test.ts @@ -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(); + }); +}); diff --git a/packages/grafana-data/src/transformations/matchers/valueMatchers/substringMatchers.ts b/packages/grafana-data/src/transformations/matchers/valueMatchers/substringMatchers.ts new file mode 100644 index 00000000000..034e4067eef --- /dev/null +++ b/packages/grafana-data/src/transformations/matchers/valueMatchers/substringMatchers.ts @@ -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 = { + 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 = { + 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]; diff --git a/public/app/features/transformers/FilterByValueTransformer/ValueMatchers/BasicMatcherEditor.tsx b/public/app/features/transformers/FilterByValueTransformer/ValueMatchers/BasicMatcherEditor.tsx index e80b2cbe2be..eadbc0b50e9 100644 --- a/public/app/features/transformers/FilterByValueTransformer/ValueMatchers/BasicMatcherEditor.tsx +++ b/public/app/features/transformers/FilterByValueTransformer/ValueMatchers/BasicMatcherEditor.tsx @@ -127,5 +127,19 @@ export const getBasicValueMatchersUI = (): Array true, }), }, + { + name: 'Is Substring', + id: ValueMatcherID.substring, + component: basicMatcherEditor({ + validator: () => true, + }), + }, + { + name: 'Is not substring', + id: ValueMatcherID.notSubstring, + component: basicMatcherEditor({ + validator: () => true, + }), + }, ]; };