From 7e7daf48f46be378f2496c2bdd8276084e35384a Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Tue, 17 Jan 2023 14:56:23 -0600 Subject: [PATCH] Transforms: make partitionByValues create field labels by default (#61255) --- .../partitionByValues.test.ts | 88 ++++++++++++++++++- .../partitionByValues/partitionByValues.ts | 66 +++++++++++--- 2 files changed, 138 insertions(+), 16 deletions(-) diff --git a/public/app/features/transformers/partitionByValues/partitionByValues.test.ts b/public/app/features/transformers/partitionByValues/partitionByValues.test.ts index 7b10b7b67aa..3b41ccbab8f 100644 --- a/public/app/features/transformers/partitionByValues/partitionByValues.test.ts +++ b/public/app/features/transformers/partitionByValues/partitionByValues.test.ts @@ -21,6 +21,10 @@ describe('Partition by values transformer', () => { const config: PartitionByValuesTransformerOptions = { fields: ['region'], + keepFields: true, + naming: { + asLabels: false, + }, }; let partitioned = partitionByValuesTransformer.transformer(config, ctx)(source); @@ -57,6 +61,10 @@ describe('Partition by values transformer', () => { const config: PartitionByValuesTransformerOptions = { fields: ['region', 'status'], + keepFields: true, + naming: { + asLabels: false, + }, }; let partitioned = partitionByValuesTransformer.transformer(config, ctx)(source); @@ -100,7 +108,7 @@ describe('Partition by values transformer', () => { expect(partitioned[3].fields[2].values.toArray()).toEqual(['FAIL']); }); - it('should partition by multiple fields with custom frame naming {withFields: true}', () => { + it('should partition by multiple fields with custom frame naming {withNames: true}', () => { const source = [ toDataFrame({ name: 'XYZ', @@ -115,8 +123,10 @@ describe('Partition by values transformer', () => { const config: PartitionByValuesTransformerOptions = { fields: ['region', 'status'], + keepFields: true, naming: { - withFields: true, + asLabels: false, + withNames: true, }, }; @@ -143,7 +153,9 @@ describe('Partition by values transformer', () => { const config: PartitionByValuesTransformerOptions = { fields: ['region', 'status'], + keepFields: true, naming: { + asLabels: false, append: true, }, }; @@ -156,7 +168,7 @@ describe('Partition by values transformer', () => { expect(partitioned[3].name).toEqual('XYZ China FAIL'); }); - it('should partition by multiple fields with custom frame naming {withFields: true, append: true}', () => { + it('should partition by multiple fields with custom frame naming {withNames: true, append: true}', () => { const source = [ toDataFrame({ name: 'XYZ', @@ -171,8 +183,10 @@ describe('Partition by values transformer', () => { const config: PartitionByValuesTransformerOptions = { fields: ['region', 'status'], + keepFields: true, naming: { - withFields: true, + asLabels: false, + withNames: true, append: true, }, }; @@ -184,4 +198,70 @@ describe('Partition by values transformer', () => { expect(partitioned[2].name).toEqual('XYZ region=China status=OK'); expect(partitioned[3].name).toEqual('XYZ region=China status=FAIL'); }); + + it('should partition by multiple fields naming: {asLabels: true}', () => { + const source = [ + toDataFrame({ + name: 'XYZ', + refId: 'A', + fields: [ + { name: 'model', type: FieldType.string, values: ['E1', 'E2', 'C1', 'E3', 'C2', 'C3'] }, + { name: 'region', type: FieldType.string, values: ['Europe', 'Europe', 'China', 'Europe', 'China', 'China'] }, + { name: 'status', type: FieldType.string, values: ['OK', 'FAIL', 'OK', 'FAIL', 'OK', 'FAIL'] }, + ], + }), + ]; + + const config: PartitionByValuesTransformerOptions = { + fields: ['region', 'status'], + keepFields: true, + naming: { + asLabels: true, + }, + }; + + let partitioned = partitionByValuesTransformer.transformer(config, ctx)(source); + + // all frame names are same + expect(partitioned[0].name).toEqual('XYZ'); + expect(partitioned[1].name).toEqual('XYZ'); + expect(partitioned[2].name).toEqual('XYZ'); + expect(partitioned[3].name).toEqual('XYZ'); + + // all frames contain all fields + expect(partitioned[0].fields[0].name).toEqual('model'); + expect(partitioned[0].fields[1].name).toEqual('region'); + expect(partitioned[0].fields[2].name).toEqual('status'); + + // in each frame, every field has same labels + expect(partitioned[0].fields[0].labels).toEqual({ region: 'Europe', status: 'OK' }); + expect(partitioned[1].fields[0].labels).toEqual({ region: 'Europe', status: 'FAIL' }); + expect(partitioned[2].fields[0].labels).toEqual({ region: 'China', status: 'OK' }); + expect(partitioned[3].fields[0].labels).toEqual({ region: 'China', status: 'FAIL' }); + }); + + it('should partition by multiple fields and omit those fields in result', () => { + const source = [ + toDataFrame({ + name: 'XYZ', + refId: 'A', + fields: [ + { name: 'model', type: FieldType.string, values: ['E1', 'E2', 'C1', 'E3', 'C2', 'C3'] }, + { name: 'region', type: FieldType.string, values: ['Europe', 'Europe', 'China', 'Europe', 'China', 'China'] }, + { name: 'status', type: FieldType.string, values: ['OK', 'FAIL', 'OK', 'FAIL', 'OK', 'FAIL'] }, + ], + }), + ]; + + const config: PartitionByValuesTransformerOptions = { + fields: ['region', 'status'], + }; + + let partitioned = partitionByValuesTransformer.transformer(config, ctx)(source); + + // all frames contain only model field + expect(partitioned[0].fields.length).toEqual(1); + expect(partitioned[0].fields[0].name).toEqual('model'); + expect(partitioned[0].fields[0].labels).toEqual({ region: 'Europe', status: 'OK' }); + }); }); diff --git a/public/app/features/transformers/partitionByValues/partitionByValues.ts b/public/app/features/transformers/partitionByValues/partitionByValues.ts index 2b26be6d6b9..1d558728a02 100644 --- a/public/app/features/transformers/partitionByValues/partitionByValues.ts +++ b/public/app/features/transformers/partitionByValues/partitionByValues.ts @@ -14,10 +14,15 @@ import { noopTransformer } from '@grafana/data/src/transformations/transformers/ import { partition } from './partition'; export interface FrameNamingOptions { + /** when true, the frame name is copied unmodified, and discriminator fields' names+values become field labels in new frames */ + asLabels?: boolean; + + /** opts below are used only when asLabels: false */ + /** whether to append to existing frame name, false -> replace */ append?: boolean; // false /** whether to include discriminator field names, e.g. true -> Region=Europe Profession=Chef, false -> 'Europe Chef' */ - withFields?: boolean; // false + withNames?: boolean; // false /** name/value separator, e.g. '=' in 'Region=Europe' */ separator1?: string; /** name/value pair separator, e.g. ' ' in 'Region=Europe Profession=Chef' */ @@ -25,8 +30,10 @@ export interface FrameNamingOptions { } const defaultFrameNameOptions: FrameNamingOptions = { + asLabels: true, + append: false, - withFields: false, + withNames: false, separator1: '=', separator2: ' ', }; @@ -36,14 +43,26 @@ export interface PartitionByValuesTransformerOptions { fields: string[]; /** how the split frames' names should be suffixed (ends up as field prefixes) */ naming?: FrameNamingOptions; + /** should the discriminator fields be kept in the output */ + keepFields?: boolean; } function buildFrameName(opts: FrameNamingOptions, names: string[], values: unknown[]): string { return names - .map((name, i) => (opts.withFields ? `${name}${opts.separator1}${values[i]}` : values[i])) + .map((name, i) => (opts.withNames ? `${name}${opts.separator1}${values[i]}` : values[i])) .join(opts.separator2); } +function buildFieldLabels(names: string[], values: unknown[]) { + const labels: Record = {}; + + names.forEach((name, i) => { + labels[name] = String(values[i]); + }); + + return labels; +} + export const partitionByValuesTransformer: SynchronousDataTransformerInfo = { id: DataTransformerID.partitionByValues, name: 'Partition by values', @@ -78,21 +97,40 @@ export const partitionByValuesTransformer: SynchronousDataTransformerInfo { - let name = buildFrameName( - frameNameOpts, - names, - keyFields.map((f, i) => keyFieldsVals[i][idxs[0]]) - ); + let frameName = frame.name; + let fieldLabels = {}; - if (options.naming?.append && frame.name) { - name = `${frame.name} ${name}`; + if (frameNameOpts.asLabels) { + fieldLabels = buildFieldLabels( + names, + keyFields.map((f, i) => keyFieldsVals[i][idxs[0]]) + ); + } else { + let name = buildFrameName( + frameNameOpts, + names, + keyFields.map((f, i) => keyFieldsVals[i][idxs[0]]) + ); + + if (options.naming?.append && frame.name) { + name = `${frame.name} ${name}`; + } + + frameName = name; + } + + let filteredFields = frame.fields; + + if (!options.keepFields) { + const keyFieldNames = new Set(names); + filteredFields = frame.fields.filter((field) => !keyFieldNames.has(field.name)); } return { ...frame, - name, + name: frameName, length: idxs.length, - fields: frame.fields.map((f) => { + fields: filteredFields.map((f) => { const vals = f.values.toArray(); const vals2 = Array(idxs.length); @@ -102,6 +140,10 @@ export const partitionByValuesTransformer: SynchronousDataTransformerInfo