Transformations: Add "Auto" mode to Organize Fields (#103055)
Co-authored-by: Leon Sorokin <leeoniya@gmail.com> Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com> Co-authored-by: Paul Marbach <paul.marbach@grafana.com>
This commit is contained in:
co-authored by
Leon Sorokin
Isabel Matwawana
Paul Marbach
parent
96f1582c36
commit
5e9bb9f506
@@ -1131,6 +1131,7 @@ Use this transformation to provide the flexibility to rename, reorder, or hide f
|
||||
|
||||
Grafana displays a list of fields returned by the query, allowing you to perform the following actions:
|
||||
|
||||
- **Set field order mode** - If the mode is **Manual**, you can change the field order by hovering the cursor over a field and dragging the field to its new position. If it's **Auto**, use the **OFF**, **ASC**, and **DESC** options to order by any labels on the field or by the field name. You can then drag the labels and the field name to set the priority of the sorting.
|
||||
- **Change field order** - Hover over a field, and when your cursor turns into a hand, drag the field to its new position.
|
||||
- **Hide or show a field** - Use the eye icon next to the field name to toggle the visibility of a specific field.
|
||||
- **Rename fields** - Type a new name in the "Rename <field>" box to customize field names.
|
||||
|
||||
@@ -63,7 +63,13 @@ export { LabelsToFieldsMode, type LabelsToFieldsOptions } from '../transformatio
|
||||
export { type LimitTransformerOptions } from '../transformations/transformers/limit';
|
||||
export { type MergeTransformerOptions } from '../transformations/transformers/merge';
|
||||
export { ReduceTransformerMode, type ReduceTransformerOptions } from '../transformations/transformers/reduce';
|
||||
export { createOrderFieldsComparer } from '../transformations/transformers/order';
|
||||
export {
|
||||
createOrderFieldsComparer,
|
||||
Order,
|
||||
OrderByMode,
|
||||
OrderByType,
|
||||
type OrderByItem,
|
||||
} from '../transformations/transformers/order';
|
||||
export { type RenameByRegexTransformerOptions } from '../transformations/transformers/renameByRegex';
|
||||
export { type OrganizeFieldsTransformerOptions } from '../transformations/transformers/organize';
|
||||
export { compareValues } from '../transformations/matchers/compareValues';
|
||||
|
||||
@@ -5,7 +5,14 @@ import { mockTransformationsRegistry } from '../../utils/tests/mockTransformatio
|
||||
import { transformDataFrame } from '../transformDataFrame';
|
||||
|
||||
import { DataTransformerID } from './ids';
|
||||
import { orderFieldsTransformer, OrderFieldsTransformerOptions } from './order';
|
||||
import {
|
||||
OrderByMode,
|
||||
Order,
|
||||
orderFieldsTransformer,
|
||||
OrderFieldsTransformerOptions,
|
||||
OrderByType,
|
||||
OrderByItem,
|
||||
} from './order';
|
||||
|
||||
describe('Order Transformer', () => {
|
||||
beforeAll(() => {
|
||||
@@ -25,6 +32,7 @@ describe('Order Transformer', () => {
|
||||
const cfg: DataTransformerConfig<OrderFieldsTransformerOptions> = {
|
||||
id: DataTransformerID.order,
|
||||
options: {
|
||||
orderByMode: OrderByMode.Manual,
|
||||
indexByName: {
|
||||
time: 2,
|
||||
temperature: 0,
|
||||
@@ -79,6 +87,7 @@ describe('Order Transformer', () => {
|
||||
id: DataTransformerID.order,
|
||||
disabled: true,
|
||||
options: {
|
||||
orderByMode: OrderByMode.Manual,
|
||||
indexByName: {
|
||||
time: 2,
|
||||
temperature: 0,
|
||||
@@ -143,6 +152,7 @@ describe('Order Transformer', () => {
|
||||
const cfg: DataTransformerConfig<OrderFieldsTransformerOptions> = {
|
||||
id: DataTransformerID.order,
|
||||
options: {
|
||||
orderByMode: OrderByMode.Manual,
|
||||
indexByName: {
|
||||
time: 2,
|
||||
temperature: 0,
|
||||
@@ -207,6 +217,7 @@ describe('Order Transformer', () => {
|
||||
const cfg: DataTransformerConfig<OrderFieldsTransformerOptions> = {
|
||||
id: DataTransformerID.order,
|
||||
options: {
|
||||
orderByMode: OrderByMode.Manual,
|
||||
indexByName: {},
|
||||
},
|
||||
};
|
||||
@@ -220,21 +231,178 @@ describe('Order Transformer', () => {
|
||||
name: 'time',
|
||||
type: FieldType.time,
|
||||
values: [3000, 4000, 5000, 6000],
|
||||
state: {
|
||||
displayName: 'time',
|
||||
multipleFrames: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
config: {},
|
||||
name: 'pressure',
|
||||
type: FieldType.number,
|
||||
values: [10.3, 10.4, 10.5, 10.6],
|
||||
state: {
|
||||
displayName: 'pressure',
|
||||
multipleFrames: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
config: {},
|
||||
name: 'humidity',
|
||||
type: FieldType.number,
|
||||
values: [10000.3, 10000.4, 10000.5, 10000.6],
|
||||
state: {
|
||||
displayName: 'humidity',
|
||||
multipleFrames: false,
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('auto order', () => {
|
||||
it.each`
|
||||
labelPodOrder | labelUserOrder | fieldNameOrder | expectedFieldNameOrder
|
||||
${Order.Off} | ${Order.Off} | ${Order.Off} | ${['Series-1', 'Series-2', 'Series-3']}
|
||||
${Order.Off} | ${Order.Asc} | ${Order.Off} | ${['Series-2', 'Series-1', 'Series-3']}
|
||||
${Order.Off} | ${Order.Off} | ${Order.Asc} | ${['Series-1', 'Series-2', 'Series-3']}
|
||||
${Order.Off} | ${Order.Desc} | ${Order.Off} | ${['Series-1', 'Series-3', 'Series-2']}
|
||||
${Order.Off} | ${Order.Off} | ${Order.Desc} | ${['Series-3', 'Series-2', 'Series-1']}
|
||||
${Order.Off} | ${Order.Desc} | ${Order.Desc} | ${['Series-3', 'Series-1', 'Series-2']}
|
||||
${Order.Off} | ${Order.Asc} | ${Order.Asc} | ${['Series-2', 'Series-1', 'Series-3']}
|
||||
${Order.Off} | ${Order.Asc} | ${Order.Desc} | ${['Series-2', 'Series-3', 'Series-1']}
|
||||
${Order.Off} | ${Order.Desc} | ${Order.Asc} | ${['Series-1', 'Series-3', 'Series-2']}
|
||||
${Order.Asc} | ${Order.Off} | ${Order.Off} | ${['Series-1', 'Series-2', 'Series-3']}
|
||||
${Order.Asc} | ${Order.Off} | ${Order.Asc} | ${['Series-1', 'Series-2', 'Series-3']}
|
||||
${Order.Asc} | ${Order.Off} | ${Order.Desc} | ${['Series-2', 'Series-1', 'Series-3']}
|
||||
${Order.Asc} | ${Order.Asc} | ${Order.Off} | ${['Series-2', 'Series-1', 'Series-3']}
|
||||
${Order.Asc} | ${Order.Desc} | ${Order.Off} | ${['Series-1', 'Series-2', 'Series-3']}
|
||||
${Order.Asc} | ${Order.Desc} | ${Order.Desc} | ${['Series-1', 'Series-2', 'Series-3']}
|
||||
${Order.Asc} | ${Order.Asc} | ${Order.Asc} | ${['Series-2', 'Series-1', 'Series-3']}
|
||||
${Order.Asc} | ${Order.Asc} | ${Order.Desc} | ${['Series-2', 'Series-1', 'Series-3']}
|
||||
${Order.Asc} | ${Order.Desc} | ${Order.Asc} | ${['Series-1', 'Series-2', 'Series-3']}
|
||||
${Order.Desc} | ${Order.Asc} | ${Order.Off} | ${['Series-3', 'Series-2', 'Series-1']}
|
||||
${Order.Desc} | ${Order.Off} | ${Order.Asc} | ${['Series-3', 'Series-1', 'Series-2']}
|
||||
${Order.Desc} | ${Order.Off} | ${Order.Desc} | ${['Series-3', 'Series-2', 'Series-1']}
|
||||
${Order.Desc} | ${Order.Asc} | ${Order.Asc} | ${['Series-3', 'Series-2', 'Series-1']}
|
||||
${Order.Desc} | ${Order.Asc} | ${Order.Desc} | ${['Series-3', 'Series-2', 'Series-1']}
|
||||
${Order.Desc} | ${Order.Desc} | ${Order.Asc} | ${['Series-3', 'Series-1', 'Series-2']}
|
||||
${Order.Desc} | ${Order.Desc} | ${Order.Off} | ${['Series-3', 'Series-1', 'Series-2']}
|
||||
${Order.Desc} | ${Order.Desc} | ${Order.Desc} | ${['Series-3', 'Series-1', 'Series-2']}
|
||||
${Order.Desc} | ${Order.Off} | ${Order.Off} | ${['Series-3', 'Series-1', 'Series-2']}
|
||||
`(
|
||||
'When the order is (with index order as given) label pod: $labelPodOrder / label user: $labelUserOrder / field name: $fieldNameOrder , then the field order is $expectedFieldNameOrder',
|
||||
async ({ labelPodOrder, labelUserOrder, fieldNameOrder, expectedFieldNameOrder }) => {
|
||||
let items: OrderByItem[] = [];
|
||||
|
||||
if (labelPodOrder !== Order.Off) {
|
||||
items.push({ type: OrderByType.Label, name: 'pod', desc: labelPodOrder === Order.Desc });
|
||||
}
|
||||
|
||||
if (labelUserOrder !== Order.Off) {
|
||||
items.push({ type: OrderByType.Label, name: 'user', desc: labelUserOrder === Order.Desc });
|
||||
}
|
||||
|
||||
if (fieldNameOrder !== Order.Off) {
|
||||
items.push({ type: OrderByType.Name, desc: fieldNameOrder === Order.Desc });
|
||||
}
|
||||
|
||||
const cfg: DataTransformerConfig<OrderFieldsTransformerOptions> = {
|
||||
id: DataTransformerID.order,
|
||||
options: {
|
||||
orderByMode: OrderByMode.Auto,
|
||||
orderBy: items,
|
||||
},
|
||||
};
|
||||
|
||||
const data = toDataFrame({
|
||||
name: 'A',
|
||||
fields: [
|
||||
{
|
||||
name: 'Series-1',
|
||||
type: FieldType.number,
|
||||
labels: { pod: 123, user: 555 },
|
||||
values: [10.3],
|
||||
},
|
||||
{
|
||||
name: 'Series-2',
|
||||
type: FieldType.number,
|
||||
labels: { pod: 123, user: 312 },
|
||||
values: [100.3],
|
||||
},
|
||||
{
|
||||
name: 'Series-3',
|
||||
labels: { pod: 456, user: 555 },
|
||||
type: FieldType.number,
|
||||
values: [10000.3],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await expect(transformDataFrame([cfg], [data])).toEmitValuesWith((received) => {
|
||||
const data = received[0];
|
||||
const ordered = data[0];
|
||||
expect(ordered.fields.map((f) => f.name)).toEqual(expectedFieldNameOrder);
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it.each`
|
||||
labelPodIndex | labelUserIndex | fieldNameIndex | expectedFieldNameOrder
|
||||
${0} | ${1} | ${2} | ${['Series-3', 'Series-1', 'Series-2']}
|
||||
${0} | ${2} | ${1} | ${['Series-3', 'Series-2', 'Series-1']}
|
||||
${1} | ${0} | ${2} | ${['Series-3', 'Series-1', 'Series-2']}
|
||||
${2} | ${0} | ${1} | ${['Series-3', 'Series-1', 'Series-2']}
|
||||
${1} | ${2} | ${0} | ${['Series-3', 'Series-2', 'Series-1']}
|
||||
${2} | ${1} | ${0} | ${['Series-3', 'Series-2', 'Series-1']}
|
||||
`(
|
||||
'When the indexes are label pod: $labelPodIndex / label user: $labelUserIndex / field name: $fieldNameIndex when all of them are sort DESC, then the field order is $expectedFieldNameOrder',
|
||||
async ({ labelPodIndex, labelUserIndex, fieldNameIndex, expectedFieldNameOrder }) => {
|
||||
let items: OrderByItem[] = Array(3);
|
||||
|
||||
items[labelPodIndex] = { type: OrderByType.Label, name: 'pod', desc: true };
|
||||
items[labelUserIndex] = { type: OrderByType.Label, name: 'user', desc: true };
|
||||
items[fieldNameIndex] = { type: OrderByType.Name, desc: true };
|
||||
|
||||
const cfg: DataTransformerConfig<OrderFieldsTransformerOptions> = {
|
||||
id: DataTransformerID.order,
|
||||
options: {
|
||||
orderByMode: OrderByMode.Auto,
|
||||
orderBy: items,
|
||||
},
|
||||
};
|
||||
|
||||
const data = toDataFrame({
|
||||
name: 'A',
|
||||
fields: [
|
||||
{
|
||||
name: 'Series-1',
|
||||
type: FieldType.number,
|
||||
labels: { pod: 123, user: 555 },
|
||||
values: [10.3],
|
||||
},
|
||||
{
|
||||
name: 'Series-2',
|
||||
type: FieldType.number,
|
||||
labels: { pod: 123, user: 312 },
|
||||
values: [100.3],
|
||||
},
|
||||
{
|
||||
name: 'Series-3',
|
||||
labels: { pod: 456, user: 555 },
|
||||
type: FieldType.number,
|
||||
values: [10000.3],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await expect(transformDataFrame([cfg], [data])).toEmitValuesWith((received) => {
|
||||
const data = received[0];
|
||||
const ordered = data[0];
|
||||
expect(ordered.fields.map((f) => f.name)).toEqual(expectedFieldNameOrder);
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,14 +1,38 @@
|
||||
import { clone } from 'lodash';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
import { getFieldDisplayName } from '../../field/fieldState';
|
||||
import { cacheFieldDisplayNames, getFieldDisplayName } from '../../field/fieldState';
|
||||
import { DataFrame, Field } from '../../types/dataFrame';
|
||||
import { DataTransformerInfo } from '../../types/transformations';
|
||||
|
||||
import { DataTransformerID } from './ids';
|
||||
|
||||
export enum OrderByMode {
|
||||
Manual = 'manual',
|
||||
Auto = 'auto',
|
||||
}
|
||||
|
||||
export enum Order {
|
||||
Off = 'off',
|
||||
Asc = 'asc',
|
||||
Desc = 'desc',
|
||||
}
|
||||
|
||||
export enum OrderByType {
|
||||
Name = 'name',
|
||||
Label = 'label',
|
||||
}
|
||||
|
||||
export interface OrderByItem {
|
||||
type: OrderByType;
|
||||
name?: string;
|
||||
desc?: boolean;
|
||||
}
|
||||
|
||||
export interface OrderFieldsTransformerOptions {
|
||||
indexByName: Record<string, number>;
|
||||
indexByName?: Record<string, number>;
|
||||
orderByMode?: OrderByMode;
|
||||
orderBy?: OrderByItem[];
|
||||
}
|
||||
|
||||
export const orderFieldsTransformer: DataTransformerInfo<OrderFieldsTransformerOptions> = {
|
||||
@@ -23,36 +47,33 @@ export const orderFieldsTransformer: DataTransformerInfo<OrderFieldsTransformerO
|
||||
* 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) => {
|
||||
const orderer = createFieldsOrderer(options.indexByName);
|
||||
operator:
|
||||
({ indexByName, orderByMode = OrderByMode.Manual, orderBy = [] }) =>
|
||||
(source) =>
|
||||
source.pipe(
|
||||
map((data) => {
|
||||
cacheFieldDisplayNames(data);
|
||||
|
||||
if (!Array.isArray(data) || data.length === 0) {
|
||||
return data;
|
||||
}
|
||||
const orderer =
|
||||
orderByMode === OrderByMode.Manual
|
||||
? createFieldsOrdererManual(indexByName!)
|
||||
: createFieldsOrdererAuto(orderBy);
|
||||
|
||||
return data.map((frame) => ({
|
||||
...frame,
|
||||
fields: orderer(frame.fields, data, frame),
|
||||
}));
|
||||
})
|
||||
),
|
||||
return data.map((frame) => ({
|
||||
...frame,
|
||||
fields: orderer(frame.fields, data, frame),
|
||||
}));
|
||||
})
|
||||
),
|
||||
};
|
||||
|
||||
export const createOrderFieldsComparer = (indexByName: Record<string, number>) => (a: string, b: string) => {
|
||||
return indexOfField(a, indexByName) - indexOfField(b, indexByName);
|
||||
};
|
||||
export const createOrderFieldsComparer = (indexByName: Record<string, number>) => (a: string, b: string) =>
|
||||
indexOfField(a, indexByName) - indexOfField(b, indexByName);
|
||||
|
||||
const createFieldsOrderer =
|
||||
const createFieldsOrdererManual =
|
||||
(indexByName: Record<string, number>) => (fields: Field[], data: DataFrame[], frame: DataFrame) => {
|
||||
if (!Array.isArray(fields) || fields.length === 0) {
|
||||
return fields;
|
||||
}
|
||||
if (!indexByName || Object.keys(indexByName).length === 0) {
|
||||
return fields;
|
||||
}
|
||||
const comparer = createOrderFieldsComparer(indexByName);
|
||||
|
||||
return clone(fields).sort((a, b) =>
|
||||
comparer(getFieldDisplayName(a, frame, data), getFieldDisplayName(b, frame, data))
|
||||
);
|
||||
@@ -64,3 +85,24 @@ const indexOfField = (fieldName: string, indexByName: Record<string, number>) =>
|
||||
}
|
||||
return Number.MAX_SAFE_INTEGER;
|
||||
};
|
||||
|
||||
const compare = new Intl.Collator(undefined, { sensitivity: 'base', numeric: true }).compare;
|
||||
|
||||
/** @internal */
|
||||
export const createFieldsOrdererAuto = (orderBy: OrderByItem[]) => (fields: Field[]) =>
|
||||
fields.slice().sort((fieldA, fieldB) => {
|
||||
for (let i = 0; i < orderBy.length; i++) {
|
||||
let { type, name = '', desc = false } = orderBy[i];
|
||||
|
||||
let aVal = type === OrderByType.Name ? (fieldA.state?.displayName ?? fieldA.name) : (fieldA.labels?.[name] ?? '');
|
||||
let bVal = type === OrderByType.Name ? (fieldB.state?.displayName ?? fieldB.name) : (fieldB.labels?.[name] ?? '');
|
||||
|
||||
let res = compare(aVal, bVal) * (desc ? -1 : 1);
|
||||
|
||||
if (res !== 0) {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
@@ -94,6 +94,10 @@ describe('OrganizeFields Transformer', () => {
|
||||
name: 'time',
|
||||
type: FieldType.time,
|
||||
values: [3000, 4000, 5000, 6000],
|
||||
state: {
|
||||
displayName: 'time',
|
||||
multipleFrames: false,
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -1152,6 +1152,7 @@ Use this transformation to provide the flexibility to rename, reorder, or hide f
|
||||
|
||||
Grafana displays a list of fields returned by the query, allowing you to perform the following actions:
|
||||
|
||||
- **Set field order mode** - If the mode is **Manual**, you can change the field order by hovering the cursor over a field and dragging the field to its new position. If it's **Auto**, use the **OFF**, **ASC**, and **DESC** options to order by any labels on the field or by the field name. You can then drag the labels and the field name to set the priority of the sorting.
|
||||
- **Change field order** - Hover over a field, and when your cursor turns into a hand, drag the field to its new position.
|
||||
- **Hide or show a field** - Use the eye icon next to the field name to toggle the visibility of a specific field.
|
||||
- **Rename fields** - Type a new name in the "Rename <field>" box to customize field names.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useCallback, useId, useMemo } from 'react';
|
||||
|
||||
import {
|
||||
DataTransformerID,
|
||||
@@ -10,7 +10,14 @@ import {
|
||||
TransformerUIProps,
|
||||
TransformerCategory,
|
||||
} from '@grafana/data';
|
||||
import { createOrderFieldsComparer, OrganizeFieldsTransformerOptions } from '@grafana/data/internal';
|
||||
import {
|
||||
createOrderFieldsComparer,
|
||||
Order,
|
||||
OrderByItem,
|
||||
OrderByMode,
|
||||
OrderByType,
|
||||
OrganizeFieldsTransformerOptions,
|
||||
} from '@grafana/data/internal';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import {
|
||||
Input,
|
||||
@@ -22,18 +29,102 @@ import {
|
||||
InlineLabel,
|
||||
Text,
|
||||
Box,
|
||||
InlineField,
|
||||
InlineFieldRow,
|
||||
RadioButtonGroup,
|
||||
} from '@grafana/ui';
|
||||
|
||||
import { createFieldsOrdererAuto } from '../../../../../packages/grafana-data/src/transformations/transformers/order';
|
||||
import { getTransformationContent } from '../docs/getTransformationContent';
|
||||
import { useAllFieldNamesFromDataFrames } from '../utils';
|
||||
import { getAllFieldNamesFromDataFrames, getDistinctLabels, useAllFieldNamesFromDataFrames } from '../utils';
|
||||
|
||||
interface OrganizeFieldsTransformerEditorProps extends TransformerUIProps<OrganizeFieldsTransformerOptions> {}
|
||||
|
||||
interface UIOrderByItem {
|
||||
type: OrderByType;
|
||||
name?: string;
|
||||
order: Order;
|
||||
}
|
||||
|
||||
function move(arr: unknown[], from: number, to: number) {
|
||||
arr.splice(to, 0, arr.splice(from, 1)[0]);
|
||||
}
|
||||
|
||||
const OrganizeFieldsTransformerEditor = ({ options, input, onChange }: OrganizeFieldsTransformerEditorProps) => {
|
||||
const { indexByName, excludeByName, renameByName, includeByName } = options;
|
||||
const { indexByName, excludeByName, renameByName, includeByName, orderBy, orderByMode } = options;
|
||||
|
||||
const fieldNames = useAllFieldNamesFromDataFrames(input);
|
||||
const orderedFieldNames = useMemo(() => orderFieldNamesByIndex(fieldNames, indexByName), [fieldNames, indexByName]);
|
||||
const orderedFieldNames = useMemo(() => {
|
||||
if (input.length > 0 && orderByMode === OrderByMode.Auto) {
|
||||
const autoOrderer = createFieldsOrdererAuto(orderBy ?? []);
|
||||
|
||||
return getAllFieldNamesFromDataFrames(
|
||||
[
|
||||
{
|
||||
...input[0],
|
||||
fields: autoOrderer(input[0].fields),
|
||||
},
|
||||
],
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
return orderFieldNamesByIndex(fieldNames, indexByName);
|
||||
}, [input, fieldNames, indexByName, orderByMode, orderBy]);
|
||||
|
||||
const uiOrderByItems = useMemo(() => {
|
||||
const uiOrderByItems: UIOrderByItem[] = [];
|
||||
|
||||
if (orderByMode === OrderByMode.Auto) {
|
||||
const foundLabels = getDistinctLabels(input);
|
||||
|
||||
let byFieldNameAdded = false;
|
||||
|
||||
// add Asc or Desc items
|
||||
orderBy?.forEach((item, index) => {
|
||||
let order = item.desc ? Order.Desc : Order.Asc;
|
||||
|
||||
// by field name
|
||||
if (item.type === OrderByType.Name) {
|
||||
uiOrderByItems.push({
|
||||
type: OrderByType.Name,
|
||||
order,
|
||||
});
|
||||
|
||||
byFieldNameAdded = true;
|
||||
}
|
||||
// by label
|
||||
else if (foundLabels.has(item.name!)) {
|
||||
uiOrderByItems.push({
|
||||
type: OrderByType.Label,
|
||||
name: item.name,
|
||||
order,
|
||||
});
|
||||
|
||||
foundLabels.delete(item.name!);
|
||||
}
|
||||
});
|
||||
|
||||
// add Off items
|
||||
if (!byFieldNameAdded) {
|
||||
uiOrderByItems.push({
|
||||
type: OrderByType.Name,
|
||||
order: Order.Off,
|
||||
});
|
||||
}
|
||||
|
||||
foundLabels.forEach((name) => {
|
||||
uiOrderByItems.push({
|
||||
type: OrderByType.Label,
|
||||
name,
|
||||
order: Order.Off,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return uiOrderByItems;
|
||||
}, [input, orderByMode, orderBy]);
|
||||
|
||||
const filterType = includeByName && Object.keys(includeByName).length > 0 ? 'include' : 'exclude';
|
||||
|
||||
const onToggleVisibility = useCallback(
|
||||
@@ -63,7 +154,7 @@ const OrganizeFieldsTransformerEditor = ({ options, input, onChange }: OrganizeF
|
||||
[onChange, options, includeByName]
|
||||
);
|
||||
|
||||
const onDragEnd = useCallback(
|
||||
const onDragEndFields = useCallback(
|
||||
(result: DropResult) => {
|
||||
if (!result || !result.destination) {
|
||||
return;
|
||||
@@ -97,6 +188,59 @@ const OrganizeFieldsTransformerEditor = ({ options, input, onChange }: OrganizeF
|
||||
[onChange, options]
|
||||
);
|
||||
|
||||
const onChangeSort = useCallback(
|
||||
(item: UIOrderByItem, sortOrder: Order) => {
|
||||
item.order = sortOrder;
|
||||
|
||||
const orderBy: OrderByItem[] = [];
|
||||
|
||||
uiOrderByItems.forEach((item) => {
|
||||
if (item.order !== Order.Off) {
|
||||
orderBy.push({
|
||||
type: item.type,
|
||||
name: item.name,
|
||||
desc: item.order === Order.Desc,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
onChange({ ...options, orderBy });
|
||||
},
|
||||
[options, uiOrderByItems, onChange]
|
||||
);
|
||||
|
||||
const onDragEndLabels = useCallback(
|
||||
(result: DropResult) => {
|
||||
if (result.destination == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const startIndex = result.source.index;
|
||||
const endIndex = result.destination.index;
|
||||
|
||||
if (startIndex === endIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
move(uiOrderByItems, startIndex, endIndex);
|
||||
|
||||
const orderBy: OrderByItem[] = [];
|
||||
|
||||
uiOrderByItems.forEach((item) => {
|
||||
if (item.order !== Order.Off) {
|
||||
orderBy.push({
|
||||
type: item.type,
|
||||
name: item.name,
|
||||
desc: item.order === Order.Desc,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
onChange({ ...options, orderBy });
|
||||
},
|
||||
[options, onChange, uiOrderByItems]
|
||||
);
|
||||
|
||||
// Show warning that we only apply the first frame
|
||||
if (input.length > 1) {
|
||||
return (
|
||||
@@ -109,36 +253,84 @@ const OrganizeFieldsTransformerEditor = ({ options, input, onChange }: OrganizeF
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Droppable droppableId="sortable-fields-transformer" direction="vertical">
|
||||
{(provided) => (
|
||||
<div ref={provided.innerRef} {...provided.droppableProps}>
|
||||
{orderedFieldNames.map((fieldName, index) => {
|
||||
const isIncludeFilter = includeByName && fieldName in includeByName ? includeByName[fieldName] : false;
|
||||
const isVisible = filterType === 'include' ? isIncludeFilter : !excludeByName[fieldName];
|
||||
const onToggleFunction = filterType === 'include' ? onToggleVisibilityInclude : onToggleVisibility;
|
||||
const styles = useStyles2(getDraggableStyles);
|
||||
|
||||
return (
|
||||
<>
|
||||
<InlineFieldRow>
|
||||
<InlineField label={t('transformers.organize-fields-transformer-editor.field-order', 'Field order')}>
|
||||
<RadioButtonGroup
|
||||
options={[
|
||||
{
|
||||
label: t('transformers.organize-fields-transformer-editor.field-order-manual', 'Manual'),
|
||||
value: OrderByMode.Manual,
|
||||
},
|
||||
{
|
||||
label: t('transformers.organize-fields-transformer-editor.field-order-auto', 'Auto'),
|
||||
value: OrderByMode.Auto,
|
||||
},
|
||||
]}
|
||||
value={options.orderByMode ?? OrderByMode.Manual}
|
||||
onChange={(v) => onChange({ ...options, orderByMode: v })}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
<DragDropContext onDragEnd={onDragEndLabels}>
|
||||
{options.orderByMode === OrderByMode.Auto && (
|
||||
<Droppable droppableId="sortable-labels-transformer" direction="vertical">
|
||||
{(provided) => {
|
||||
return (
|
||||
<DraggableFieldName
|
||||
fieldName={fieldName}
|
||||
renamedFieldName={renameByName[fieldName]}
|
||||
index={index}
|
||||
onToggleVisibility={onToggleFunction}
|
||||
onRenameField={onRenameField}
|
||||
visible={isVisible}
|
||||
key={fieldName}
|
||||
/>
|
||||
<>
|
||||
<div ref={provided.innerRef} className={styles.labelsDraggable} {...provided.droppableProps}>
|
||||
{uiOrderByItems.map((item, idx) => (
|
||||
<DraggableUIOrderByItem item={item} index={idx} onChangeSort={onChangeSort} />
|
||||
))}
|
||||
</div>
|
||||
{provided.placeholder}
|
||||
</>
|
||||
);
|
||||
})}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
}}
|
||||
</Droppable>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
</DragDropContext>
|
||||
|
||||
<DragDropContext onDragEnd={onDragEndFields}>
|
||||
<Droppable droppableId="sortable-fields-transformer" direction="vertical">
|
||||
{(provided) => (
|
||||
<div ref={provided.innerRef} {...provided.droppableProps}>
|
||||
{orderedFieldNames.map((fieldName, index) => {
|
||||
const isIncludeFilter = includeByName && fieldName in includeByName ? includeByName[fieldName] : false;
|
||||
const isVisible = filterType === 'include' ? isIncludeFilter : !excludeByName[fieldName];
|
||||
const onToggleFunction = filterType === 'include' ? onToggleVisibilityInclude : onToggleVisibility;
|
||||
|
||||
return (
|
||||
<DraggableFieldName
|
||||
fieldName={fieldName}
|
||||
renamedFieldName={renameByName[fieldName]}
|
||||
index={index}
|
||||
onToggleVisibility={onToggleFunction}
|
||||
onRenameField={onRenameField}
|
||||
visible={isVisible}
|
||||
key={fieldName}
|
||||
isDragDisabled={options.orderByMode === OrderByMode.Auto}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const getDraggableStyles = (theme: GrafanaTheme2) => ({
|
||||
labelsDraggable: css({
|
||||
marginBottom: theme.spacing(3),
|
||||
}),
|
||||
});
|
||||
|
||||
OrganizeFieldsTransformerEditor.displayName = 'OrganizeFieldsTransformerEditor';
|
||||
|
||||
interface DraggableFieldProps {
|
||||
@@ -148,6 +340,7 @@ interface DraggableFieldProps {
|
||||
visible: boolean;
|
||||
onToggleVisibility: (fieldName: string, isVisible: boolean) => void;
|
||||
onRenameField: (from: string, to: string) => void;
|
||||
isDragDisabled: boolean;
|
||||
}
|
||||
|
||||
const DraggableFieldName = ({
|
||||
@@ -157,26 +350,29 @@ const DraggableFieldName = ({
|
||||
visible,
|
||||
onToggleVisibility,
|
||||
onRenameField,
|
||||
isDragDisabled,
|
||||
}: DraggableFieldProps) => {
|
||||
const styles = useStyles2(getFieldNameStyles);
|
||||
|
||||
return (
|
||||
<Draggable draggableId={fieldName} index={index}>
|
||||
<Draggable draggableId={fieldName} index={index} isDragDisabled={isDragDisabled}>
|
||||
{(provided) => (
|
||||
<Box marginBottom={0.5} display="flex" gap={0} ref={provided.innerRef} {...provided.draggableProps}>
|
||||
<Box display="flex" gap={0} ref={provided.innerRef} {...provided.draggableProps}>
|
||||
<InlineLabel width={60} as="div">
|
||||
<Stack gap={0} justifyContent="flex-start" alignItems="center" width="100%">
|
||||
<span {...provided.dragHandleProps}>
|
||||
<Icon
|
||||
name="draggabledots"
|
||||
title={t(
|
||||
'transformers.draggable-field-name.title-drag-and-drop-to-reorder',
|
||||
'Drag and drop to reorder'
|
||||
)}
|
||||
size="lg"
|
||||
className={styles.draggable}
|
||||
/>
|
||||
</span>
|
||||
{!isDragDisabled && (
|
||||
<span {...provided.dragHandleProps}>
|
||||
<Icon
|
||||
name="draggabledots"
|
||||
title={t(
|
||||
'transformers.draggable-field-name.title-drag-and-drop-to-reorder',
|
||||
'Drag and drop to reorder'
|
||||
)}
|
||||
size="lg"
|
||||
className={styles.draggable}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
<IconButton
|
||||
className={styles.toggle}
|
||||
size="md"
|
||||
@@ -197,6 +393,7 @@ const DraggableFieldName = ({
|
||||
defaultValue={renamedFieldName || ''}
|
||||
placeholder={t('transformers.draggable-field-name.rename-placeholder', 'Rename {{fieldName}}', {
|
||||
fieldName,
|
||||
interpolation: { escapeValue: false },
|
||||
})}
|
||||
onBlur={(event) => onRenameField(fieldName, event.currentTarget.value)}
|
||||
/>
|
||||
@@ -208,6 +405,57 @@ const DraggableFieldName = ({
|
||||
|
||||
DraggableFieldName.displayName = 'DraggableFieldName';
|
||||
|
||||
interface DraggableUIOrderByItemProps {
|
||||
item: UIOrderByItem;
|
||||
index: number;
|
||||
onChangeSort: (item: UIOrderByItem, order: Order) => void;
|
||||
}
|
||||
|
||||
const DraggableUIOrderByItem = ({ index, item, onChangeSort }: DraggableUIOrderByItemProps) => {
|
||||
const styles = useStyles2(getFieldNameStyles);
|
||||
const draggableId = useId();
|
||||
|
||||
return (
|
||||
<Draggable draggableId={draggableId} index={index} isDragDisabled={item.order === Order.Off}>
|
||||
{(provided) => (
|
||||
<Box marginBottom={0.5} display="flex" gap={0} ref={provided.innerRef} {...provided.draggableProps}>
|
||||
<InlineLabel width={60} as="div">
|
||||
<Stack gap={3} justifyContent="flex-start" alignItems="center" width="100%">
|
||||
<span {...provided.dragHandleProps}>
|
||||
<Icon
|
||||
name="draggabledots"
|
||||
title={t(
|
||||
'transformers.draggable-field-name.title-drag-and-drop-to-reorder',
|
||||
'Drag and drop to reorder'
|
||||
)}
|
||||
size="lg"
|
||||
className={styles.draggable}
|
||||
/>
|
||||
</span>
|
||||
<Text truncate={true} element="p" variant="bodySmall" weight="bold">
|
||||
{item.type === OrderByType.Label ? `Label: ${item.name}` : `Field name`}
|
||||
</Text>
|
||||
</Stack>
|
||||
</InlineLabel>
|
||||
<RadioButtonGroup
|
||||
options={[
|
||||
{ label: t('transformers.draggable-sort-order.off', 'Off'), value: Order.Off },
|
||||
{ label: t('transformers.draggable-sort-order.asc', 'ASC'), value: Order.Asc },
|
||||
{ label: t('transformers.draggable-sort-order.desc', 'DESC'), value: Order.Desc },
|
||||
]}
|
||||
value={item.order}
|
||||
onChange={(order) => {
|
||||
onChangeSort(item, order);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
};
|
||||
|
||||
DraggableUIOrderByItem.displayName = 'DraggableUIOrderByItem';
|
||||
|
||||
const getFieldNameStyles = (theme: GrafanaTheme2) => ({
|
||||
toggle: css({
|
||||
margin: theme.spacing(0, 1),
|
||||
|
||||
@@ -10665,6 +10665,11 @@
|
||||
"tooltip-disable": "Disable",
|
||||
"tooltip-enable": "Enable"
|
||||
},
|
||||
"draggable-sort-order": {
|
||||
"asc": "ASC",
|
||||
"desc": "DESC",
|
||||
"off": "Off"
|
||||
},
|
||||
"enum-mapping-editor": {
|
||||
"add-enum-value": "Add enum value",
|
||||
"generate-enum-values-from-data": "Generate enum values from data"
|
||||
@@ -10843,6 +10848,9 @@
|
||||
"time-in-ascending-order": "Time in ascending order"
|
||||
},
|
||||
"organize-fields-transformer-editor": {
|
||||
"field-order": "Field order",
|
||||
"field-order-auto": "Auto",
|
||||
"field-order-manual": "Manual",
|
||||
"first-frame-warning": "Organize fields only works with a single frame. Consider applying a join transformation or filtering the input first."
|
||||
},
|
||||
"partion-by-values-editor": {
|
||||
|
||||
Reference in New Issue
Block a user