From 96f26cbd5b9e7d9a7e89a3bd0dd4bc843b1469ef Mon Sep 17 00:00:00 2001 From: Marcus Andersson Date: Thu, 14 May 2020 20:48:43 +0200 Subject: [PATCH] Transform: fixes so we match the field based on the proper name. (#24659) * fixes so we match the transformer based on name properly. * changed the signature on the FieldMatcher. * introduced a names option so you can filter in name specificly. * changed so the matcher UI uses the new options format. * moved the exported functions together. * changing editors a bit. * made the filter by name work with both regex and name filtering. * fixed failing tests and make sure we always parse regex the same way. * removed unused code. * simplified to make the existing field overrides still working. * fixed issue reported by hugo. * added tests for the name matcher. * added tests for filter by name. * added more tests. --- .../grafana-data/src/field/fieldOverrides.ts | 2 +- .../grafana-data/src/transformations/index.ts | 1 + .../matchers/fieldTypeMatcher.test.ts | 3 +- .../matchers/fieldTypeMatcher.ts | 4 +- .../src/transformations/matchers/ids.ts | 3 + .../matchers/nameMatcher.test.ts | 266 +++++++++++++++++- .../transformations/matchers/nameMatcher.ts | 112 ++++++-- .../matchers/predicates.test.ts | 9 +- .../transformations/matchers/predicates.ts | 12 +- .../src/transformations/transformers.ts | 2 + .../transformers/calculateField.test.ts | 6 +- .../transformers/calculateField.ts | 53 +--- .../transformers/ensureColumns.test.ts | 136 +++++++++ .../transformers/ensureColumns.ts | 43 +++ .../transformations/transformers/filter.ts | 5 +- .../transformers/filterByName.test.ts | 108 ++++++- .../transformers/filterByName.ts | 49 ++-- .../src/transformations/transformers/ids.ts | 28 +- .../transformations/transformers/organize.ts | 2 +- .../transformations/transformers/reduce.ts | 2 +- .../grafana-data/src/types/transformations.ts | 2 +- .../MatchersUI/FieldNameMatcherEditor.tsx | 63 +++-- .../CalculateFieldTransformerEditor.tsx | 14 +- .../FilterByNameTransformerEditor.tsx | 66 +++-- 24 files changed, 806 insertions(+), 185 deletions(-) create mode 100644 packages/grafana-data/src/transformations/transformers/ensureColumns.test.ts create mode 100644 packages/grafana-data/src/transformations/transformers/ensureColumns.ts diff --git a/packages/grafana-data/src/field/fieldOverrides.ts b/packages/grafana-data/src/field/fieldOverrides.ts index d6abd3a7ad6..f39a4faf9d5 100644 --- a/packages/grafana-data/src/field/fieldOverrides.ts +++ b/packages/grafana-data/src/field/fieldOverrides.ts @@ -138,7 +138,7 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra // Find any matching rules and then override for (const rule of override) { - if (rule.match(field)) { + if (rule.match(field, frame, options.data!)) { for (const prop of rule.properties) { // config.scopedVars is set already here setDynamicConfigValue(config, prop, context); diff --git a/packages/grafana-data/src/transformations/index.ts b/packages/grafana-data/src/transformations/index.ts index aad76b97463..16894368444 100644 --- a/packages/grafana-data/src/transformations/index.ts +++ b/packages/grafana-data/src/transformations/index.ts @@ -9,3 +9,4 @@ export { TransformerUIProps, standardTransformersRegistry, } from './standardTransformersRegistry'; +export { RegexpOrNamesMatcherOptions } from './matchers/nameMatcher'; diff --git a/packages/grafana-data/src/transformations/matchers/fieldTypeMatcher.test.ts b/packages/grafana-data/src/transformations/matchers/fieldTypeMatcher.test.ts index cf8f7006e33..c6370b0ba8e 100644 --- a/packages/grafana-data/src/transformations/matchers/fieldTypeMatcher.test.ts +++ b/packages/grafana-data/src/transformations/matchers/fieldTypeMatcher.test.ts @@ -16,7 +16,8 @@ describe('Field Type Matcher', () => { it('finds numbers', () => { for (const field of simpleSeriesWithTypes.fields) { const matches = matcher.get(FieldType.number); - expect(matches(field)).toBe(field.type === FieldType.number); + const didMatch = matches(field, simpleSeriesWithTypes, [simpleSeriesWithTypes]); + expect(didMatch).toBe(field.type === FieldType.number); } }); }); diff --git a/packages/grafana-data/src/transformations/matchers/fieldTypeMatcher.ts b/packages/grafana-data/src/transformations/matchers/fieldTypeMatcher.ts index 4a9d2dffe43..3ef4508d2a2 100644 --- a/packages/grafana-data/src/transformations/matchers/fieldTypeMatcher.ts +++ b/packages/grafana-data/src/transformations/matchers/fieldTypeMatcher.ts @@ -1,4 +1,4 @@ -import { Field, FieldType } from '../../types/dataFrame'; +import { Field, FieldType, DataFrame } from '../../types/dataFrame'; import { FieldMatcherID } from './ids'; import { FieldMatcherInfo } from '../../types/transformations'; @@ -10,7 +10,7 @@ const fieldTypeMatcher: FieldMatcherInfo = { defaultOptions: FieldType.number, get: (type: FieldType) => { - return (field: Field) => { + return (field: Field, frame: DataFrame, allFrames: DataFrame[]) => { return type === field.type; }; }, diff --git a/packages/grafana-data/src/transformations/matchers/ids.ts b/packages/grafana-data/src/transformations/matchers/ids.ts index ee57678e26f..cb4c5b5529b 100644 --- a/packages/grafana-data/src/transformations/matchers/ids.ts +++ b/packages/grafana-data/src/transformations/matchers/ids.ts @@ -18,6 +18,9 @@ export enum FieldMatcherID { // With arguments byType = 'byType', byName = 'byName', + byNames = 'byNames', + byRegexp = 'byRegexp', + byRegexpOrNames = 'byRegexpOrNames', // byIndex = 'byIndex', // byLabel = 'byLabel', } diff --git a/packages/grafana-data/src/transformations/matchers/nameMatcher.test.ts b/packages/grafana-data/src/transformations/matchers/nameMatcher.test.ts index 0f3df71088f..fefca6bd406 100644 --- a/packages/grafana-data/src/transformations/matchers/nameMatcher.test.ts +++ b/packages/grafana-data/src/transformations/matchers/nameMatcher.test.ts @@ -2,20 +2,20 @@ import { getFieldMatcher } from '../matchers'; import { FieldMatcherID } from './ids'; import { toDataFrame } from '../../dataframe/processDataFrame'; -describe('Field Name Matcher', () => { +describe('Field Name by Regexp Matcher', () => { it('Match all with wildcard regex', () => { const seriesWithNames = toDataFrame({ fields: [{ name: 'A hello world' }, { name: 'AAA' }, { name: 'C' }], }); const config = { - id: FieldMatcherID.byName, + id: FieldMatcherID.byRegexp, options: '/.*/', }; const matcher = getFieldMatcher(config); for (const field of seriesWithNames.fields) { - expect(matcher(field)).toBe(true); + expect(matcher(field, seriesWithNames, [seriesWithNames])).toBe(true); } }); @@ -24,14 +24,14 @@ describe('Field Name Matcher', () => { fields: [{ name: '12' }, { name: '112' }, { name: '13' }], }); const config = { - id: FieldMatcherID.byName, + id: FieldMatcherID.byRegexp, options: '/^\\d+$/', }; const matcher = getFieldMatcher(config); for (const field of seriesWithNames.fields) { - expect(matcher(field)).toBe(true); + expect(matcher(field, seriesWithNames, [seriesWithNames])).toBe(true); } }); @@ -40,17 +40,269 @@ describe('Field Name Matcher', () => { fields: [{ name: 'some.instance.path' }, { name: '112' }, { name: '13' }], }); const config = { - id: FieldMatcherID.byName, + id: FieldMatcherID.byRegexp, options: '/\\b(?:\\S+?\\.)+\\S+\\b$/', }; const matcher = getFieldMatcher(config); let resultCount = 0; for (const field of seriesWithNames.fields) { - if (matcher(field)) { + if (matcher(field, seriesWithNames, [seriesWithNames])) { resultCount++; } expect(resultCount).toBe(1); } }); }); + +describe('Field Name Matcher', () => { + it('Match only exact name', () => { + const seriesWithNames = toDataFrame({ + fields: [{ name: 'A hello world' }, { name: 'AAA' }, { name: 'C' }], + }); + const config = { + id: FieldMatcherID.byName, + options: 'C', + }; + + const matcher = getFieldMatcher(config); + + for (const field of seriesWithNames.fields) { + const didMatch = matcher(field, seriesWithNames, [seriesWithNames]); + expect(didMatch).toBe(field.name === 'C'); + } + }); + + it('Match should respect letter case', () => { + const seriesWithNames = toDataFrame({ + fields: [{ name: '12' }, { name: '112' }, { name: '13' }, { name: 'C' }], + }); + const config = { + id: FieldMatcherID.byName, + options: 'c', + }; + + const matcher = getFieldMatcher(config); + + for (const field of seriesWithNames.fields) { + expect(matcher(field, seriesWithNames, [seriesWithNames])).toBe(false); + } + }); + + it('Match none of the field names', () => { + const seriesWithNames = toDataFrame({ + fields: [{ name: 'some.instance.path' }, { name: '112' }, { name: '13' }], + }); + const config = { + id: FieldMatcherID.byName, + options: '', + }; + + const matcher = getFieldMatcher(config); + + for (const field of seriesWithNames.fields) { + expect(matcher(field, seriesWithNames, [seriesWithNames])).toBe(false); + } + }); +}); + +describe('Field Multiple Names Matcher', () => { + it('Match only exact name', () => { + const seriesWithNames = toDataFrame({ + fields: [{ name: 'A hello world' }, { name: 'AAA' }, { name: 'C' }], + }); + const config = { + id: FieldMatcherID.byNames, + options: ['C'], + }; + + const matcher = getFieldMatcher(config); + + for (const field of seriesWithNames.fields) { + const didMatch = matcher(field, seriesWithNames, [seriesWithNames]); + expect(didMatch).toBe(field.name === 'C'); + } + }); + + it('Match should respect letter case', () => { + const seriesWithNames = toDataFrame({ + fields: [{ name: '12' }, { name: '112' }, { name: '13' }, { name: 'C' }], + }); + const config = { + id: FieldMatcherID.byNames, + options: ['c'], + }; + + const matcher = getFieldMatcher(config); + + for (const field of seriesWithNames.fields) { + expect(matcher(field, seriesWithNames, [seriesWithNames])).toBe(false); + } + }); + + it('Match none of the field names', () => { + const seriesWithNames = toDataFrame({ + fields: [{ name: 'some.instance.path' }, { name: '112' }, { name: '13' }], + }); + const config = { + id: FieldMatcherID.byNames, + options: [], + }; + + const matcher = getFieldMatcher(config); + + for (const field of seriesWithNames.fields) { + expect(matcher(field, seriesWithNames, [seriesWithNames])).toBe(false); + } + }); + + it('Match all of the field names', () => { + const seriesWithNames = toDataFrame({ + fields: [{ name: 'some.instance.path' }, { name: '112' }, { name: '13' }], + }); + const config = { + id: FieldMatcherID.byNames, + options: ['some.instance.path', '112', '13'], + }; + + const matcher = getFieldMatcher(config); + + for (const field of seriesWithNames.fields) { + expect(matcher(field, seriesWithNames, [seriesWithNames])).toBe(true); + } + }); +}); + +describe('Field Regexp or Names Matcher', () => { + it('Match only exact name by name', () => { + const seriesWithNames = toDataFrame({ + fields: [{ name: 'A hello world' }, { name: 'AAA' }, { name: 'C' }], + }); + const config = { + id: FieldMatcherID.byRegexpOrNames, + options: { + names: ['C'], + }, + }; + + const matcher = getFieldMatcher(config); + + for (const field of seriesWithNames.fields) { + const didMatch = matcher(field, seriesWithNames, [seriesWithNames]); + expect(didMatch).toBe(field.name === 'C'); + } + }); + + it('Match all starting with AA', () => { + const seriesWithNames = toDataFrame({ + fields: [{ name: 'A hello world' }, { name: 'AAA' }, { name: 'C' }], + }); + const config = { + id: FieldMatcherID.byRegexpOrNames, + options: { + pattern: '/^AA/', + }, + }; + + const matcher = getFieldMatcher(config); + + for (const field of seriesWithNames.fields) { + const didMatch = matcher(field, seriesWithNames, [seriesWithNames]); + expect(didMatch).toBe(field.name === 'AAA'); + } + }); + + it('Match all starting with AA and C', () => { + const seriesWithNames = toDataFrame({ + fields: [{ name: 'A hello world' }, { name: 'AAA' }, { name: 'C' }], + }); + const config = { + id: FieldMatcherID.byRegexpOrNames, + options: { + pattern: '/^AA/', + names: ['C'], + }, + }; + + const matcher = getFieldMatcher(config); + + for (const field of seriesWithNames.fields) { + const didMatch = matcher(field, seriesWithNames, [seriesWithNames]); + expect(didMatch).toBe(field.name === 'AAA' || field.name === 'C'); + } + }); + + it('Match should respect letter case by name if not igored in pattern', () => { + const seriesWithNames = toDataFrame({ + fields: [{ name: '12' }, { name: '112' }, { name: '13' }, { name: 'C' }], + }); + const config = { + id: FieldMatcherID.byRegexpOrNames, + options: { + names: ['c'], + pattern: '/c/i', + }, + }; + + const matcher = getFieldMatcher(config); + + for (const field of seriesWithNames.fields) { + const didMatch = matcher(field, seriesWithNames, [seriesWithNames]); + expect(didMatch).toBe(field.name === 'C'); + } + }); + + it('Match none of the field names by name', () => { + const seriesWithNames = toDataFrame({ + fields: [{ name: 'some.instance.path' }, { name: '112' }, { name: '13' }], + }); + const config = { + id: FieldMatcherID.byRegexpOrNames, + options: { + names: [], + }, + }; + + const matcher = getFieldMatcher(config); + + for (const field of seriesWithNames.fields) { + expect(matcher(field, seriesWithNames, [seriesWithNames])).toBe(false); + } + }); + + it('Match all of the field names by name', () => { + const seriesWithNames = toDataFrame({ + fields: [{ name: 'some.instance.path' }, { name: '112' }, { name: '13' }], + }); + const config = { + id: FieldMatcherID.byRegexpOrNames, + options: { + names: ['some.instance.path', '112', '13'], + }, + }; + + const matcher = getFieldMatcher(config); + + for (const field of seriesWithNames.fields) { + expect(matcher(field, seriesWithNames, [seriesWithNames])).toBe(true); + } + }); + + it('Match all of the field names by regexp', () => { + const seriesWithNames = toDataFrame({ + fields: [{ name: 'some.instance.path' }, { name: '112' }, { name: '13' }], + }); + const config = { + id: FieldMatcherID.byRegexpOrNames, + options: { + pattern: '/.*/', + }, + }; + + const matcher = getFieldMatcher(config); + + for (const field of seriesWithNames.fields) { + expect(matcher(field, seriesWithNames, [seriesWithNames])).toBe(true); + } + }); +}); diff --git a/packages/grafana-data/src/transformations/matchers/nameMatcher.ts b/packages/grafana-data/src/transformations/matchers/nameMatcher.ts index 34a24c89007..b09976cfa27 100644 --- a/packages/grafana-data/src/transformations/matchers/nameMatcher.ts +++ b/packages/grafana-data/src/transformations/matchers/nameMatcher.ts @@ -1,35 +1,111 @@ import { Field, DataFrame } from '../../types/dataFrame'; import { FieldMatcherID, FrameMatcherID } from './ids'; -import { FieldMatcherInfo, FrameMatcherInfo } from '../../types/transformations'; +import { FieldMatcherInfo, FrameMatcherInfo, FieldMatcher } from '../../types/transformations'; import { stringToJsRegex } from '../../text/string'; import { getFieldDisplayName } from '../../field/fieldState'; +export interface RegexpOrNamesMatcherOptions { + pattern?: string; + names?: string[]; +} + // General Field matcher -const fieldNameMacher: FieldMatcherInfo = { +const fieldNameMatcher: FieldMatcherInfo = { id: FieldMatcherID.byName, name: 'Field Name', description: 'match the field name', - defaultOptions: '/.*/', + defaultOptions: '', - get: (pattern: string) => { - let regex = new RegExp(''); - try { - regex = stringToJsRegex(pattern); - } catch (e) { - console.error(e); - } - return (field: Field) => { - return regex.test(getFieldDisplayName(field) ?? ''); + get: (name: string): FieldMatcher => { + return (field: Field, frame: DataFrame, allFrames: DataFrame[]) => { + return getFieldDisplayName(field, frame, allFrames) === name; }; }, - getOptionsDisplayText: (pattern: string) => { - return `Field name: ${pattern}`; + getOptionsDisplayText: (name: string) => { + return `Field name: ${name}`; }, }; -// General Field matcher -const frameNameMacher: FrameMatcherInfo = { +const multipleFieldNamesMatcher: FieldMatcherInfo = { + id: FieldMatcherID.byNames, + name: 'Field Names', + description: 'match any of the given the field names', + defaultOptions: [], + + get: (names: string[]): FieldMatcher => { + const uniqueNames = new Set(names ?? []); + + return (field: Field, frame: DataFrame, allFrames: DataFrame[]) => { + return uniqueNames.has(getFieldDisplayName(field, frame, allFrames)); + }; + }, + + getOptionsDisplayText: (names: string[]): string => { + return `Field names: ${names.join(', ')}`; + }, +}; + +const regexpFieldNameMatcher: FieldMatcherInfo = { + id: FieldMatcherID.byRegexp, + name: 'Field Name by Regexp', + description: 'match the field name by a given regexp pattern', + defaultOptions: '/.*/', + + get: (pattern: string): FieldMatcher => { + const regexp = patternToRegex(pattern); + + return (field: Field, frame: DataFrame, allFrames: DataFrame[]) => { + const displayName = getFieldDisplayName(field, frame, allFrames); + return !!regexp && regexp.test(displayName); + }; + }, + + getOptionsDisplayText: (pattern: string): string => { + return `Field name by pattern: ${pattern}`; + }, +}; + +const regexpOrMultipleNamesMatcher: FieldMatcherInfo = { + id: FieldMatcherID.byRegexpOrNames, + name: 'Field Name by Regexp or Names', + description: 'match the field name by a given regexp pattern or given names', + defaultOptions: { + pattern: '/.*/', + names: [], + }, + + get: (options: RegexpOrNamesMatcherOptions): FieldMatcher => { + const regexpMatcher = regexpFieldNameMatcher.get(options?.pattern || ''); + const namesMatcher = multipleFieldNamesMatcher.get(options?.names ?? []); + + return (field: Field, frame: DataFrame, allFrames: DataFrame[]) => { + return namesMatcher(field, frame, allFrames) || regexpMatcher(field, frame, allFrames); + }; + }, + + getOptionsDisplayText: (options: RegexpOrNamesMatcherOptions): string => { + const pattern = options?.pattern ?? ''; + const names = options?.names?.join(',') ?? ''; + return `Field name by pattern: ${pattern} or names: ${names}`; + }, +}; + +const patternToRegex = (pattern?: string): RegExp | undefined => { + if (!pattern) { + return undefined; + } + + try { + return stringToJsRegex(pattern); + } catch (error) { + console.log(error); + return undefined; + } +}; + +// General Frame matcher +const frameNameMatcher: FrameMatcherInfo = { id: FrameMatcherID.byName, name: 'Frame Name', description: 'match the frame name', @@ -51,9 +127,9 @@ const frameNameMacher: FrameMatcherInfo = { * Registry Initalization */ export function getFieldNameMatchers(): FieldMatcherInfo[] { - return [fieldNameMacher]; + return [fieldNameMatcher, regexpFieldNameMatcher, multipleFieldNamesMatcher, regexpOrMultipleNamesMatcher]; } export function getFrameNameMatchers(): FrameMatcherInfo[] { - return [frameNameMacher]; + return [frameNameMatcher]; } diff --git a/packages/grafana-data/src/transformations/matchers/predicates.test.ts b/packages/grafana-data/src/transformations/matchers/predicates.test.ts index cb4fd1909c1..ef007b5dc28 100644 --- a/packages/grafana-data/src/transformations/matchers/predicates.test.ts +++ b/packages/grafana-data/src/transformations/matchers/predicates.test.ts @@ -13,26 +13,29 @@ const matchesTimeConfig: MatcherConfig = { options: FieldType.time, }; const both = [matchesNumberConfig, matchesTimeConfig]; +const allFrames = [simpleSeriesWithTypes]; describe('Check Predicates', () => { it('can not match both', () => { const matches = fieldMatchers.get(MatcherID.allMatch).get(both); for (const field of simpleSeriesWithTypes.fields) { - expect(matches(field)).toBe(false); + expect(matches(field, simpleSeriesWithTypes, allFrames)).toBe(false); } }); it('match either time or number', () => { const matches = fieldMatchers.get(MatcherID.anyMatch).get(both); for (const field of simpleSeriesWithTypes.fields) { - expect(matches(field)).toBe(field.type === FieldType.number || field.type === FieldType.time); + expect(matches(field, simpleSeriesWithTypes, allFrames)).toBe( + field.type === FieldType.number || field.type === FieldType.time + ); } }); it('match not time', () => { const matches = fieldMatchers.get(MatcherID.invertMatch).get(matchesTimeConfig); for (const field of simpleSeriesWithTypes.fields) { - expect(matches(field)).toBe(field.type !== FieldType.time); + expect(matches(field, simpleSeriesWithTypes, allFrames)).toBe(field.type !== FieldType.time); } }); }); diff --git a/packages/grafana-data/src/transformations/matchers/predicates.ts b/packages/grafana-data/src/transformations/matchers/predicates.ts index e122b84ab3c..1cb28b4a684 100644 --- a/packages/grafana-data/src/transformations/matchers/predicates.ts +++ b/packages/grafana-data/src/transformations/matchers/predicates.ts @@ -14,9 +14,9 @@ const anyFieldMatcher: FieldMatcherInfo = { const children = options.map(option => { return getFieldMatcher(option); }); - return (field: Field) => { + return (field: Field, frame: DataFrame, allFrames: DataFrame[]) => { for (const child of children) { - if (child(field)) { + if (child(field, frame, allFrames)) { return true; } } @@ -82,9 +82,9 @@ const allFieldsMatcher: FieldMatcherInfo = { const children = options.map(option => { return getFieldMatcher(option); }); - return (field: Field) => { + return (field: Field, frame: DataFrame, allFrames: DataFrame[]) => { for (const child of children) { - if (!child(field)) { + if (!child(field, frame, allFrames)) { return false; } } @@ -147,8 +147,8 @@ const notFieldMatcher: FieldMatcherInfo = { get: (option: MatcherConfig) => { const check = getFieldMatcher(option); - return (field: Field) => { - return !check(field); + return (field: Field, frame: DataFrame, allFrames: DataFrame[]) => { + return !check(field, frame, allFrames); }; }, diff --git a/packages/grafana-data/src/transformations/transformers.ts b/packages/grafana-data/src/transformations/transformers.ts index 910d7751cbe..aad35cfcd68 100644 --- a/packages/grafana-data/src/transformations/transformers.ts +++ b/packages/grafana-data/src/transformations/transformers.ts @@ -10,6 +10,7 @@ import { organizeFieldsTransformer } from './transformers/organize'; import { seriesToColumnsTransformer } from './transformers/seriesToColumns'; import { renameFieldsTransformer } from './transformers/rename'; import { labelsToFieldsTransformer } from './transformers/labelsToFields'; +import { ensureColumnsTransformer } from './transformers/ensureColumns'; export const standardTransformers = { noopTransformer, @@ -25,4 +26,5 @@ export const standardTransformers = { seriesToColumnsTransformer, renameFieldsTransformer, labelsToFieldsTransformer, + ensureColumnsTransformer, }; diff --git a/packages/grafana-data/src/transformations/transformers/calculateField.test.ts b/packages/grafana-data/src/transformations/transformers/calculateField.test.ts index 004b2e57316..7cf684c2aa1 100644 --- a/packages/grafana-data/src/transformations/transformers/calculateField.test.ts +++ b/packages/grafana-data/src/transformations/transformers/calculateField.test.ts @@ -4,7 +4,7 @@ import { FieldType } from '../../types/dataFrame'; import { ReducerID } from '../fieldReducer'; import { mockTransformationsRegistry } from '../../utils/tests/mockTransformationsRegistry'; import { transformDataFrame } from '../transformDataFrame'; -import { calculateFieldTransformer, CalculateFieldMode } from './calculateField'; +import { calculateFieldTransformer, CalculateFieldMode, ReduceOptions } from './calculateField'; import { DataFrameView } from '../../dataframe'; import { BinaryOperationID } from '../../utils'; @@ -96,9 +96,9 @@ describe('calculateField transformer w/ timeseries', () => { options: { mode: CalculateFieldMode.ReduceRow, reduce: { - include: 'B', + include: ['B'], reducer: ReducerID.mean, - }, + } as ReduceOptions, replaceFields: true, }, }; diff --git a/packages/grafana-data/src/transformations/transformers/calculateField.ts b/packages/grafana-data/src/transformations/transformers/calculateField.ts index 6f481a470a6..919119b2f43 100644 --- a/packages/grafana-data/src/transformations/transformers/calculateField.ts +++ b/packages/grafana-data/src/transformations/transformers/calculateField.ts @@ -6,10 +6,10 @@ import { FieldMatcherID } from '../matchers/ids'; import { RowVector } from '../../vector/RowVector'; import { ArrayVector, BinaryOperationVector, ConstantVector } from '../../vector'; import { doStandardCalcs } from '../fieldReducer'; -import { seriesToColumnsTransformer } from './seriesToColumns'; import { getTimeField } from '../../dataframe/processDataFrame'; import defaults from 'lodash/defaults'; import { BinaryOperationID, binaryOperators } from '../../utils/binaryOperators'; +import { ensureColumnsTransformer } from './ensureColumns'; import { getFieldDisplayName } from '../../field'; export enum CalculateFieldMode { @@ -18,7 +18,7 @@ export enum CalculateFieldMode { } export interface ReduceOptions { - include?: string; // Assume all fields + include?: string[]; // Assume all fields reducer: ReducerID; nullValueMode?: NullValueMode; } @@ -69,22 +69,17 @@ export const calculateFieldTransformer: DataTransformerInfo (data: DataFrame[]) => { - // Assume timeseries should first be joined by time - const timeFieldName = findConsistentTimeFieldName(data); - - if (data.length > 1 && timeFieldName && options.timeSeries !== false) { - data = seriesToColumnsTransformer.transformer({ - byField: timeFieldName, - })(data); + if (options && options.timeSeries !== false) { + data = ensureColumnsTransformer.transformer(null)(data); } const mode = options.mode ?? CalculateFieldMode.ReduceRow; let creator: ValuesCreator | undefined = undefined; if (mode === CalculateFieldMode.ReduceRow) { - creator = getReduceRowCreator(defaults(options.reduce, defaultReduceOptions)); + creator = getReduceRowCreator(defaults(options.reduce, defaultReduceOptions), data); } else if (mode === CalculateFieldMode.BinaryOperation) { - creator = getBinaryCreator(defaults(options.binary, defaultBinaryOptions)); + creator = getBinaryCreator(defaults(options.binary, defaultBinaryOptions), data); } // Nothing configured @@ -126,14 +121,14 @@ export const calculateFieldTransformer: DataTransformerInfo { - const left = findFieldValuesWithNameOrConstant(frame, options.left); - const right = findFieldValuesWithNameOrConstant(frame, options.right); + const left = findFieldValuesWithNameOrConstant(frame, options.left, allFrames); + const right = findFieldValuesWithNameOrConstant(frame, options.right, allFrames); if (!left || !right || !operator) { return (undefined as unknown) as Vector; } @@ -210,26 +205,6 @@ function getBinaryCreator(options: BinaryOptions): ValuesCreator { }; } -/** - * Find the name for the time field used in all frames (if one exists) - */ -function findConsistentTimeFieldName(data: DataFrame[]): string | undefined { - let name: string | undefined = undefined; - for (const frame of data) { - const { timeField } = getTimeField(frame); - if (!timeField) { - return undefined; // Not timeseries - } - if (!name) { - name = timeField.name; - } else if (name !== timeField.name) { - // Second frame has a different time column?! - return undefined; - } - } - return name; -} - export function getNameFromOptions(options: CalculateFieldTransformerOptions) { if (options.alias?.length) { return options.alias; diff --git a/packages/grafana-data/src/transformations/transformers/ensureColumns.test.ts b/packages/grafana-data/src/transformations/transformers/ensureColumns.test.ts new file mode 100644 index 00000000000..b0301c205e7 --- /dev/null +++ b/packages/grafana-data/src/transformations/transformers/ensureColumns.test.ts @@ -0,0 +1,136 @@ +import { DataTransformerID } from './ids'; +import { toDataFrame } from '../../dataframe/processDataFrame'; +import { FieldType } from '../../types/dataFrame'; +import { mockTransformationsRegistry } from '../../utils/tests/mockTransformationsRegistry'; +import { transformDataFrame } from '../transformDataFrame'; +import { ensureColumnsTransformer } from './ensureColumns'; +import { seriesToColumnsTransformer } from './seriesToColumns'; + +const seriesA = toDataFrame({ + fields: [ + { name: 'TheTime', type: FieldType.time, values: [1000, 2000] }, + { name: 'A', type: FieldType.number, values: [1, 100] }, + ], +}); + +const seriesBC = toDataFrame({ + fields: [ + { name: 'TheTime', type: FieldType.time, values: [1000, 2000] }, + { name: 'B', type: FieldType.number, values: [2, 200] }, + { name: 'C', type: FieldType.number, values: [3, 300] }, + { name: 'D', type: FieldType.string, values: ['first', 'second'] }, + ], +}); + +const seriesNoTime = toDataFrame({ + fields: [ + { name: 'B', type: FieldType.number, values: [2, 200] }, + { name: 'C', type: FieldType.number, values: [3, 300] }, + { name: 'D', type: FieldType.string, values: ['first', 'second'] }, + ], +}); + +describe('ensureColumns transformer', () => { + beforeAll(() => { + mockTransformationsRegistry([ensureColumnsTransformer, seriesToColumnsTransformer]); + }); + + it('will transform to columns if time field exists and multiple frames', () => { + const cfg = { + id: DataTransformerID.ensureColumns, + options: {}, + }; + + const data = [seriesA, seriesBC]; + const filtered = transformDataFrame([cfg], data); + + expect(filtered.length).toEqual(1); + expect(filtered[0]).toMatchInlineSnapshot(` + Object { + "fields": Array [ + Object { + "config": Object {}, + "labels": undefined, + "name": "TheTime", + "type": "time", + "values": Array [ + 1000, + 2000, + ], + }, + Object { + "config": Object {}, + "labels": Object {}, + "name": "A", + "type": "number", + "values": Array [ + 1, + 100, + ], + }, + Object { + "config": Object {}, + "labels": Object {}, + "name": "B", + "type": "number", + "values": Array [ + 2, + 200, + ], + }, + Object { + "config": Object {}, + "labels": Object {}, + "name": "C", + "type": "number", + "values": Array [ + 3, + 300, + ], + }, + Object { + "config": Object {}, + "labels": Object {}, + "name": "D", + "type": "string", + "values": Array [ + "first", + "second", + ], + }, + ], + "meta": Object { + "transformations": Array [ + "ensureColumns", + ], + }, + "name": undefined, + "refId": undefined, + } + `); + }); + + it('will not transform to columns if time field is missing for any of the series', () => { + const cfg = { + id: DataTransformerID.ensureColumns, + options: {}, + }; + + const data = [seriesBC, seriesNoTime]; + const filtered = transformDataFrame([cfg], data); + + expect(filtered).toEqual(data); + }); + + it('will not transform to columns if only one series', () => { + const cfg = { + id: DataTransformerID.ensureColumns, + options: {}, + }; + + const data = [seriesBC]; + const filtered = transformDataFrame([cfg], data); + + expect(filtered).toEqual(data); + }); +}); diff --git a/packages/grafana-data/src/transformations/transformers/ensureColumns.ts b/packages/grafana-data/src/transformations/transformers/ensureColumns.ts new file mode 100644 index 00000000000..a5eb8b6fdc7 --- /dev/null +++ b/packages/grafana-data/src/transformations/transformers/ensureColumns.ts @@ -0,0 +1,43 @@ +import { seriesToColumnsTransformer } from './seriesToColumns'; +import { DataFrame } from '../../types/dataFrame'; +import { getTimeField } from '../../dataframe/processDataFrame'; +import { DataTransformerInfo } from '../../types/transformations'; +import { DataTransformerID } from './ids'; + +export const ensureColumnsTransformer: DataTransformerInfo = { + id: DataTransformerID.ensureColumns, + name: 'Ensure Columns Transformer', + description: 'Will check if current data frames is series or columns. If in series it will convert to columns.', + transformer: () => (data: DataFrame[]) => { + // Assume timeseries should first be joined by time + const timeFieldName = findConsistentTimeFieldName(data); + + if (data.length > 1 && timeFieldName) { + return seriesToColumnsTransformer.transformer({ + byField: timeFieldName, + })(data); + } + + return data; + }, +}; + +/** + * Find the name for the time field used in all frames (if one exists) + */ +function findConsistentTimeFieldName(data: DataFrame[]): string | undefined { + let name: string | undefined = undefined; + for (const frame of data) { + const { timeField } = getTimeField(frame); + if (!timeField) { + return undefined; // Not timeseries + } + if (!name) { + name = timeField.name; + } else if (name !== timeField.name) { + // Second frame has a different time column?! + return undefined; + } + } + return name; +} diff --git a/packages/grafana-data/src/transformations/transformers/filter.ts b/packages/grafana-data/src/transformations/transformers/filter.ts index 7a1638d2b75..4eb09a9373f 100644 --- a/packages/grafana-data/src/transformations/transformers/filter.ts +++ b/packages/grafana-data/src/transformations/transformers/filter.ts @@ -34,15 +34,16 @@ export const filterFieldsTransformer: DataTransformerInfo = { const fields: Field[] = []; for (let i = 0; i < series.fields.length; i++) { const field = series.fields[i]; + if (exclude) { - if (exclude(field)) { + if (exclude(field, series, data)) { continue; } if (!include) { fields.push(field); } } - if (include && include(field)) { + if (include && include(field, series, data)) { fields.push(field); } } diff --git a/packages/grafana-data/src/transformations/transformers/filterByName.test.ts b/packages/grafana-data/src/transformations/transformers/filterByName.test.ts index d6dc8d19599..90e6690d88e 100644 --- a/packages/grafana-data/src/transformations/transformers/filterByName.test.ts +++ b/packages/grafana-data/src/transformations/transformers/filterByName.test.ts @@ -31,11 +31,13 @@ describe('filterByName transformer', () => { }); describe('respects', () => { - it('inclusion', () => { + it('inclusion by pattern', () => { const cfg = { id: DataTransformerID.filterFieldsByName, options: { - include: ['^(startsWith)'], + include: { + pattern: '/^(startsWith)/', + }, }, }; @@ -44,11 +46,13 @@ describe('filterByName transformer', () => { expect(filtered.fields[0].name).toBe('startsWithA'); }); - it('exclusion', () => { + it('exclusion by pattern', () => { const cfg = { id: DataTransformerID.filterFieldsByName, options: { - exclude: ['^(startsWith)'], + exclude: { + pattern: '/^(startsWith)/', + }, }, }; @@ -57,12 +61,102 @@ describe('filterByName transformer', () => { expect(filtered.fields[0].name).toBe('B'); }); - it('inclusion and exclusion', () => { + it('inclusion and exclusion by pattern', () => { const cfg = { id: DataTransformerID.filterFieldsByName, options: { - exclude: ['^(startsWith)'], - include: [`^(B)$`], + exclude: { pattern: '/^(startsWith)/' }, + include: { pattern: '/^(B)$/' }, + }, + }; + + const filtered = transformDataFrame([cfg], [seriesWithNamesToMatch])[0]; + expect(filtered.fields.length).toBe(1); + expect(filtered.fields[0].name).toBe('B'); + }); + + it('inclusion by names', () => { + const cfg = { + id: DataTransformerID.filterFieldsByName, + options: { + include: { + names: ['startsWithA', 'startsWithC'], + }, + }, + }; + + const filtered = transformDataFrame([cfg], [seriesWithNamesToMatch])[0]; + expect(filtered.fields.length).toBe(2); + expect(filtered.fields[0].name).toBe('startsWithA'); + }); + + it('exclusion by names', () => { + const cfg = { + id: DataTransformerID.filterFieldsByName, + options: { + exclude: { + names: ['startsWithA', 'startsWithC'], + }, + }, + }; + + const filtered = transformDataFrame([cfg], [seriesWithNamesToMatch])[0]; + expect(filtered.fields.length).toBe(2); + expect(filtered.fields[0].name).toBe('B'); + }); + + it('inclusion and exclusion by names', () => { + const cfg = { + id: DataTransformerID.filterFieldsByName, + options: { + exclude: { names: ['startsWithA', 'startsWithC'] }, + include: { names: ['B'] }, + }, + }; + + const filtered = transformDataFrame([cfg], [seriesWithNamesToMatch])[0]; + expect(filtered.fields.length).toBe(1); + expect(filtered.fields[0].name).toBe('B'); + }); + + it('inclusion by both', () => { + const cfg = { + id: DataTransformerID.filterFieldsByName, + options: { + include: { + pattern: '/^(startsWith)/', + names: ['startsWithA'], + }, + }, + }; + + const filtered = transformDataFrame([cfg], [seriesWithNamesToMatch])[0]; + expect(filtered.fields.length).toBe(2); + expect(filtered.fields[0].name).toBe('startsWithA'); + }); + + it('exclusion by both', () => { + const cfg = { + id: DataTransformerID.filterFieldsByName, + options: { + exclude: { + pattern: '/^(startsWith)/', + names: ['startsWithA'], + }, + }, + }; + + const filtered = transformDataFrame([cfg], [seriesWithNamesToMatch])[0]; + expect(filtered.fields.length).toBe(2); + expect(filtered.fields[0].name).toBe('B'); + }); + + it('inclusion and exclusion by both', () => { + const cfg = { + id: DataTransformerID.filterFieldsByName, + options: { + exclude: { names: ['startsWithA', 'startsWithC'] }, + include: { pattern: '/^(B)$/' }, }, }; diff --git a/packages/grafana-data/src/transformations/transformers/filterByName.ts b/packages/grafana-data/src/transformations/transformers/filterByName.ts index 80374a8e15b..ba4ce2568d6 100644 --- a/packages/grafana-data/src/transformations/transformers/filterByName.ts +++ b/packages/grafana-data/src/transformations/transformers/filterByName.ts @@ -1,11 +1,12 @@ import { DataTransformerID } from './ids'; -import { filterFieldsTransformer, FilterOptions } from './filter'; -import { DataTransformerInfo } from '../../types/transformations'; +import { DataTransformerInfo, MatcherConfig } from '../../types/transformations'; import { FieldMatcherID } from '../matchers/ids'; +import { FilterOptions, filterFieldsTransformer } from './filter'; +import { RegexpOrNamesMatcherOptions } from '../matchers/nameMatcher'; export interface FilterFieldsByNameTransformerOptions { - include?: string[]; - exclude?: string[]; + include?: RegexpOrNamesMatcherOptions; + exclude?: RegexpOrNamesMatcherOptions; } export const filterFieldsByNameTransformer: DataTransformerInfo = { @@ -19,25 +20,33 @@ export const filterFieldsByNameTransformer: DataTransformerInfo { - const filterOptions: FilterOptions = {}; - if (options.include) { - filterOptions.include = { - id: FieldMatcherID.byName, - options: options.include.length > 0 ? buildRegex(options.include) : '', - }; - } - if (options.exclude) { - filterOptions.exclude = { - id: FieldMatcherID.byName, - options: options.exclude.length > 0 ? buildRegex(options.exclude) : '', - }; - } + const filterOptions: FilterOptions = { + include: getMatcherConfig(options.include), + exclude: getMatcherConfig(options.exclude), + }; return filterFieldsTransformer.transformer(filterOptions); }, }; -const buildRegex = (regexs: string[]) => { - const include = regexs.map(s => `(${s})`).join('|'); - return `/${include}/`; +const getMatcherConfig = (options?: RegexpOrNamesMatcherOptions): MatcherConfig | undefined => { + if (!options) { + return undefined; + } + + const { names, pattern } = options; + + if ((!Array.isArray(names) || names.length === 0) && !pattern) { + return undefined; + } + + if (!pattern) { + return { id: FieldMatcherID.byNames, options: names }; + } + + if (!Array.isArray(names) || names.length === 0) { + return { id: FieldMatcherID.byRegexp, options: pattern }; + } + + return { id: FieldMatcherID.byRegexpOrNames, options }; }; diff --git a/packages/grafana-data/src/transformations/transformers/ids.ts b/packages/grafana-data/src/transformations/transformers/ids.ts index 11847973d32..da2c561227b 100644 --- a/packages/grafana-data/src/transformations/transformers/ids.ts +++ b/packages/grafana-data/src/transformations/transformers/ids.ts @@ -1,18 +1,18 @@ export enum DataTransformerID { // join = 'join', // Pick a field and merge all series based on that field - append = 'append', // Merge all series together + append = 'append', // rotate = 'rotate', // Columns to rows - reduce = 'reduce', // Run calculations on fields - order = 'order', // order fields based on user configuration - organize = 'organize', // order, rename and filter based on user configuration - rename = 'rename', // rename field based on user configuration - calculateField = 'calculateField', // Run a reducer on the row - - seriesToColumns = 'seriesToColumns', // former table transform timeseries_to_columns - labelsToFields = 'labelsToFields', // former table transform table - filterFields = 'filterFields', // Pick some fields (keep all frames) - filterFieldsByName = 'filterFieldsByName', // Pick fields with name matching regex (keep all frames) - filterFrames = 'filterFrames', // Pick some frames (keep all fields) - filterByRefId = 'filterByRefId', // Pick some frames by RefId - noop = 'noop', // Does nothing to the dataframe + reduce = 'reduce', + order = 'order', + organize = 'organize', + rename = 'rename', + calculateField = 'calculateField', + seriesToColumns = 'seriesToColumns', + labelsToFields = 'labelsToFields', + filterFields = 'filterFields', + filterFieldsByName = 'filterFieldsByName', + filterFrames = 'filterFrames', + filterByRefId = 'filterByRefId', + noop = 'noop', + ensureColumns = 'ensureColumns', } diff --git a/packages/grafana-data/src/transformations/transformers/organize.ts b/packages/grafana-data/src/transformations/transformers/organize.ts index 5c06d5cc36c..990d12e171f 100644 --- a/packages/grafana-data/src/transformations/transformers/organize.ts +++ b/packages/grafana-data/src/transformations/transformers/organize.ts @@ -29,7 +29,7 @@ export const organizeFieldsTransformer: DataTransformerInfo rename(order(filter(data))); diff --git a/packages/grafana-data/src/transformations/transformers/reduce.ts b/packages/grafana-data/src/transformations/transformers/reduce.ts index b5691f1fd5b..23376cb2b99 100644 --- a/packages/grafana-data/src/transformations/transformers/reduce.ts +++ b/packages/grafana-data/src/transformations/transformers/reduce.ts @@ -72,7 +72,7 @@ export const reduceTransformer: DataTransformerInfo = continue; } - if (matcher(field)) { + if (matcher(field, series, data)) { const results = reduceField({ field, reducers, diff --git a/packages/grafana-data/src/types/transformations.ts b/packages/grafana-data/src/types/transformations.ts index d0198f59795..98b379d2880 100644 --- a/packages/grafana-data/src/types/transformations.ts +++ b/packages/grafana-data/src/types/transformations.ts @@ -25,7 +25,7 @@ export interface DataTransformerConfig { options: TOptions; } -export type FieldMatcher = (field: Field) => boolean; +export type FieldMatcher = (field: Field, frame: DataFrame, allFrames: DataFrame[]) => boolean; export type FrameMatcher = (frame: DataFrame) => boolean; export interface FieldMatcherInfo extends RegistryItemWithOptions { diff --git a/packages/grafana-ui/src/components/MatchersUI/FieldNameMatcherEditor.tsx b/packages/grafana-ui/src/components/MatchersUI/FieldNameMatcherEditor.tsx index 79f8ee22cb1..b8904629d16 100644 --- a/packages/grafana-ui/src/components/MatchersUI/FieldNameMatcherEditor.tsx +++ b/packages/grafana-ui/src/components/MatchersUI/FieldNameMatcherEditor.tsx @@ -1,32 +1,26 @@ -import React from 'react'; +import React, { memo, useMemo, useCallback } from 'react'; import { MatcherUIProps, FieldMatcherUIRegistryItem } from './types'; -import { FieldMatcherID, fieldMatchers, getFieldDisplayName } from '@grafana/data'; +import { FieldMatcherID, fieldMatchers, getFieldDisplayName, SelectableValue, DataFrame } from '@grafana/data'; import { Select } from '../Select/Select'; -export class FieldNameMatcherEditor extends React.PureComponent> { - render() { - const { data, options, onChange } = this.props; - const names: Set = new Set(); +export const FieldNameMatcherEditor = memo>(props => { + const { data, options } = props; + const names = useFieldDisplayNames(data); + const selectOptions = useSelectOptions(names); - for (const frame of data) { - for (const field of frame.fields) { - names.add(getFieldDisplayName(field, frame, data)); + const onChange = useCallback( + (selection: SelectableValue) => { + if (!selection.value || !names.has(selection.value)) { + return; } - } - if (options) { - names.add(options); - } - const selectOptions = Array.from(names).map(n => ({ - value: n, - label: n, - })); - const selectedOption = selectOptions.find(v => v.value === options); + return props.onChange(selection.value); + }, + [names, props.onChange] + ); - return ( - ; +}); export const fieldNameMatcherItem: FieldMatcherUIRegistryItem = { id: FieldMatcherID.byName, @@ -35,3 +29,26 @@ export const fieldNameMatcherItem: FieldMatcherUIRegistryItem = { name: 'Filter by field', description: 'Set properties for fields matching the name', }; + +const useFieldDisplayNames = (data: DataFrame[]): Set => { + return useMemo(() => { + const names: Set = new Set(); + + for (const frame of data) { + for (const field of frame.fields) { + names.add(getFieldDisplayName(field, frame, data)); + } + } + + return names; + }, [data]); +}; + +const useSelectOptions = (displayNames: Set): Array> => { + return useMemo(() => { + return Array.from(displayNames).map(n => ({ + value: n, + label: n, + })); + }, [displayNames]); +}; diff --git a/public/app/core/components/TransformersUI/CalculateFieldTransformerEditor.tsx b/public/app/core/components/TransformersUI/CalculateFieldTransformerEditor.tsx index c1c8ef52f5b..5f4b8b603c4 100644 --- a/public/app/core/components/TransformersUI/CalculateFieldTransformerEditor.tsx +++ b/public/app/core/components/TransformersUI/CalculateFieldTransformerEditor.tsx @@ -27,7 +27,7 @@ import defaults from 'lodash/defaults'; interface CalculateFieldTransformerEditorProps extends TransformerUIProps {} interface CalculateFieldTransformerEditorState { - include: string; + include: string[]; names: string[]; selected: string[]; } @@ -45,7 +45,7 @@ export class CalculateFieldTransformerEditor extends React.PureComponent< super(props); this.state = { - include: props.options?.reduce?.include || '', + include: props.options?.reduce?.include || [], names: [], selected: [], }; @@ -62,9 +62,9 @@ export class CalculateFieldTransformerEditor extends React.PureComponent< } private initOptions() { - const { input, options } = this.props; - const include = options?.reduce?.include || ''; - const configuredOptions = include.split('|'); + const { options } = this.props; + const configuredOptions = options?.reduce?.include || []; + const input = standardTransformers.ensureColumnsTransformer.transformer(null)(this.props.input); const allNames: string[] = []; const byName: KeyValue = {}; @@ -156,7 +156,7 @@ export class CalculateFieldTransformerEditor extends React.PureComponent< const { reduce } = this.props.options; this.updateReduceOptions({ ...reduce!, - include: selected.join('|'), + include: selected, }); }; @@ -274,7 +274,6 @@ export class CalculateFieldTransformerEditor extends React.PureComponent<
= {}; @@ -73,28 +75,34 @@ export class FilterByNameTransformerEditor extends React.PureComponent< } } - let regexOption; + if (options.include?.pattern) { + try { + const regex = stringToJsRegex(options.include.pattern); + + for (const info of allNames) { + if (regex.test(info.name)) { + configuredOptions.push(info.name); + } + } + } catch (error) { + console.log(error); + } + } if (configuredOptions.length) { - let selected: FieldNameInfo[] = []; - - for (const o of configuredOptions) { - const selectedFields = allNames.filter(n => n.name === o); - if (selectedFields.length > 0) { - selected = selected.concat(selectedFields); - } else { - // there can be only one regex in the options - regexOption = o; - } - } + const selected: FieldNameInfo[] = allNames.filter(n => configuredOptions.includes(n.name)); this.setState({ options: allNames, selected: selected.map(s => s.name), - regex: regexOption, + regex: options.include?.pattern, }); } else { - this.setState({ options: allNames, selected: allNames.map(n => n.name) }); + this.setState({ + options: allNames, + selected: allNames.map(n => n.name), + regex: options.include?.pattern, + }); } } @@ -109,44 +117,46 @@ export class FilterByNameTransformerEditor extends React.PureComponent< onChange = (selected: string[]) => { const { regex, isRegexValid } = this.state; - let include = selected; + const options: FilterFieldsByNameTransformerOptions = { + ...this.props.options, + include: { names: selected }, + }; if (regex && isRegexValid) { - include = include.concat([regex]); + options.include = options.include ?? {}; + options.include.pattern = regex; } this.setState({ selected }, () => { - this.props.onChange({ - ...this.props.options, - include, - }); + this.props.onChange(options); }); }; onInputBlur = (e: React.FocusEvent) => { const { selected, regex } = this.state; let isRegexValid = true; + try { if (regex) { - new RegExp(regex); + stringToJsRegex(regex); } } catch (e) { isRegexValid = false; } + if (isRegexValid) { this.props.onChange({ ...this.props.options, - include: regex ? [...selected, regex] : selected, + include: { pattern: regex }, }); } else { this.props.onChange({ ...this.props.options, - include: selected, + include: { names: selected }, }); } - this.setState({ - isRegexValid, - }); + + this.setState({ isRegexValid }); }; render() {