mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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.
This commit is contained in:
parent
0e8638ec92
commit
96f26cbd5b
@ -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);
|
||||
|
@ -9,3 +9,4 @@ export {
|
||||
TransformerUIProps,
|
||||
standardTransformersRegistry,
|
||||
} from './standardTransformersRegistry';
|
||||
export { RegexpOrNamesMatcherOptions } from './matchers/nameMatcher';
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -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<FieldType> = {
|
||||
defaultOptions: FieldType.number,
|
||||
|
||||
get: (type: FieldType) => {
|
||||
return (field: Field) => {
|
||||
return (field: Field, frame: DataFrame, allFrames: DataFrame[]) => {
|
||||
return type === field.type;
|
||||
};
|
||||
},
|
||||
|
@ -18,6 +18,9 @@ export enum FieldMatcherID {
|
||||
// With arguments
|
||||
byType = 'byType',
|
||||
byName = 'byName',
|
||||
byNames = 'byNames',
|
||||
byRegexp = 'byRegexp',
|
||||
byRegexpOrNames = 'byRegexpOrNames',
|
||||
// byIndex = 'byIndex',
|
||||
// byLabel = 'byLabel',
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -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<string> = {
|
||||
const fieldNameMatcher: FieldMatcherInfo<string> = {
|
||||
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<string> = {
|
||||
const multipleFieldNamesMatcher: FieldMatcherInfo<string[]> = {
|
||||
id: FieldMatcherID.byNames,
|
||||
name: 'Field Names',
|
||||
description: 'match any of the given the field names',
|
||||
defaultOptions: [],
|
||||
|
||||
get: (names: string[]): FieldMatcher => {
|
||||
const uniqueNames = new Set<string>(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<string> = {
|
||||
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<RegexpOrNamesMatcherOptions> = {
|
||||
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<string> = {
|
||||
id: FrameMatcherID.byName,
|
||||
name: 'Frame Name',
|
||||
description: 'match the frame name',
|
||||
@ -51,9 +127,9 @@ const frameNameMacher: FrameMatcherInfo<string> = {
|
||||
* Registry Initalization
|
||||
*/
|
||||
export function getFieldNameMatchers(): FieldMatcherInfo[] {
|
||||
return [fieldNameMacher];
|
||||
return [fieldNameMatcher, regexpFieldNameMatcher, multipleFieldNamesMatcher, regexpOrMultipleNamesMatcher];
|
||||
}
|
||||
|
||||
export function getFrameNameMatchers(): FrameMatcherInfo[] {
|
||||
return [frameNameMacher];
|
||||
return [frameNameMatcher];
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -14,9 +14,9 @@ const anyFieldMatcher: FieldMatcherInfo<MatcherConfig[]> = {
|
||||
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<MatcherConfig[]> = {
|
||||
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<MatcherConfig> = {
|
||||
|
||||
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);
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
@ -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<CalculateFieldTransf
|
||||
},
|
||||
},
|
||||
transformer: options => (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<CalculateFieldTransf
|
||||
},
|
||||
};
|
||||
|
||||
function getReduceRowCreator(options: ReduceOptions): ValuesCreator {
|
||||
function getReduceRowCreator(options: ReduceOptions, allFrames: DataFrame[]): ValuesCreator {
|
||||
let matcher = getFieldMatcher({
|
||||
id: FieldMatcherID.numeric,
|
||||
});
|
||||
|
||||
if (options.include && options.include.length) {
|
||||
matcher = getFieldMatcher({
|
||||
id: FieldMatcherID.byName,
|
||||
id: FieldMatcherID.byNames,
|
||||
options: options.include,
|
||||
});
|
||||
}
|
||||
@ -152,7 +147,7 @@ function getReduceRowCreator(options: ReduceOptions): ValuesCreator {
|
||||
// Find the columns that should be examined
|
||||
const columns: Vector[] = [];
|
||||
for (const field of frame.fields) {
|
||||
if (matcher(field)) {
|
||||
if (matcher(field, frame, allFrames)) {
|
||||
columns.push(field.values);
|
||||
}
|
||||
}
|
||||
@ -177,13 +172,13 @@ function getReduceRowCreator(options: ReduceOptions): ValuesCreator {
|
||||
};
|
||||
}
|
||||
|
||||
function findFieldValuesWithNameOrConstant(frame: DataFrame, name: string): Vector | undefined {
|
||||
function findFieldValuesWithNameOrConstant(frame: DataFrame, name: string, allFrames: DataFrame[]): Vector | undefined {
|
||||
if (!name) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
for (const f of frame.fields) {
|
||||
if (name === getFieldDisplayName(f, frame)) {
|
||||
if (name === getFieldDisplayName(f, frame, allFrames)) {
|
||||
return f.values;
|
||||
}
|
||||
}
|
||||
@ -196,12 +191,12 @@ function findFieldValuesWithNameOrConstant(frame: DataFrame, name: string): Vect
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getBinaryCreator(options: BinaryOptions): ValuesCreator {
|
||||
function getBinaryCreator(options: BinaryOptions, allFrames: DataFrame[]): ValuesCreator {
|
||||
const operator = binaryOperators.getIfExists(options.operator);
|
||||
|
||||
return (frame: DataFrame) => {
|
||||
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;
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
@ -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;
|
||||
}
|
@ -34,15 +34,16 @@ export const filterFieldsTransformer: DataTransformerInfo<FilterOptions> = {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -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)$/' },
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -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<FilterFieldsByNameTransformerOptions> = {
|
||||
@ -19,25 +20,33 @@ export const filterFieldsByNameTransformer: DataTransformerInfo<FilterFieldsByNa
|
||||
* be applied, just return the input series
|
||||
*/
|
||||
transformer: (options: FilterFieldsByNameTransformerOptions) => {
|
||||
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 };
|
||||
};
|
||||
|
@ -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',
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ export const organizeFieldsTransformer: DataTransformerInfo<OrganizeFieldsTransf
|
||||
const rename = renameFieldsTransformer.transformer(options);
|
||||
const order = orderFieldsTransformer.transformer(options);
|
||||
const filter = filterFieldsByNameTransformer.transformer({
|
||||
exclude: mapToExcludeArray(options.excludeByName),
|
||||
exclude: { names: mapToExcludeArray(options.excludeByName) },
|
||||
});
|
||||
|
||||
return (data: DataFrame[]) => rename(order(filter(data)));
|
||||
|
@ -72,7 +72,7 @@ export const reduceTransformer: DataTransformerInfo<ReduceTransformerOptions> =
|
||||
continue;
|
||||
}
|
||||
|
||||
if (matcher(field)) {
|
||||
if (matcher(field, series, data)) {
|
||||
const results = reduceField({
|
||||
field,
|
||||
reducers,
|
||||
|
@ -25,7 +25,7 @@ export interface DataTransformerConfig<TOptions = any> {
|
||||
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<TOptions = any> extends RegistryItemWithOptions<TOptions> {
|
||||
|
@ -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<MatcherUIProps<string>> {
|
||||
render() {
|
||||
const { data, options, onChange } = this.props;
|
||||
const names: Set<string> = new Set();
|
||||
export const FieldNameMatcherEditor = memo<MatcherUIProps<string>>(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<string>) => {
|
||||
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 (
|
||||
<Select allowCustomValue value={selectedOption} options={selectOptions} onChange={o => onChange(o.value!)} />
|
||||
);
|
||||
}
|
||||
}
|
||||
const selectedOption = selectOptions.find(v => v.value === options);
|
||||
return <Select value={selectedOption} options={selectOptions} onChange={onChange} />;
|
||||
});
|
||||
|
||||
export const fieldNameMatcherItem: FieldMatcherUIRegistryItem<string> = {
|
||||
id: FieldMatcherID.byName,
|
||||
@ -35,3 +29,26 @@ export const fieldNameMatcherItem: FieldMatcherUIRegistryItem<string> = {
|
||||
name: 'Filter by field',
|
||||
description: 'Set properties for fields matching the name',
|
||||
};
|
||||
|
||||
const useFieldDisplayNames = (data: DataFrame[]): Set<string> => {
|
||||
return useMemo(() => {
|
||||
const names: Set<string> = 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<string>): Array<SelectableValue<string>> => {
|
||||
return useMemo(() => {
|
||||
return Array.from(displayNames).map(n => ({
|
||||
value: n,
|
||||
label: n,
|
||||
}));
|
||||
}, [displayNames]);
|
||||
};
|
||||
|
@ -27,7 +27,7 @@ import defaults from 'lodash/defaults';
|
||||
interface CalculateFieldTransformerEditorProps extends TransformerUIProps<CalculateFieldTransformerOptions> {}
|
||||
|
||||
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<boolean> = {};
|
||||
@ -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<
|
||||
</div>
|
||||
<div className="gf-form">
|
||||
<Select
|
||||
allowCustomValue
|
||||
placeholder="Field or number"
|
||||
options={leftNames}
|
||||
className="min-width-18 gf-form-spacing"
|
||||
@ -290,7 +289,6 @@ export class CalculateFieldTransformerEditor extends React.PureComponent<
|
||||
menuPlacement="bottom"
|
||||
/>
|
||||
<Select
|
||||
allowCustomValue
|
||||
placeholder="Field or number"
|
||||
className="min-width-10"
|
||||
options={rightNames}
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
TransformerRegistyItem,
|
||||
TransformerUIProps,
|
||||
getFieldDisplayName,
|
||||
stringToJsRegex,
|
||||
} from '@grafana/data';
|
||||
import { Field, Input, FilterPill, HorizontalGroup } from '@grafana/ui';
|
||||
import { css } from 'emotion';
|
||||
@ -32,7 +33,8 @@ export class FilterByNameTransformerEditor extends React.PureComponent<
|
||||
constructor(props: FilterByNameTransformerEditorProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
include: props.options.include || [],
|
||||
include: props.options.include?.names || [],
|
||||
regex: props.options.include?.pattern,
|
||||
options: [],
|
||||
selected: [],
|
||||
isRegexValid: true,
|
||||
@ -51,7 +53,7 @@ export class FilterByNameTransformerEditor extends React.PureComponent<
|
||||
|
||||
private initOptions() {
|
||||
const { input, options } = this.props;
|
||||
const configuredOptions = options.include ? options.include : [];
|
||||
const configuredOptions = Array.from(options.include?.names ?? []);
|
||||
|
||||
const allNames: FieldNameInfo[] = [];
|
||||
const byName: KeyValue<FieldNameInfo> = {};
|
||||
@ -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<HTMLInputElement>) => {
|
||||
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() {
|
||||
|
Loading…
Reference in New Issue
Block a user