diff --git a/packages/grafana-data/src/transformations/transformers/__snapshots__/reduce.test.ts.snap b/packages/grafana-data/src/transformations/transformers/__snapshots__/reduce.test.ts.snap deleted file mode 100644 index edd3cdb78a2..00000000000 --- a/packages/grafana-data/src/transformations/transformers/__snapshots__/reduce.test.ts.snap +++ /dev/null @@ -1,73 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Reducer Transformer filters by include 1`] = ` -Object { - "fields": Array [ - Object { - "config": Object {}, - "labels": undefined, - "name": "Field", - "type": "string", - "values": Array [ - "A", - "B", - ], - }, - Object { - "config": Object { - "title": "First", - }, - "labels": undefined, - "name": "first", - "type": "number", - "values": Array [ - 1, - "a", - ], - }, - Object { - "config": Object { - "title": "Min", - }, - "labels": undefined, - "name": "min", - "type": "number", - "values": Array [ - 1, - null, - ], - }, - Object { - "config": Object { - "title": "Max", - }, - "labels": undefined, - "name": "max", - "type": "number", - "values": Array [ - 4, - null, - ], - }, - Object { - "config": Object { - "title": "Delta", - }, - "labels": undefined, - "name": "delta", - "type": "number", - "values": Array [ - 3, - 0, - ], - }, - ], - "meta": Object { - "transformations": Array [ - "reduce", - ], - }, - "name": undefined, - "refId": undefined, -} -`; diff --git a/packages/grafana-data/src/transformations/transformers/reduce.test.ts b/packages/grafana-data/src/transformations/transformers/reduce.test.ts index 938bb6f7d20..1d5791548b2 100644 --- a/packages/grafana-data/src/transformations/transformers/reduce.test.ts +++ b/packages/grafana-data/src/transformations/transformers/reduce.test.ts @@ -1,14 +1,43 @@ import { ReducerID } from '../fieldReducer'; import { DataTransformerID } from './ids'; -import { toDataFrame, toDataFrameDTO } from '../../dataframe/processDataFrame'; +import { toDataFrame } from '../../dataframe/processDataFrame'; import { mockTransformationsRegistry } from '../../utils/tests/mockTransformationsRegistry'; import { reduceTransformer } from './reduce'; import { transformDataFrame } from '../transformDataFrame'; +import { Field, FieldType } from '../../types'; +import { ArrayVector } from '../../vector'; -const seriesWithValues = toDataFrame({ +const seriesAWithSingleField = toDataFrame({ + name: 'A', fields: [ - { name: 'A', values: [1, 2, 3, 4] }, // Numbers - { name: 'B', values: ['a', 'b', 'c', 'd'] }, // Strings + { name: 'time', type: FieldType.time, values: [3000, 4000, 5000, 6000] }, + { name: 'temperature', type: FieldType.number, values: [3, 4, 5, 6] }, + ], +}); + +const seriesAWithMultipleFields = toDataFrame({ + name: 'A', + fields: [ + { name: 'time', type: FieldType.time, values: [3000, 4000, 5000, 6000] }, + { name: 'temperature', type: FieldType.number, values: [3, 4, 5, 6] }, + { name: 'humidity', type: FieldType.number, values: [10000.3, 10000.4, 10000.5, 10000.6] }, + ], +}); + +const seriesBWithSingleField = toDataFrame({ + name: 'B', + fields: [ + { name: 'time', type: FieldType.time, values: [1000, 3000, 5000, 7000] }, + { name: 'temperature', type: FieldType.number, values: [1, 3, 5, 7] }, + ], +}); + +const seriesBWithMultipleFields = toDataFrame({ + name: 'B', + fields: [ + { name: 'time', type: FieldType.time, values: [1000, 3000, 5000, 7000] }, + { name: 'temperature', type: FieldType.number, values: [1, 3, 5, 7] }, + { name: 'humidity', type: FieldType.number, values: [11000.1, 11000.3, 11000.5, 11000.7] }, ], }); @@ -16,15 +45,188 @@ describe('Reducer Transformer', () => { beforeAll(() => { mockTransformationsRegistry([reduceTransformer]); }); - it('filters by include', () => { + + it('reduces multiple data frames with many fields', () => { const cfg = { id: DataTransformerID.reduce, options: { - reducers: [ReducerID.first, ReducerID.min, ReducerID.max, ReducerID.delta], + reducers: [ReducerID.first, ReducerID.min, ReducerID.max, ReducerID.last], }, }; - const processed = transformDataFrame([cfg], [seriesWithValues])[0]; - expect(processed.fields.length).toBe(5); - expect(toDataFrameDTO(processed)).toMatchSnapshot(); + const processed = transformDataFrame([cfg], [seriesAWithMultipleFields, seriesBWithMultipleFields]); + const expected: Field[] = [ + { + name: 'Field', + type: FieldType.string, + values: new ArrayVector(['temperature {A}', 'humidity {A}', 'temperature {B}', 'humidity {B}']), + config: {}, + }, + { + name: 'first', + type: FieldType.number, + values: new ArrayVector([3, 10000.3, 1, 11000.1]), + config: { title: 'First' }, + }, + { + name: 'min', + type: FieldType.number, + values: new ArrayVector([3, 10000.3, 1, 11000.1]), + config: { title: 'Min' }, + }, + { + name: 'max', + type: FieldType.number, + values: new ArrayVector([6, 10000.6, 7, 11000.7]), + config: { title: 'Max' }, + }, + { + name: 'last', + type: FieldType.number, + values: new ArrayVector([6, 10000.6, 7, 11000.7]), + config: { title: 'Last' }, + }, + ]; + + expect(processed.length).toEqual(1); + expect(processed[0].length).toEqual(4); + expect(processed[0].fields).toEqual(expected); + }); + + it('reduces multiple data frames with single field', () => { + const cfg = { + id: DataTransformerID.reduce, + options: { + reducers: [ReducerID.first, ReducerID.min, ReducerID.max, ReducerID.last], + }, + }; + const processed = transformDataFrame([cfg], [seriesAWithSingleField, seriesBWithSingleField]); + const expected: Field[] = [ + { + name: 'Field', + type: FieldType.string, + values: new ArrayVector(['temperature {A}', 'temperature {B}']), + config: {}, + }, + { + name: 'first', + type: FieldType.number, + values: new ArrayVector([3, 1]), + config: { title: 'First' }, + }, + { + name: 'min', + type: FieldType.number, + values: new ArrayVector([3, 1]), + config: { title: 'Min' }, + }, + { + name: 'max', + type: FieldType.number, + values: new ArrayVector([6, 7]), + config: { title: 'Max' }, + }, + { + name: 'last', + type: FieldType.number, + values: new ArrayVector([6, 7]), + config: { title: 'Last' }, + }, + ]; + + expect(processed.length).toEqual(1); + expect(processed[0].length).toEqual(2); + expect(processed[0].fields).toEqual(expected); + }); + + it('reduces single data frame with many fields', () => { + const cfg = { + id: DataTransformerID.reduce, + options: { + reducers: [ReducerID.first, ReducerID.min, ReducerID.max, ReducerID.last], + }, + }; + const processed = transformDataFrame([cfg], [seriesAWithMultipleFields]); + const expected: Field[] = [ + { + name: 'Field', + type: FieldType.string, + values: new ArrayVector(['temperature', 'humidity']), + config: {}, + }, + { + name: 'first', + type: FieldType.number, + values: new ArrayVector([3, 10000.3]), + config: { title: 'First' }, + }, + { + name: 'min', + type: FieldType.number, + values: new ArrayVector([3, 10000.3]), + config: { title: 'Min' }, + }, + { + name: 'max', + type: FieldType.number, + values: new ArrayVector([6, 10000.6]), + config: { title: 'Max' }, + }, + { + name: 'last', + type: FieldType.number, + values: new ArrayVector([6, 10000.6]), + config: { title: 'Last' }, + }, + ]; + + expect(processed.length).toEqual(1); + expect(processed[0].length).toEqual(2); + expect(processed[0].fields).toEqual(expected); + }); + + it('reduces single data frame with single field', () => { + const cfg = { + id: DataTransformerID.reduce, + options: { + reducers: [ReducerID.first, ReducerID.min, ReducerID.max, ReducerID.last], + }, + }; + const processed = transformDataFrame([cfg], [seriesAWithSingleField]); + const expected: Field[] = [ + { + name: 'Field', + type: FieldType.string, + values: new ArrayVector(['temperature']), + config: {}, + }, + { + name: 'first', + type: FieldType.number, + values: new ArrayVector([3]), + config: { title: 'First' }, + }, + { + name: 'min', + type: FieldType.number, + values: new ArrayVector([3]), + config: { title: 'Min' }, + }, + { + name: 'max', + type: FieldType.number, + values: new ArrayVector([6]), + config: { title: 'Max' }, + }, + { + name: 'last', + type: FieldType.number, + values: new ArrayVector([6]), + config: { title: 'Last' }, + }, + ]; + + expect(processed.length).toEqual(1); + expect(processed[0].length).toEqual(1); + expect(processed[0].fields).toEqual(expected); }); }); diff --git a/packages/grafana-data/src/transformations/transformers/reduce.ts b/packages/grafana-data/src/transformations/transformers/reduce.ts index ca61bc93c2e..043e3dc17ea 100644 --- a/packages/grafana-data/src/transformations/transformers/reduce.ts +++ b/packages/grafana-data/src/transformations/transformers/reduce.ts @@ -1,12 +1,14 @@ import { DataTransformerID } from './ids'; -import { MatcherConfig, DataTransformerInfo } from '../../types/transformations'; -import { ReducerID, fieldReducers, reduceField } from '../fieldReducer'; +import { DataTransformerInfo, MatcherConfig } from '../../types/transformations'; +import { fieldReducers, reduceField, ReducerID } from '../fieldReducer'; import { alwaysFieldMatcher } from '../matchers/predicates'; import { DataFrame, Field, FieldType } from '../../types/dataFrame'; import { ArrayVector } from '../../vector/ArrayVector'; import { KeyValue } from '../../types/data'; import { guessFieldTypeForField } from '../../dataframe/processDataFrame'; import { getFieldMatcher } from '../matchers'; +import { FieldMatcherID } from '../matchers/ids'; +import { filterFieldsTransformer } from './filter'; export interface ReduceTransformerOptions { reducers: ReducerID[]; @@ -15,10 +17,10 @@ export interface ReduceTransformerOptions { export const reduceTransformer: DataTransformerInfo = { id: DataTransformerID.reduce, - name: 'Reducer', - description: 'Return a DataFrame with the reduction results', + name: 'Reduce', + description: 'Reduce all rows to a single row and concatenate all results', defaultOptions: { - reducers: [ReducerID.min, ReducerID.max, ReducerID.mean, ReducerID.last], + reducers: [ReducerID.max], }, /** @@ -32,10 +34,13 @@ export const reduceTransformer: DataTransformerInfo = return (data: DataFrame[]) => { const processed: DataFrame[] = []; - for (const series of data) { + + for (let seriesIndex = 0; seriesIndex < data.length; seriesIndex++) { + const series = data[seriesIndex]; const values: ArrayVector[] = []; const fields: Field[] = []; const byId: KeyValue = {}; + values.push(new ArrayVector()); // The name fields.push({ name: 'Field', @@ -43,10 +48,12 @@ export const reduceTransformer: DataTransformerInfo = values: values[0], config: {}, }); + for (const info of calculators) { const vals = new ArrayVector(); byId[info.id] = vals; values.push(vals); + fields.push({ name: info.id, type: FieldType.other, // UNKNOWN until after we call the functions @@ -57,34 +64,80 @@ export const reduceTransformer: DataTransformerInfo = }, }); } + for (let i = 0; i < series.fields.length; i++) { const field = series.fields[i]; + + if (field.type === FieldType.time) { + continue; + } + if (matcher(field)) { const results = reduceField({ field, reducers, }); + // Update the name list - values[0].buffer.push(field.name); + const seriesName = series.name ?? series.refId ?? seriesIndex; + const fieldName = + field.name === seriesName || data.length === 1 ? field.name : `${field.name} {${seriesName}}`; + + values[0].buffer.push(fieldName); + for (const info of calculators) { const v = results[info.id]; byId[info.id].buffer.push(v); } } } + for (const f of fields) { const t = guessFieldTypeForField(f); + if (t) { f.type = t; } } + processed.push({ ...series, // Same properties, different fields fields, length: values[0].length, }); } - return processed; + + const withoutTime = filterFieldsTransformer.transformer({ exclude: { id: FieldMatcherID.time } })(processed); + return mergeResults(withoutTime); }; }, }; + +const mergeResults = (data: DataFrame[]) => { + if (data.length <= 1) { + return data; + } + + const baseFrame = data[0]; + + for (let seriesIndex = 1; seriesIndex < data.length; seriesIndex++) { + const series = data[seriesIndex]; + + for (const baseField of baseFrame.fields) { + for (const field of series.fields) { + if (baseField.type !== field.type || baseField.name !== field.name) { + continue; + } + + const baseValues: any[] = ((baseField.values as unknown) as ArrayVector).buffer; + const values: any[] = ((field.values as unknown) as ArrayVector).buffer; + ((baseField.values as unknown) as ArrayVector).buffer = baseValues.concat(values); + } + } + } + + baseFrame.name = undefined; + baseFrame.length = baseFrame.fields[0].values.length; + + return [baseFrame]; +}; diff --git a/packages/grafana-ui/src/components/Select/SelectBase.tsx b/packages/grafana-ui/src/components/Select/SelectBase.tsx index 10685c0306a..79ed097a699 100644 --- a/packages/grafana-ui/src/components/Select/SelectBase.tsx +++ b/packages/grafana-ui/src/components/Select/SelectBase.tsx @@ -102,6 +102,7 @@ export function SelectBase({ renderControl, size = 'auto', tabSelectsValue = true, + className, value, width, }: SelectBaseProps) { @@ -277,7 +278,7 @@ export function SelectBase({ width: inputSizesPixels(size), }), }} - className={cx('select-container', widthClass)} + className={cx('select-container', widthClass, className)} {...commonSelectProps} {...creatableProps} {...asyncSelectProps} diff --git a/packages/grafana-ui/src/components/SingleStatShared/FieldDisplayEditor.tsx b/packages/grafana-ui/src/components/SingleStatShared/FieldDisplayEditor.tsx deleted file mode 100644 index 26a82e3dd30..00000000000 --- a/packages/grafana-ui/src/components/SingleStatShared/FieldDisplayEditor.tsx +++ /dev/null @@ -1,97 +0,0 @@ -// Libraries -import React, { PureComponent, ChangeEvent } from 'react'; - -// Components -import { FormLabel } from '../FormLabel/FormLabel'; -import { FormField } from '../FormField/FormField'; -import { StatsPicker } from '../StatsPicker/StatsPicker'; - -// Types -import Select from '../Forms/Legacy/Select/Select'; -import { - ReduceDataOptions, - DEFAULT_FIELD_DISPLAY_VALUES_LIMIT, - ReducerID, - toNumberString, - toIntegerOrUndefined, - SelectableValue, -} from '@grafana/data'; - -const showOptions: Array> = [ - { - value: true, - label: 'All Values', - description: 'Each row in the response data', - }, - { - value: false, - label: 'Calculation', - description: 'Calculate a value based on the response', - }, -]; - -export interface Props { - labelWidth?: number; - value: ReduceDataOptions; - onChange: (value: ReduceDataOptions, event?: React.SyntheticEvent) => void; -} - -export class FieldDisplayEditor extends PureComponent { - onShowValuesChange = (item: SelectableValue) => { - const val = item.value === true; - this.props.onChange({ ...this.props.value, values: val }); - }; - - onCalcsChange = (calcs: string[]) => { - this.props.onChange({ ...this.props.value, calcs }); - }; - - onLimitChange = (event: ChangeEvent) => { - this.props.onChange({ - ...this.props.value, - limit: toIntegerOrUndefined(event.target.value), - }); - }; - - render() { - const { value } = this.props; - const { calcs, values, limit } = value; - - const labelWidth = this.props.labelWidth || 5; - - return ( - <> -
- Show - { return ( - { - onChange({ - ...options, - reducers: stats as ReducerID[], - }); - }} - /> +
+
+
Calculations
+ { + onChange({ + ...options, + reducers: stats as ReducerID[], + }); + }} + /> +
+
); }; @@ -34,6 +39,6 @@ export const reduceTransformRegistryItem: TransformerRegistyItem