diff --git a/docs/sources/panels/transformations/types-options.md b/docs/sources/panels/transformations/types-options.md index 500166ef2df..575010c8ee8 100644 --- a/docs/sources/panels/transformations/types-options.md +++ b/docs/sources/panels/transformations/types-options.md @@ -192,20 +192,33 @@ In the example below, I added two fields together and named them Sum. ## Labels to fields -This transformation changes time series results that include labels or tags into to a table structure where each label becomes its own field. +This transformation changes time series results that include labels or tags into to a table structure where each label keys and values +are included in the table result. The labels can be displayed either as columns or as row values. Given a query result of two time series: - Series 1: labels Server=Server A, Datacenter=EU - Series 2: labels Server=Server B, Datacenter=EU -This would result in a table like this: +In "Columns" mode, the result looks like this: | Time | Server | Datacenter | Value | | ------------------- | -------- | ---------- | ----- | | 2020-07-07 11:34:20 | Server A | EU | 1 | | 2020-07-07 11:34:20 | Server B | EU | 2 | +In "Rows" mode, the result has a table for each series and show each label value like this: + +| label | value | +| ---------- | -------- | +| Server | Server A | +| Datacenter | EU | + +| label | value | +| ---------- | -------- | +| Server | Server B | +| Datacenter | EU | + ### Value field name If you selected Server as the **Value field name**, then you would get one field for every value of the Server label. diff --git a/packages/grafana-data/src/transformations/transformers/labelsToFields.test.ts b/packages/grafana-data/src/transformations/transformers/labelsToFields.test.ts index 1fb32c79cb2..515b39c42ae 100644 --- a/packages/grafana-data/src/transformations/transformers/labelsToFields.test.ts +++ b/packages/grafana-data/src/transformations/transformers/labelsToFields.test.ts @@ -1,5 +1,5 @@ import { mockTransformationsRegistry } from '../../utils/tests/mockTransformationsRegistry'; -import { LabelsToFieldsOptions, labelsToFieldsTransformer } from './labelsToFields'; +import { LabelsToFieldsMode, LabelsToFieldsOptions, labelsToFieldsTransformer } from './labelsToFields'; import { DataFrame, DataTransformerConfig, FieldDTO, FieldType } from '../../types'; import { DataTransformerID } from './ids'; import { toDataFrame, toDataFrameDTO } from '../../dataframe'; @@ -176,6 +176,61 @@ describe('Labels as Columns', () => { }); }); + it('filter labels from source', async () => { + const cfg: DataTransformerConfig = { + id: DataTransformerID.labelsToFields, + options: { + keepLabels: ['foo', 'bar'], + }, + }; + + const source = toDataFrame({ + name: 'A', + fields: [ + { name: 'time', type: FieldType.time, values: [1000, 2000] }, + { name: 'a', type: FieldType.number, values: [1, 3], labels: { foo: 'thing', x: 'hide', y: 'z' } }, + { name: 'b', type: FieldType.number, values: [2, 4], labels: { bar: 'thing', a: 'nope' } }, + ], + }); + + await expect(transformDataFrame([cfg], [source])).toEmitValuesWith((received) => { + expect(received[0][0].fields.map((f) => ({ [f.name]: f.values.toArray() }))).toMatchInlineSnapshot(` + Array [ + Object { + "time": Array [ + 1000, + 2000, + ], + }, + Object { + "a": Array [ + 1, + 3, + ], + }, + Object { + "b": Array [ + 2, + 4, + ], + }, + Object { + "foo": Array [ + "thing", + "thing", + ], + }, + Object { + "bar": Array [ + "thing", + "thing", + ], + }, + ] + `); + }); + }); + it('data frame with labels and multiple fields with different labels', async () => { const cfg: DataTransformerConfig = { id: DataTransformerID.labelsToFields, @@ -207,6 +262,131 @@ describe('Labels as Columns', () => { expect(result.fields).toEqual(expected); }); }); + + it('Show labels as rows', async () => { + const cfg: DataTransformerConfig = { + id: DataTransformerID.labelsToFields, + options: { + mode: LabelsToFieldsMode.Rows, + }, + }; + + const source = toDataFrame({ + name: 'A', + fields: [ + { name: 'time', type: FieldType.time, values: [1000, 2000] }, + { name: 'a', type: FieldType.number, values: [1, 3], labels: { foo: 'thing', bar: 'a', zaz: 'xyz' } }, + { name: 'b', type: FieldType.number, values: [2, 4], labels: { foo: 'thing', bar: 'b' } }, + ], + }); + + await expect(transformDataFrame([cfg], [source])).toEmitValuesWith((received) => { + expect( + received[0].map((f) => ({ name: f.name, fields: f.fields.map((v) => ({ [v.name]: v.values.toArray() })) })) + ).toMatchInlineSnapshot(` + Array [ + Object { + "fields": Array [ + Object { + "label": Array [ + "foo", + "bar", + "zaz", + ], + }, + Object { + "value": Array [ + "thing", + "a", + "xyz", + ], + }, + ], + "name": "a {bar=\\"a\\", foo=\\"thing\\", zaz=\\"xyz\\"}", + }, + Object { + "fields": Array [ + Object { + "label": Array [ + "foo", + "bar", + ], + }, + Object { + "value": Array [ + "thing", + "b", + ], + }, + ], + "name": "b {bar=\\"b\\", foo=\\"thing\\"}", + }, + ] + `); + }); + }); + + it('Show labels as rows (and filter)', async () => { + const cfg: DataTransformerConfig = { + id: DataTransformerID.labelsToFields, + options: { + mode: LabelsToFieldsMode.Rows, + keepLabels: ['zaz', 'bar'], + }, + }; + + const source = toDataFrame({ + name: 'A', + fields: [ + { name: 'time', type: FieldType.time, values: [1000, 2000] }, + { name: 'a', type: FieldType.number, values: [1, 3], labels: { foo: 'thing', bar: 'a', zaz: 'xyz' } }, + { name: 'b', type: FieldType.number, values: [2, 4], labels: { foo: 'thing', bar: 'b' } }, + ], + }); + + await expect(transformDataFrame([cfg], [source])).toEmitValuesWith((received) => { + expect( + received[0].map((f) => ({ name: f.name, fields: f.fields.map((v) => ({ [v.name]: v.values.toArray() })) })) + ).toMatchInlineSnapshot(` + Array [ + Object { + "fields": Array [ + Object { + "label": Array [ + "zaz", + "bar", + ], + }, + Object { + "value": Array [ + "xyz", + "a", + ], + }, + ], + "name": "a {bar=\\"a\\", foo=\\"thing\\", zaz=\\"xyz\\"}", + }, + Object { + "fields": Array [ + Object { + "label": Array [ + "zaz", + "bar", + ], + }, + Object { + "value": Array [ + undefined, + "b", + ], + }, + ], + "name": "b {bar=\\"b\\", foo=\\"thing\\"}", + }, + ] + `); + }); + }); }); function toSimpleObject(frame: DataFrame) { diff --git a/packages/grafana-data/src/transformations/transformers/labelsToFields.ts b/packages/grafana-data/src/transformations/transformers/labelsToFields.ts index 181e801e115..b203c6f6d91 100644 --- a/packages/grafana-data/src/transformations/transformers/labelsToFields.ts +++ b/packages/grafana-data/src/transformations/transformers/labelsToFields.ts @@ -3,10 +3,20 @@ import { map } from 'rxjs/operators'; import { DataFrame, Field, FieldType, SynchronousDataTransformerInfo } from '../../types'; import { DataTransformerID } from './ids'; import { ArrayVector } from '../../vector'; +import { getFieldDisplayName } from '../..'; +export enum LabelsToFieldsMode { + Columns = 'columns', // default mode + Rows = 'rows', +} export interface LabelsToFieldsOptions { - /* - * If set this will use this label's value as the value field name. + mode?: LabelsToFieldsMode; + + /** When empty, this will keep all labels, otherise it will keep only labels matching the value */ + keepLabels?: string[]; + + /** + * When in column mode and if set this will use this label's value as the value field name. */ valueLabel?: string; } @@ -14,18 +24,23 @@ export interface LabelsToFieldsOptions { export const labelsToFieldsTransformer: SynchronousDataTransformerInfo = { id: DataTransformerID.labelsToFields, name: 'Labels to fields', - description: 'Extract time series labels to fields (columns)', + description: 'Extract time series labels to fields (columns or rows)', defaultOptions: {}, operator: (options) => (source) => source.pipe(map((data) => labelsToFieldsTransformer.transformer(options)(data))), transformer: (options: LabelsToFieldsOptions) => (data: DataFrame[]) => { + // Show each label as a field row + if (options.mode === LabelsToFieldsMode.Rows) { + return convertLabelsToRows(data, options.keepLabels); + } + const result: DataFrame[] = []; + const keepLabels = options.keepLabels?.length ? new Set(options.keepLabels) : undefined; for (const frame of data) { const newFields: Field[] = []; const uniqueLabels: Record> = {}; - for (const field of frame.fields) { if (!field.labels) { newFields.push(field); @@ -45,6 +60,10 @@ export const labelsToFieldsTransformer: SynchronousDataTransformerInfo> = [ + { value: LabelsToFieldsMode.Columns, label: 'Columns' }, + { value: LabelsToFieldsMode.Rows, label: 'Rows' }, +]; export const LabelsAsFieldsTransformerEditor: React.FC> = ({ input, options, onChange, }) => { - let labelNames: Array> = []; - let uniqueLabels: Record = {}; + const labelWidth = 20; - for (const frame of input) { - for (const field of frame.fields) { - if (!field.labels) { - continue; - } + const { labelNames, selected } = useMemo(() => { + let labelNames: Array> = []; + let uniqueLabels: Record = {}; - for (const labelName of Object.keys(field.labels)) { - if (!uniqueLabels[labelName]) { - labelNames.push({ value: labelName, label: labelName }); - uniqueLabels[labelName] = true; + for (const frame of input) { + for (const field of frame.fields) { + if (!field.labels) { + continue; + } + + for (const labelName of Object.keys(field.labels)) { + if (!uniqueLabels[labelName]) { + labelNames.push({ value: labelName, label: labelName }); + uniqueLabels[labelName] = true; + } } } } - } + + const selected = new Set(options.keepLabels?.length ? options.keepLabels : Object.keys(uniqueLabels)); + return { labelNames, selected }; + }, [options.keepLabels, input]); const onValueLabelChange = (value: SelectableValue | null) => { - onChange({ valueLabel: value?.value }); + onChange({ ...options, valueLabel: value?.value }); + }; + + const onToggleSelection = (v: string) => { + if (selected.has(v)) { + selected.delete(v); + } else { + selected.add(v); + } + if (selected.size === labelNames.length || !selected.size) { + const { keepLabels, ...rest } = options; + onChange(rest); + } else { + onChange({ ...options, keepLabels: [...selected] }); + } }; return ( -
-
-
Value field name
- + + + )}
); };