Commit bb65113c by Ryan McKinley Committed by GitHub

Transforms: add sort by transformer (#30370)

parent 8c31e259
......@@ -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+.
......
......@@ -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,
};
......@@ -21,4 +21,5 @@ export enum DataTransformerID {
noop = 'noop',
ensureColumns = 'ensureColumns',
groupBy = 'groupBy',
sortBy = 'sortBy',
}
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<SortByTransformerOptions> = {
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<SortByTransformerOptions> = {
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<SortByTransformerOptions> = {
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() };
}
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<SortByTransformerOptions> = {
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)),
};
});
}
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<TransformerUIProps<SortByTransformerOptions>> = ({
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 (
<div>
{sorts.map((s, index) => {
return (
<InlineFieldRow key={`${s.field}/${index}`}>
<InlineField label="Field" labelWidth={10} grow={true}>
<Select
options={fieldNames}
value={fieldNames.find(v => v.value === s.field)}
placeholder="Select field"
onChange={v => {
onSortChange(index, { ...s, field: v.value! });
}}
/>
</InlineField>
<InlineField label="Reverse">
<InlineSwitch
value={!!s.desc}
onChange={() => {
onSortChange(index, { ...s, desc: !!!s.desc });
}}
/>
</InlineField>
</InlineFieldRow>
);
})}
</div>
);
};
export const sortByTransformRegistryItem: TransformerRegistyItem<SortByTransformerOptions> = {
id: DataTransformerID.sortBy,
editor: SortByTransformerEditor,
transformation: standardTransformers.sortByTransformer,
name: standardTransformers.sortByTransformer.name,
description: standardTransformers.sortByTransformer.description,
};
......@@ -8,6 +8,7 @@ import { seriesToFieldsTransformerRegistryItem } from '../components/Transformer
import { calculateFieldTransformRegistryItem } from '../components/TransformersUI/CalculateFieldTransformerEditor';
import { labelsToFieldsTransformerRegistryItem } from '../components/TransformersUI/LabelsToFieldsTransformerEditor';
import { groupByTransformRegistryItem } from '../components/TransformersUI/GroupByTransformerEditor';
import { sortByTransformRegistryItem } from '../components/TransformersUI/SortByTransformerEditor';
import { mergeTransformerRegistryItem } from '../components/TransformersUI/MergeTransformerEditor';
import { seriesToRowsTransformerRegistryItem } from '../components/TransformersUI/SeriesToRowsTransformerEditor';
import { concatenateTransformRegistryItem } from '../components/TransformersUI/ConcatenateTransformerEditor';
......@@ -27,6 +28,7 @@ export const getStandardTransformers = (): Array<TransformerRegistyItem<any>> =>
calculateFieldTransformRegistryItem,
labelsToFieldsTransformerRegistryItem,
groupByTransformRegistryItem,
sortByTransformRegistryItem,
mergeTransformerRegistryItem,
];
};
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment