diff --git a/packages/grafana-data/src/transformations/transformers/labelsToFields.test.ts b/packages/grafana-data/src/transformations/transformers/labelsToFields.test.ts index 1f8286525b1..164da1cc347 100644 --- a/packages/grafana-data/src/transformations/transformers/labelsToFields.test.ts +++ b/packages/grafana-data/src/transformations/transformers/labelsToFields.test.ts @@ -1,6 +1,6 @@ import { mockTransformationsRegistry } from '../../utils/tests/mockTransformationsRegistry'; import { LabelsToFieldsOptions, labelsToFieldsTransformer } from './labelsToFields'; -import { DataTransformerConfig, FieldDTO, FieldType } from '../../types'; +import { DataFrame, DataTransformerConfig, FieldDTO, FieldType } from '../../types'; import { DataTransformerID } from './ids'; import { toDataFrame, toDataFrameDTO } from '../../dataframe'; import { transformDataFrame } from '../transformDataFrame'; @@ -26,21 +26,26 @@ describe('Labels as Columns', () => { await expect(transformDataFrame([cfg], [source])).toEmitValuesWith((received) => { const data = received[0]; - const result = toDataFrameDTO(data[0]); - - const expected: FieldDTO[] = [ - { name: 'time', type: FieldType.time, values: [1000, 2000], config: {} }, - { - name: 'location', - type: FieldType.string, - values: ['inside', 'inside'], - config: {}, - }, - { name: 'feelsLike', type: FieldType.string, values: ['ok', 'ok'], config: {} }, - { name: 'Value', type: FieldType.number, values: [1, 2], config: {} }, - ]; - - expect(result.fields).toEqual(expected); + expect(toSimpleObject(data[0])).toMatchInlineSnapshot(` + Object { + "Value": Array [ + 1, + 2, + ], + "feelsLike": Array [ + "ok", + "ok", + ], + "location": Array [ + "inside", + "inside", + ], + "time": Array [ + 1000, + 2000, + ], + } + `); }); }); @@ -73,13 +78,13 @@ describe('Labels as Columns', () => { const expected: FieldDTO[] = [ { name: 'time', type: FieldType.time, values: [1000, 2000], config: {} }, + { name: 'Request', type: FieldType.number, values: [1, 2], config: {} }, { name: 'location', type: FieldType.string, values: ['inside', 'inside'], config: {}, }, - { name: 'Request', type: FieldType.number, values: [1, 2], config: {} }, ]; expect(result.fields).toEqual(expected); @@ -110,15 +115,42 @@ describe('Labels as Columns', () => { await expect(transformDataFrame([cfg], [oneValueOneLabelA, oneValueOneLabelB])).toEmitValuesWith((received) => { const data = received[0]; - const result = toDataFrameDTO(data[0]); + expect(data.length).toEqual(2); - const expected: FieldDTO[] = [ - { name: 'time', type: FieldType.time, values: [1000, 2000], config: {} }, - { name: 'location', type: FieldType.string, values: ['inside', 'outside'], config: {} }, - { name: 'temp', type: FieldType.number, values: [1, -1], config: {} }, - ]; - - expect(result.fields).toEqual(expected); + expect(toSimpleObject(data[0])).toMatchInlineSnapshot(` + Object { + "location": Array [ + "inside", + ], + "temp": Array [ + 1, + ], + "time": Array [ + 1000, + ], + } + `); + expect(toSimpleObject(data[1])).toMatchInlineSnapshot(` + Object { + "location": Array [ + "outside", + ], + "temp": Array [ + -1, + ], + "time": Array [ + 2000, + ], + } + `); }); }); }); + +function toSimpleObject(frame: DataFrame) { + const obj: any = {}; + for (const field of frame.fields) { + obj[field.name] = field.values.toArray(); + } + return obj; +} diff --git a/packages/grafana-data/src/transformations/transformers/labelsToFields.ts b/packages/grafana-data/src/transformations/transformers/labelsToFields.ts index 67b718d02b3..fb930d5968c 100644 --- a/packages/grafana-data/src/transformations/transformers/labelsToFields.ts +++ b/packages/grafana-data/src/transformations/transformers/labelsToFields.ts @@ -1,9 +1,8 @@ import { map } from 'rxjs/operators'; -import { DataFrame, DataTransformerInfo, Field, FieldType } from '../../types'; +import { DataFrame, Field, FieldType, SynchronousDataTransformerInfo } from '../../types'; import { DataTransformerID } from './ids'; import { ArrayVector } from '../../vector'; -import { mergeTransformer } from './merge'; export interface LabelsToFieldsOptions { /* @@ -12,65 +11,61 @@ export interface LabelsToFieldsOptions { valueLabel?: string; } -export const labelsToFieldsTransformer: DataTransformerInfo = { +export const labelsToFieldsTransformer: SynchronousDataTransformerInfo = { id: DataTransformerID.labelsToFields, name: 'Labels to fields', description: 'Extract time series labels to fields (columns)', defaultOptions: {}, - operator: (options) => (source) => - source.pipe( - map((data) => { - const result: DataFrame[] = []; - for (const frame of data) { - const newFields: Field[] = []; + operator: (options) => (source) => source.pipe(map((data) => labelsToFieldsTransformer.transformer(options)(data))), - for (const field of frame.fields) { - if (!field.labels) { - newFields.push(field); - continue; - } + transformer: (options: LabelsToFieldsOptions) => (data: DataFrame[]) => { + const result: DataFrame[] = []; - let name = field.name; + for (const frame of data) { + const newFields: Field[] = []; - for (const labelName of Object.keys(field.labels)) { - // if we should use this label as the value field name store it and skip adding this as a separate field - if (options.valueLabel === labelName) { - name = field.labels[labelName]; - continue; - } - - const values = new Array(frame.length).fill(field.labels[labelName]); - newFields.push({ - name: labelName, - type: FieldType.string, - values: new ArrayVector(values), - config: {}, - }); - } - - // add the value field but clear out any labels or displayName - newFields.push({ - ...field, - name, - config: { - ...field.config, - // we need to clear thes for this transform as these can contain label names that we no longer want - displayName: undefined, - displayNameFromDS: undefined, - }, - labels: undefined, - }); - } - - result.push({ - fields: newFields, - length: frame.length, - }); + for (const field of frame.fields) { + if (!field.labels) { + newFields.push(field); + continue; } - return result; - }), - mergeTransformer.operator({}) - ), + const sansLabels = { + ...field, + config: { + ...field.config, + // we need to clear thes for this transform as these can contain label names that we no longer want + displayName: undefined, + displayNameFromDS: undefined, + }, + labels: undefined, + }; + newFields.push(sansLabels); + + for (const labelName of Object.keys(field.labels)) { + // if we should use this label as the value field name store it and skip adding this as a separate field + if (options.valueLabel === labelName) { + sansLabels.name = field.labels[labelName]; + continue; + } + + const values = new Array(frame.length).fill(field.labels[labelName]); + newFields.push({ + name: labelName, + type: FieldType.string, + values: new ArrayVector(values), + config: {}, + }); + } + } + + result.push({ + fields: newFields, + length: frame.length, + }); + } + + return result; + }, }; diff --git a/public/app/features/dashboard/state/DashboardMigrator.test.ts b/public/app/features/dashboard/state/DashboardMigrator.test.ts index 0db0bef259b..f85a117f016 100644 --- a/public/app/features/dashboard/state/DashboardMigrator.test.ts +++ b/public/app/features/dashboard/state/DashboardMigrator.test.ts @@ -162,7 +162,7 @@ describe('DashboardModel', () => { }); it('dashboard schema version should be set to latest', () => { - expect(model.schemaVersion).toBe(30); + expect(model.schemaVersion).toBe(31); }); it('graph thresholds should be migrated', () => { @@ -1420,6 +1420,38 @@ describe('DashboardModel', () => { expect(model.panels[0].panels[0].fieldConfig.defaults).toEqual(undefined); }); }); + + describe('labelsToFields should be split into two transformers', () => { + let model: DashboardModel; + + beforeEach(() => { + model = new DashboardModel({ + schemaVersion: 29, + panels: [ + { + id: 1, + type: 'timeseries', + transformations: [{ id: 'labelsToFields' }], + }, + ], + }); + }); + + it('should create two transormatoins', () => { + const xforms = model.panels[0].transformations; + expect(xforms).toMatchInlineSnapshot(` + Array [ + Object { + "id": "labelsToFields", + }, + Object { + "id": "merge", + "options": Object {}, + }, + ] + `); + }); + }); }); function createRow(options: any, panelDescriptions: any[]) { diff --git a/public/app/features/dashboard/state/DashboardMigrator.ts b/public/app/features/dashboard/state/DashboardMigrator.ts index a9ebd680179..2d0e83fac09 100644 --- a/public/app/features/dashboard/state/DashboardMigrator.ts +++ b/public/app/features/dashboard/state/DashboardMigrator.ts @@ -19,6 +19,7 @@ import { ValueMap, ValueMapping, getActiveThreshold, + DataTransformerConfig, } from '@grafana/data'; // Constants import { @@ -36,6 +37,8 @@ import { config } from 'app/core/config'; import { plugin as statPanelPlugin } from 'app/plugins/panel/stat/module'; import { plugin as gaugePanelPlugin } from 'app/plugins/panel/gauge/module'; import { getStandardFieldConfigs, getStandardOptionEditors } from '@grafana/ui'; +import { labelsToFieldsTransformer } from '../../../../../packages/grafana-data/src/transformations/transformers/labelsToFields'; +import { mergeTransformer } from '../../../../../packages/grafana-data/src/transformations/transformers/merge'; standardEditorsRegistry.setInit(getStandardOptionEditors); standardFieldConfigEditorRegistry.setInit(getStandardFieldConfigs); @@ -52,7 +55,7 @@ export class DashboardMigrator { let i, j, k, n; const oldVersion = this.dashboard.schemaVersion; const panelUpgrades: PanelSchemeUpgradeHandler[] = []; - this.dashboard.schemaVersion = 30; + this.dashboard.schemaVersion = 31; if (oldVersion === this.dashboard.schemaVersion) { return; @@ -660,6 +663,22 @@ export class DashboardMigrator { panelUpgrades.push(migrateTooltipOptions); } + if (oldVersion < 31) { + panelUpgrades.push((panel: PanelModel) => { + if (panel.transformations) { + for (const t of panel.transformations) { + if (t.id === labelsToFieldsTransformer.id) { + return appedTransformerAfter(panel, labelsToFieldsTransformer.id, { + id: mergeTransformer.id, + options: {}, + }); + } + } + } + return panel; + }); + } + if (panelUpgrades.length === 0) { return; } @@ -949,6 +968,21 @@ function migrateSinglestat(panel: PanelModel) { return panel; } +// mutates transformations appending a new transformer after the existing one +function appedTransformerAfter(panel: PanelModel, id: string, cfg: DataTransformerConfig) { + if (panel.transformations) { + const transformations: DataTransformerConfig[] = []; + for (const t of panel.transformations) { + transformations.push(t); + if (t.id === id) { + transformations.push({ ...cfg }); + } + } + panel.transformations = transformations; + } + return panel; +} + function upgradeValueMappingsForPanel(panel: PanelModel) { const fieldConfig = panel.fieldConfig; if (!fieldConfig) {