diff --git a/docs/sources/panels/transformations/types-options.md b/docs/sources/panels/transformations/types-options.md index 3c490c7e731..3ffeaa81939 100644 --- a/docs/sources/panels/transformations/types-options.md +++ b/docs/sources/panels/transformations/types-options.md @@ -17,6 +17,7 @@ Grafana comes with the following transformations: - [Add field from calculation](#add-field-from-calculation) - [Labels to fields](#labels-to-fields) - [Concatenate fields](#concatenate-fields) +- [Sort by](#sort-by) - [Group by](#group-by) - [Merge](#merge) - [Rename by regex](#rename-by-regex) @@ -236,6 +237,14 @@ After merge: | 2020-07-07 11:34:20 | ServerA | 10 | | | 2020-07-07 11:34:20 | | 20 | EU | +## Sort by + +> **Note:** This transformation is available in Grafana 7.4+. + +This transformation will sort each frame by the configured field, When `reverse` is checked, the values will return in +the opposite order. + + ## Group by > **Note:** This transformation is available in Grafana 7.2+. diff --git a/packages/grafana-data/src/transformations/transformers.ts b/packages/grafana-data/src/transformations/transformers.ts index a14560b4ac3..c76c703e436 100644 --- a/packages/grafana-data/src/transformations/transformers.ts +++ b/packages/grafana-data/src/transformations/transformers.ts @@ -13,6 +13,7 @@ import { renameFieldsTransformer } from './transformers/rename'; import { labelsToFieldsTransformer } from './transformers/labelsToFields'; import { ensureColumnsTransformer } from './transformers/ensureColumns'; import { groupByTransformer } from './transformers/groupBy'; +import { sortByTransformer } from './transformers/sortBy'; import { mergeTransformer } from './transformers/merge'; import { renameByRegexTransformer } from './transformers/renameByRegex'; import { filterByValueTransformer } from './transformers/filterByValue'; @@ -35,6 +36,7 @@ export const standardTransformers = { labelsToFieldsTransformer, ensureColumnsTransformer, groupByTransformer, + sortByTransformer, mergeTransformer, renameByRegexTransformer, }; diff --git a/packages/grafana-data/src/transformations/transformers/ids.ts b/packages/grafana-data/src/transformations/transformers/ids.ts index cc0b1535122..1aaf5773610 100644 --- a/packages/grafana-data/src/transformations/transformers/ids.ts +++ b/packages/grafana-data/src/transformations/transformers/ids.ts @@ -21,4 +21,5 @@ export enum DataTransformerID { noop = 'noop', ensureColumns = 'ensureColumns', groupBy = 'groupBy', + sortBy = 'sortBy', } diff --git a/packages/grafana-data/src/transformations/transformers/sortBy.test.ts b/packages/grafana-data/src/transformations/transformers/sortBy.test.ts new file mode 100644 index 00000000000..ce139eb1b99 --- /dev/null +++ b/packages/grafana-data/src/transformations/transformers/sortBy.test.ts @@ -0,0 +1,99 @@ +import { toDataFrame } from '../../dataframe/processDataFrame'; +import { sortByTransformer, SortByTransformerOptions } from './sortBy'; +import { mockTransformationsRegistry } from '../../utils/tests/mockTransformationsRegistry'; +import { transformDataFrame } from '../transformDataFrame'; +import { Field, FieldType } from '../../types'; +import { DataTransformerID } from './ids'; +import { DataTransformerConfig } from '@grafana/data'; + +const testFrame = toDataFrame({ + name: 'A', + fields: [ + { name: 'time', type: FieldType.time, values: [10, 9, 8, 7, 6, 5] }, // desc + { name: 'text', type: FieldType.string, values: ['a', 'z', 'b', 'x', 'c'] }, + { name: 'count', type: FieldType.string, values: [1, 2, 3, 4, 5] }, // asc + ], +}); + +describe('SortBy transformer', () => { + beforeAll(() => { + mockTransformationsRegistry([sortByTransformer]); + }); + + it('should not apply transformation if config is missing sort fields', async () => { + const cfg: DataTransformerConfig = { + id: DataTransformerID.sortBy, + options: { + sort: [], // nothing + }, + }; + + await expect(transformDataFrame([cfg], [testFrame])).toEmitValuesWith(received => { + const result = received[0]; + expect(result[0]).toBe(testFrame); + }); + }); + + it('should sort time asc', async () => { + const cfg: DataTransformerConfig = { + id: DataTransformerID.sortBy, + options: { + sort: [ + { + field: 'time', + }, + ], + }, + }; + + await expect(transformDataFrame([cfg], [testFrame])).toEmitValuesWith(received => { + expect(getFieldSnapshot(received[0][0].fields[0])).toMatchInlineSnapshot(` + Object { + "name": "time", + "values": Array [ + 5, + 6, + 7, + 8, + 9, + 10, + ], + } + `); + }); + }); + + it('should sort time (desc)', async () => { + const cfg: DataTransformerConfig = { + id: DataTransformerID.sortBy, + options: { + sort: [ + { + field: 'time', + desc: true, + }, + ], + }, + }; + + await expect(transformDataFrame([cfg], [testFrame])).toEmitValuesWith(received => { + expect(getFieldSnapshot(received[0][0].fields[0])).toMatchInlineSnapshot(` + Object { + "name": "time", + "values": Array [ + 10, + 9, + 8, + 7, + 6, + 5, + ], + } + `); + }); + }); +}); + +function getFieldSnapshot(f: Field): Object { + return { name: f.name, values: f.values.toArray() }; +} diff --git a/packages/grafana-data/src/transformations/transformers/sortBy.ts b/packages/grafana-data/src/transformations/transformers/sortBy.ts new file mode 100644 index 00000000000..dbd0a9627f8 --- /dev/null +++ b/packages/grafana-data/src/transformations/transformers/sortBy.ts @@ -0,0 +1,65 @@ +import { map } from 'rxjs/operators'; + +import { DataTransformerID } from './ids'; +import { DataTransformerInfo } from '../../types/transformations'; +import { DataFrame } from '../../types'; +import { getFieldDisplayName } from '../../field'; +import { sortDataFrame } from '../../dataframe'; + +export interface SortByField { + field: string; + desc?: boolean; + index?: number; +} + +export interface SortByTransformerOptions { + // NOTE: this structure supports an array, however only the first entry is used + // future versions may support multi-sort options + sort: SortByField[]; +} + +export const sortByTransformer: DataTransformerInfo = { + id: DataTransformerID.sortBy, + name: 'Sort by', + description: 'Sort fields in a frame', + defaultOptions: { + fields: {}, + }, + + /** + * Return a modified copy of the series. If the transform is not or should not + * be applied, just return the input series + */ + operator: options => source => + source.pipe( + map(data => { + if (!Array.isArray(data) || data.length === 0 || !options?.sort?.length) { + return data; + } + return sortDataFrames(data, options.sort); + }) + ), +}; + +export function sortDataFrames(data: DataFrame[], sort: SortByField[]): DataFrame[] { + return data.map(frame => { + const s = attachFieldIndex(frame, sort); + if (s.length && s[0].index != null) { + return sortDataFrame(frame, s[0].index, s[0].desc); + } + return frame; + }); +} + +function attachFieldIndex(frame: DataFrame, sort: SortByField[]): SortByField[] { + return sort.map(s => { + if (s.index != null) { + // null or undefined + return s; + } + return { + ...s, + index: frame.fields.findIndex(f => s.field === getFieldDisplayName(f, frame)), + }; + }); +} diff --git a/public/app/core/components/TransformersUI/SortByTransformerEditor.tsx b/public/app/core/components/TransformersUI/SortByTransformerEditor.tsx new file mode 100644 index 00000000000..768a99a2434 --- /dev/null +++ b/public/app/core/components/TransformersUI/SortByTransformerEditor.tsx @@ -0,0 +1,68 @@ +import React, { useCallback, useMemo } from 'react'; +import { DataTransformerID, standardTransformers, TransformerRegistyItem, TransformerUIProps } from '@grafana/data'; +import { getAllFieldNamesFromDataFrames } from './OrganizeFieldsTransformerEditor'; +import { InlineField, InlineSwitch, InlineFieldRow, Select } from '@grafana/ui'; + +import { SortByField, SortByTransformerOptions } from '@grafana/data/src/transformations/transformers/sortBy'; + +export const SortByTransformerEditor: React.FC> = ({ + input, + options, + onChange, +}) => { + const fieldNames = useMemo( + () => + getAllFieldNamesFromDataFrames(input).map(n => ({ + value: n, + label: n, + })), + [input] + ); + + // Only supports single sort for now + const onSortChange = useCallback( + (idx: number, cfg: SortByField) => { + onChange({ ...options, sort: [cfg] }); + }, + [options] + ); + + const sorts = options.sort?.length ? options.sort : [{} as SortByField]; + + return ( +
+ {sorts.map((s, index) => { + return ( + + +