mirror of
https://github.com/grafana/grafana.git
synced 2025-12-22 20:54:34 +08:00
Compare commits
21 Commits
docs/add-t
...
kristina/s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74dfaa4b7c | ||
|
|
96b2ed367e | ||
|
|
09b7d424d4 | ||
|
|
7622a81158 | ||
|
|
7454997166 | ||
|
|
caa1202d54 | ||
|
|
1410180aed | ||
|
|
23e97f9a4a | ||
|
|
0db9227da8 | ||
|
|
84deb76b21 | ||
|
|
84126470de | ||
|
|
3644e00d7f | ||
|
|
705646c8f9 | ||
|
|
0c25bc2b27 | ||
|
|
c368196e35 | ||
|
|
c1da8e2dff | ||
|
|
883fb2de83 | ||
|
|
86faef83c2 | ||
|
|
ddca5c0a0a | ||
|
|
8f7ddd3c4c | ||
|
|
141885acae |
@@ -127,6 +127,8 @@ DataTopic: "series" | "annotations" | "alertStates" @cog(kind="enum",memberNames
|
|||||||
DataTransformerConfig: {
|
DataTransformerConfig: {
|
||||||
// Unique identifier of transformer
|
// Unique identifier of transformer
|
||||||
id: string
|
id: string
|
||||||
|
// Unique identifier of the instance of the transformer
|
||||||
|
refId?: string
|
||||||
// Disabled transformations are skipped
|
// Disabled transformations are skipped
|
||||||
disabled?: bool
|
disabled?: bool
|
||||||
// Optional frame matcher. When missing it will be applied to all results
|
// Optional frame matcher. When missing it will be applied to all results
|
||||||
|
|||||||
@@ -516,6 +516,8 @@ lineage: schemas: [{
|
|||||||
#DataTransformerConfig: {
|
#DataTransformerConfig: {
|
||||||
// Unique identifier of transformer
|
// Unique identifier of transformer
|
||||||
id: string
|
id: string
|
||||||
|
// Unique identifier of the instance of the transformer
|
||||||
|
refId?: string
|
||||||
// Disabled transformations are skipped
|
// Disabled transformations are skipped
|
||||||
disabled?: bool
|
disabled?: bool
|
||||||
// Optional frame matcher. When missing it will be applied to all results
|
// Optional frame matcher. When missing it will be applied to all results
|
||||||
|
|||||||
@@ -516,6 +516,8 @@ lineage: schemas: [{
|
|||||||
#DataTransformerConfig: {
|
#DataTransformerConfig: {
|
||||||
// Unique identifier of transformer
|
// Unique identifier of transformer
|
||||||
id: string
|
id: string
|
||||||
|
// Unique identifier of the instance of the transformer
|
||||||
|
refId?: string
|
||||||
// Disabled transformations are skipped
|
// Disabled transformations are skipped
|
||||||
disabled?: bool
|
disabled?: bool
|
||||||
// Optional frame matcher. When missing it will be applied to all results
|
// Optional frame matcher. When missing it will be applied to all results
|
||||||
|
|||||||
@@ -131,6 +131,8 @@ DataTopic: "series" | "annotations" | "alertStates" @cog(kind="enum",memberNames
|
|||||||
DataTransformerConfig: {
|
DataTransformerConfig: {
|
||||||
// Unique identifier of transformer
|
// Unique identifier of transformer
|
||||||
id: string
|
id: string
|
||||||
|
// Unique identifier of the instance of the transformer
|
||||||
|
refId?: string
|
||||||
// Disabled transformations are skipped
|
// Disabled transformations are skipped
|
||||||
disabled?: bool
|
disabled?: bool
|
||||||
// Optional frame matcher. When missing it will be applied to all results
|
// Optional frame matcher. When missing it will be applied to all results
|
||||||
|
|||||||
@@ -225,6 +225,8 @@ func NewDashboardTransformationKind() *DashboardTransformationKind {
|
|||||||
type DashboardDataTransformerConfig struct {
|
type DashboardDataTransformerConfig struct {
|
||||||
// Unique identifier of transformer
|
// Unique identifier of transformer
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
|
// Unique identifier of the instance of the transformer
|
||||||
|
RefId *string `json:"refId,omitempty"`
|
||||||
// Disabled transformations are skipped
|
// Disabled transformations are skipped
|
||||||
Disabled *bool `json:"disabled,omitempty"`
|
Disabled *bool `json:"disabled,omitempty"`
|
||||||
// Optional frame matcher. When missing it will be applied to all results
|
// Optional frame matcher. When missing it will be applied to all results
|
||||||
|
|||||||
@@ -1707,6 +1707,13 @@ func schema_pkg_apis_dashboard_v2beta1_DashboardDataTransformerConfig(ref common
|
|||||||
Format: "",
|
Format: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"refId": {
|
||||||
|
SchemaProps: spec.SchemaProps{
|
||||||
|
Description: "Unique identifier of the instance of the transformer",
|
||||||
|
Type: []string{"string"},
|
||||||
|
Format: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
"disabled": {
|
"disabled": {
|
||||||
SchemaProps: spec.SchemaProps{
|
SchemaProps: spec.SchemaProps{
|
||||||
Description: "Disabled transformations are skipped",
|
Description: "Disabled transformations are skipped",
|
||||||
|
|||||||
@@ -512,6 +512,8 @@ lineage: schemas: [{
|
|||||||
#DataTransformerConfig: {
|
#DataTransformerConfig: {
|
||||||
// Unique identifier of transformer
|
// Unique identifier of transformer
|
||||||
id: string
|
id: string
|
||||||
|
// Unique identifier of the instance of the transformer
|
||||||
|
refId?: string
|
||||||
// Disabled transformations are skipped
|
// Disabled transformations are skipped
|
||||||
disabled?: bool
|
disabled?: bool
|
||||||
// Optional frame matcher. When missing it will be applied to all results
|
// Optional frame matcher. When missing it will be applied to all results
|
||||||
|
|||||||
@@ -268,6 +268,7 @@ export { fuzzySearch } from './utils/fuzzySearch';
|
|||||||
|
|
||||||
// Transformations
|
// Transformations
|
||||||
export { standardTransformers } from './transformations/transformers';
|
export { standardTransformers } from './transformations/transformers';
|
||||||
|
export { getTransformationDynamicRefId } from './transformations/transformers/utils';
|
||||||
export {
|
export {
|
||||||
fieldMatchers,
|
fieldMatchers,
|
||||||
frameMatchers,
|
frameMatchers,
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const getOperator =
|
|||||||
}
|
}
|
||||||
|
|
||||||
const defaultOptions = info.transformation.defaultOptions ?? {};
|
const defaultOptions = info.transformation.defaultOptions ?? {};
|
||||||
const options = { ...defaultOptions, ...config.options };
|
const options = { ...defaultOptions, refId: config.refId, ...config.options };
|
||||||
|
|
||||||
// when running within Scenes, we can skip var interpolation, since it's already handled upstream
|
// when running within Scenes, we can skip var interpolation, since it's already handled upstream
|
||||||
const isScenes = window.__grafanaSceneContext != null;
|
const isScenes = window.__grafanaSceneContext != null;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ describe('JOIN Transformer', () => {
|
|||||||
|
|
||||||
describe('outer join', () => {
|
describe('outer join', () => {
|
||||||
const everySecondSeries = toDataFrame({
|
const everySecondSeries = toDataFrame({
|
||||||
|
refId: 'A',
|
||||||
name: 'even',
|
name: 'even',
|
||||||
fields: [
|
fields: [
|
||||||
{ name: 'time', type: FieldType.time, values: [3000, 4000, 5000, 6000] },
|
{ name: 'time', type: FieldType.time, values: [3000, 4000, 5000, 6000] },
|
||||||
@@ -23,6 +24,7 @@ describe('JOIN Transformer', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const everyOtherSecondSeries = toDataFrame({
|
const everyOtherSecondSeries = toDataFrame({
|
||||||
|
refId: 'B',
|
||||||
name: 'odd',
|
name: 'odd',
|
||||||
fields: [
|
fields: [
|
||||||
{ name: 'time', type: FieldType.time, values: [1000, 3000, 5000, 7000] },
|
{ name: 'time', type: FieldType.time, values: [1000, 3000, 5000, 7000] },
|
||||||
@@ -31,18 +33,20 @@ describe('JOIN Transformer', () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
it('joins by time field', async () => {
|
it('joins by time field with defined refId', async () => {
|
||||||
const cfg: DataTransformerConfig<JoinByFieldOptions> = {
|
const cfg: DataTransformerConfig<JoinByFieldOptions> = {
|
||||||
id: DataTransformerID.seriesToColumns,
|
id: DataTransformerID.seriesToColumns,
|
||||||
options: {
|
options: {
|
||||||
byField: 'time',
|
byField: 'time',
|
||||||
},
|
},
|
||||||
|
refId: 'test',
|
||||||
};
|
};
|
||||||
|
|
||||||
await expect(transformDataFrame([cfg], [everySecondSeries, everyOtherSecondSeries])).toEmitValuesWith(
|
await expect(transformDataFrame([cfg], [everySecondSeries, everyOtherSecondSeries])).toEmitValuesWith(
|
||||||
(received) => {
|
(received) => {
|
||||||
const data = received[0];
|
const data = received[0];
|
||||||
const filtered = data[0];
|
const filtered = data[0];
|
||||||
|
expect(filtered.refId).toBe('test');
|
||||||
expect(filtered.fields).toMatchInlineSnapshot(`
|
expect(filtered.fields).toMatchInlineSnapshot(`
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
@@ -133,7 +137,7 @@ describe('JOIN Transformer', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('joins by temperature field', async () => {
|
it('joins by temperature field with dynamic refId', async () => {
|
||||||
const cfg: DataTransformerConfig<JoinByFieldOptions> = {
|
const cfg: DataTransformerConfig<JoinByFieldOptions> = {
|
||||||
id: DataTransformerID.seriesToColumns,
|
id: DataTransformerID.seriesToColumns,
|
||||||
options: {
|
options: {
|
||||||
@@ -145,6 +149,7 @@ describe('JOIN Transformer', () => {
|
|||||||
(received) => {
|
(received) => {
|
||||||
const data = received[0];
|
const data = received[0];
|
||||||
const filtered = data[0];
|
const filtered = data[0];
|
||||||
|
expect(filtered.refId).toBe('joinByField-A-B');
|
||||||
expect(filtered.fields).toMatchInlineSnapshot(`
|
expect(filtered.fields).toMatchInlineSnapshot(`
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { FieldMatcherID } from '../matchers/ids';
|
|||||||
|
|
||||||
import { DataTransformerID } from './ids';
|
import { DataTransformerID } from './ids';
|
||||||
import { joinDataFrames } from './joinDataFrames';
|
import { joinDataFrames } from './joinDataFrames';
|
||||||
|
import { getTransformationDynamicRefId } from './utils';
|
||||||
|
|
||||||
export enum JoinMode {
|
export enum JoinMode {
|
||||||
outer = 'outer', // best for time series, non duplicated join on values
|
outer = 'outer', // best for time series, non duplicated join on values
|
||||||
@@ -17,6 +18,7 @@ export enum JoinMode {
|
|||||||
export interface JoinByFieldOptions {
|
export interface JoinByFieldOptions {
|
||||||
byField?: string; // empty will pick the field automatically
|
byField?: string; // empty will pick the field automatically
|
||||||
mode?: JoinMode;
|
mode?: JoinMode;
|
||||||
|
refId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const joinByFieldTransformer: SynchronousDataTransformerInfo<JoinByFieldOptions> = {
|
export const joinByFieldTransformer: SynchronousDataTransformerInfo<JoinByFieldOptions> = {
|
||||||
@@ -42,11 +44,13 @@ export const joinByFieldTransformer: SynchronousDataTransformerInfo<JoinByFieldO
|
|||||||
}
|
}
|
||||||
const joined = joinDataFrames({ frames: data, joinBy, mode: options.mode });
|
const joined = joinDataFrames({ frames: data, joinBy, mode: options.mode });
|
||||||
if (joined) {
|
if (joined) {
|
||||||
joined.refId = `${DataTransformerID.joinByField}-${data.map((frame) => frame.refId).join('-')}`;
|
joined.refId = options.refId ?? getTransformationDynamicRefId(DataTransformerID.joinByField, data);
|
||||||
return [joined];
|
return [joined];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
usesDynamicRefId: true,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -32,8 +32,9 @@ describe('Merge multiple to single', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('combine two series into one', async () => {
|
it('combine two series into one with dynamic refId', async () => {
|
||||||
const seriesA = toDataFrame({
|
const seriesA = toDataFrame({
|
||||||
|
refId: 'A',
|
||||||
name: 'A',
|
name: 'A',
|
||||||
fields: [
|
fields: [
|
||||||
{ name: 'Time', type: FieldType.time, values: [1000] },
|
{ name: 'Time', type: FieldType.time, values: [1000] },
|
||||||
@@ -42,6 +43,7 @@ describe('Merge multiple to single', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const seriesB = toDataFrame({
|
const seriesB = toDataFrame({
|
||||||
|
refId: 'B',
|
||||||
name: 'B',
|
name: 'B',
|
||||||
fields: [
|
fields: [
|
||||||
{ name: 'Time', type: FieldType.time, values: [2000] },
|
{ name: 'Time', type: FieldType.time, values: [2000] },
|
||||||
@@ -55,7 +57,7 @@ describe('Merge multiple to single', () => {
|
|||||||
createField('Time', FieldType.time, [1000, 2000]),
|
createField('Time', FieldType.time, [1000, 2000]),
|
||||||
createField('Temp', FieldType.number, [1, -1]),
|
createField('Temp', FieldType.number, [1, -1]),
|
||||||
];
|
];
|
||||||
|
expect(result[0].refId).toBe('merge-A-B');
|
||||||
expect(unwrap(result[0].fields)).toEqual(expected);
|
expect(unwrap(result[0].fields)).toEqual(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -77,13 +79,15 @@ describe('Merge multiple to single', () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(transformDataFrame([cfg], [seriesA, seriesB])).toEmitValuesWith((received) => {
|
const config = { ...cfg, refId: 'test' };
|
||||||
|
|
||||||
|
await expect(transformDataFrame([config], [seriesA, seriesB])).toEmitValuesWith((received) => {
|
||||||
const result = received[0];
|
const result = received[0];
|
||||||
const expected: Field[] = [
|
const expected: Field[] = [
|
||||||
createField('Time', FieldType.time, [100, 150, 200, 100, 125, 126]),
|
createField('Time', FieldType.time, [100, 150, 200, 100, 125, 126]),
|
||||||
createField('Temp', FieldType.number, [1, 4, 5, -1, 2, 3]),
|
createField('Temp', FieldType.number, [1, 4, 5, -1, 2, 3]),
|
||||||
];
|
];
|
||||||
|
expect(result[0].refId).toBe('test');
|
||||||
expect(unwrap(result[0].fields)).toEqual(expected);
|
expect(unwrap(result[0].fields)).toEqual(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,13 +6,16 @@ import { DataFrame, Field } from '../../types/dataFrame';
|
|||||||
import { DataTransformerInfo, TransformationApplicabilityLevels } from '../../types/transformations';
|
import { DataTransformerInfo, TransformationApplicabilityLevels } from '../../types/transformations';
|
||||||
|
|
||||||
import { DataTransformerID } from './ids';
|
import { DataTransformerID } from './ids';
|
||||||
|
import { getTransformationDynamicRefId } from './utils';
|
||||||
|
|
||||||
interface ValuePointer {
|
interface ValuePointer {
|
||||||
key: string;
|
key: string;
|
||||||
index: number;
|
index: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MergeTransformerOptions {}
|
export interface MergeTransformerOptions {
|
||||||
|
refId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const mergeTransformer: DataTransformerInfo<MergeTransformerOptions> = {
|
export const mergeTransformer: DataTransformerInfo<MergeTransformerOptions> = {
|
||||||
id: DataTransformerID.merge,
|
id: DataTransformerID.merge,
|
||||||
@@ -44,7 +47,7 @@ export const mergeTransformer: DataTransformerInfo<MergeTransformerOptions> = {
|
|||||||
const fieldIndexByName: Record<string, Record<number, number>> = {};
|
const fieldIndexByName: Record<string, Record<number, number>> = {};
|
||||||
const fieldNamesForKey: string[] = [];
|
const fieldNamesForKey: string[] = [];
|
||||||
const dataFrame = new MutableDataFrame({
|
const dataFrame = new MutableDataFrame({
|
||||||
refId: `${DataTransformerID.merge}-${data.map((frame) => frame.refId).join('-')}`,
|
refId: options.refId ?? getTransformationDynamicRefId(DataTransformerID.merge, data),
|
||||||
fields: [],
|
fields: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -122,6 +125,7 @@ export const mergeTransformer: DataTransformerInfo<MergeTransformerOptions> = {
|
|||||||
return [dataFrame];
|
return [dataFrame];
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
|
usesDynamicRefId: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const copyFieldStructure = (field: Field): Field => {
|
const copyFieldStructure = (field: Field): Field => {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { DataTransformerID } from './ids';
|
|||||||
import { reduceFields, reduceTransformer, ReduceTransformerMode, ReduceTransformerOptions } from './reduce';
|
import { reduceFields, reduceTransformer, ReduceTransformerMode, ReduceTransformerOptions } from './reduce';
|
||||||
|
|
||||||
const seriesAWithSingleField = toDataFrame({
|
const seriesAWithSingleField = toDataFrame({
|
||||||
|
refId: 'A',
|
||||||
name: 'A',
|
name: 'A',
|
||||||
fields: [
|
fields: [
|
||||||
{ name: 'time', type: FieldType.time, values: [3000, 4000, 5000, 6000] },
|
{ name: 'time', type: FieldType.time, values: [3000, 4000, 5000, 6000] },
|
||||||
@@ -19,6 +20,7 @@ const seriesAWithSingleField = toDataFrame({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const seriesAWithMultipleFields = toDataFrame({
|
const seriesAWithMultipleFields = toDataFrame({
|
||||||
|
refId: 'B',
|
||||||
name: 'A',
|
name: 'A',
|
||||||
fields: [
|
fields: [
|
||||||
{ name: 'time', type: FieldType.time, values: [3000, 4000, 5000, 6000] },
|
{ name: 'time', type: FieldType.time, values: [3000, 4000, 5000, 6000] },
|
||||||
@@ -28,6 +30,7 @@ const seriesAWithMultipleFields = toDataFrame({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const seriesAWithAllNulls = toDataFrame({
|
const seriesAWithAllNulls = toDataFrame({
|
||||||
|
refId: 'C',
|
||||||
name: 'A',
|
name: 'A',
|
||||||
fields: [
|
fields: [
|
||||||
{ name: 'time', type: FieldType.time, values: [3000, 4000, 5000, 6000] },
|
{ name: 'time', type: FieldType.time, values: [3000, 4000, 5000, 6000] },
|
||||||
@@ -36,6 +39,7 @@ const seriesAWithAllNulls = toDataFrame({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const seriesBWithSingleField = toDataFrame({
|
const seriesBWithSingleField = toDataFrame({
|
||||||
|
refId: 'D',
|
||||||
name: 'B',
|
name: 'B',
|
||||||
fields: [
|
fields: [
|
||||||
{ name: 'time', type: FieldType.time, values: [1000, 3000, 5000, 7000] },
|
{ name: 'time', type: FieldType.time, values: [1000, 3000, 5000, 7000] },
|
||||||
@@ -44,6 +48,7 @@ const seriesBWithSingleField = toDataFrame({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const seriesBWithMultipleFields = toDataFrame({
|
const seriesBWithMultipleFields = toDataFrame({
|
||||||
|
refId: 'E',
|
||||||
name: 'B',
|
name: 'B',
|
||||||
fields: [
|
fields: [
|
||||||
{ name: 'time', type: FieldType.time, values: [1000, 3000, 5000, 7000] },
|
{ name: 'time', type: FieldType.time, values: [1000, 3000, 5000, 7000] },
|
||||||
@@ -53,6 +58,7 @@ const seriesBWithMultipleFields = toDataFrame({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const seriesBWithAllNulls = toDataFrame({
|
const seriesBWithAllNulls = toDataFrame({
|
||||||
|
refId: 'F',
|
||||||
name: 'B',
|
name: 'B',
|
||||||
fields: [
|
fields: [
|
||||||
{ name: 'time', type: FieldType.time, values: [3000, 4000, 5000, 6000] },
|
{ name: 'time', type: FieldType.time, values: [3000, 4000, 5000, 6000] },
|
||||||
@@ -65,7 +71,7 @@ describe('Reducer Transformer', () => {
|
|||||||
mockTransformationsRegistry([reduceTransformer]);
|
mockTransformationsRegistry([reduceTransformer]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('reduces multiple data frames with many fields', async () => {
|
it('reduces multiple data frames with many fields and dynamic refId', async () => {
|
||||||
const cfg = {
|
const cfg = {
|
||||||
id: DataTransformerID.reduce,
|
id: DataTransformerID.reduce,
|
||||||
options: {
|
options: {
|
||||||
@@ -135,6 +141,7 @@ describe('Reducer Transformer', () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
expect(processed[0].refId).toBe('reduce-B-E');
|
||||||
expect(processed.length).toEqual(1);
|
expect(processed.length).toEqual(1);
|
||||||
expect(processed[0].length).toEqual(4);
|
expect(processed[0].length).toEqual(4);
|
||||||
expect(processed[0].fields).toEqual(expected);
|
expect(processed[0].fields).toEqual(expected);
|
||||||
@@ -142,12 +149,13 @@ describe('Reducer Transformer', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('reduces multiple data frames with single field', async () => {
|
it('reduces multiple data frames with single field and static refId', async () => {
|
||||||
const cfg = {
|
const cfg = {
|
||||||
id: DataTransformerID.reduce,
|
id: DataTransformerID.reduce,
|
||||||
options: {
|
options: {
|
||||||
reducers: [ReducerID.first, ReducerID.min, ReducerID.max, ReducerID.last],
|
reducers: [ReducerID.first, ReducerID.min, ReducerID.max, ReducerID.last],
|
||||||
},
|
},
|
||||||
|
refId: 'test',
|
||||||
};
|
};
|
||||||
|
|
||||||
await expect(transformDataFrame([cfg], [seriesAWithSingleField, seriesBWithSingleField])).toEmitValuesWith(
|
await expect(transformDataFrame([cfg], [seriesAWithSingleField, seriesBWithSingleField])).toEmitValuesWith(
|
||||||
@@ -187,6 +195,7 @@ describe('Reducer Transformer', () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
expect(processed.length).toEqual(1);
|
expect(processed.length).toEqual(1);
|
||||||
|
expect(processed[0].refId).toBe('test');
|
||||||
expect(processed[0].length).toEqual(2);
|
expect(processed[0].length).toEqual(2);
|
||||||
expect(processed[0].fields).toEqual(expected);
|
expect(processed[0].fields).toEqual(expected);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { getFieldMatcher } from '../matchers';
|
|||||||
import { alwaysFieldMatcher, notTimeFieldMatcher } from '../matchers/predicates';
|
import { alwaysFieldMatcher, notTimeFieldMatcher } from '../matchers/predicates';
|
||||||
|
|
||||||
import { DataTransformerID } from './ids';
|
import { DataTransformerID } from './ids';
|
||||||
|
import { getTransformationDynamicRefId } from './utils';
|
||||||
|
|
||||||
export enum ReduceTransformerMode {
|
export enum ReduceTransformerMode {
|
||||||
SeriesToRows = 'seriesToRows', // default
|
SeriesToRows = 'seriesToRows', // default
|
||||||
@@ -22,6 +23,7 @@ export interface ReduceTransformerOptions {
|
|||||||
mode?: ReduceTransformerMode;
|
mode?: ReduceTransformerMode;
|
||||||
includeTimeField?: boolean;
|
includeTimeField?: boolean;
|
||||||
labelsToFields?: boolean;
|
labelsToFields?: boolean;
|
||||||
|
refId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const reduceTransformer: DataTransformerInfo<ReduceTransformerOptions> = {
|
export const reduceTransformer: DataTransformerInfo<ReduceTransformerOptions> = {
|
||||||
@@ -57,10 +59,16 @@ export const reduceTransformer: DataTransformerInfo<ReduceTransformerOptions> =
|
|||||||
// Add a row for each series
|
// Add a row for each series
|
||||||
const res = reduceSeriesToRows(data, matcher, options.reducers, options.labelsToFields);
|
const res = reduceSeriesToRows(data, matcher, options.reducers, options.labelsToFields);
|
||||||
return res
|
return res
|
||||||
? [{ ...res, refId: `${DataTransformerID.reduce}-${data.map((frame) => frame.refId).join('-')}` }]
|
? [
|
||||||
|
{
|
||||||
|
...res,
|
||||||
|
refId: options.refId ?? getTransformationDynamicRefId(DataTransformerID.reduce, data),
|
||||||
|
},
|
||||||
|
]
|
||||||
: [];
|
: [];
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
|
usesDynamicRefId: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -39,10 +39,11 @@ describe('Series to rows', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('combine two series into one', async () => {
|
it('combine two series into one with static refId', async () => {
|
||||||
const cfg: DataTransformerConfig<SeriesToRowsTransformerOptions> = {
|
const cfg: DataTransformerConfig<SeriesToRowsTransformerOptions> = {
|
||||||
id: DataTransformerID.seriesToRows,
|
id: DataTransformerID.seriesToRows,
|
||||||
options: {},
|
options: {},
|
||||||
|
refId: 'test',
|
||||||
};
|
};
|
||||||
|
|
||||||
const seriesA = toDataFrame({
|
const seriesA = toDataFrame({
|
||||||
@@ -69,18 +70,19 @@ describe('Series to rows', () => {
|
|||||||
createField('Metric', FieldType.string, ['B', 'A']),
|
createField('Metric', FieldType.string, ['B', 'A']),
|
||||||
createField('Value', FieldType.number, [-1, 1]),
|
createField('Value', FieldType.number, [-1, 1]),
|
||||||
];
|
];
|
||||||
|
expect(result[0].refId).toBe('test');
|
||||||
expect(unwrap(result[0].fields)).toEqual(expected);
|
expect(unwrap(result[0].fields)).toEqual(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('combine two series with multiple values into one', async () => {
|
it('combine two series with multiple values into one with dynamic refid', async () => {
|
||||||
const cfg: DataTransformerConfig<SeriesToRowsTransformerOptions> = {
|
const cfg: DataTransformerConfig<SeriesToRowsTransformerOptions> = {
|
||||||
id: DataTransformerID.seriesToRows,
|
id: DataTransformerID.seriesToRows,
|
||||||
options: {},
|
options: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const seriesA = toDataFrame({
|
const seriesA = toDataFrame({
|
||||||
|
refId: 'A',
|
||||||
name: 'A',
|
name: 'A',
|
||||||
fields: [
|
fields: [
|
||||||
{ name: 'Time', type: FieldType.time, values: [100, 150, 200] },
|
{ name: 'Time', type: FieldType.time, values: [100, 150, 200] },
|
||||||
@@ -89,6 +91,7 @@ describe('Series to rows', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const seriesB = toDataFrame({
|
const seriesB = toDataFrame({
|
||||||
|
refId: 'B',
|
||||||
name: 'B',
|
name: 'B',
|
||||||
fields: [
|
fields: [
|
||||||
{ name: 'Time', type: FieldType.time, values: [100, 125, 126] },
|
{ name: 'Time', type: FieldType.time, values: [100, 125, 126] },
|
||||||
@@ -104,7 +107,7 @@ describe('Series to rows', () => {
|
|||||||
createField('Metric', FieldType.string, ['A', 'A', 'B', 'B', 'A', 'B']),
|
createField('Metric', FieldType.string, ['A', 'A', 'B', 'B', 'A', 'B']),
|
||||||
createField('Value', FieldType.number, [5, 4, 3, 2, 1, -1]),
|
createField('Value', FieldType.number, [5, 4, 3, 2, 1, -1]),
|
||||||
];
|
];
|
||||||
|
expect(result[0].refId).toBe('seriesToRows-A-B');
|
||||||
expect(unwrap(result[0].fields)).toEqual(expected);
|
expect(unwrap(result[0].fields)).toEqual(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,8 +15,11 @@ import {
|
|||||||
import { DataTransformerInfo } from '../../types/transformations';
|
import { DataTransformerInfo } from '../../types/transformations';
|
||||||
|
|
||||||
import { DataTransformerID } from './ids';
|
import { DataTransformerID } from './ids';
|
||||||
|
import { getTransformationDynamicRefId } from './utils';
|
||||||
|
|
||||||
export interface SeriesToRowsTransformerOptions {}
|
export interface SeriesToRowsTransformerOptions {
|
||||||
|
refId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const seriesToRowsTransformer: DataTransformerInfo<SeriesToRowsTransformerOptions> = {
|
export const seriesToRowsTransformer: DataTransformerInfo<SeriesToRowsTransformerOptions> = {
|
||||||
id: DataTransformerID.seriesToRows,
|
id: DataTransformerID.seriesToRows,
|
||||||
@@ -38,7 +41,7 @@ export const seriesToRowsTransformer: DataTransformerInfo<SeriesToRowsTransforme
|
|||||||
const timeFieldByIndex: Record<number, number> = {};
|
const timeFieldByIndex: Record<number, number> = {};
|
||||||
const targetFields = new Set<string>();
|
const targetFields = new Set<string>();
|
||||||
const dataFrame = new MutableDataFrame({
|
const dataFrame = new MutableDataFrame({
|
||||||
refId: `${DataTransformerID.seriesToRows}-${data.map((frame) => frame.refId).join('-')}`,
|
refId: options.refId ?? getTransformationDynamicRefId(DataTransformerID.seriesToRows, data),
|
||||||
fields: [],
|
fields: [],
|
||||||
});
|
});
|
||||||
const metricField: Field = {
|
const metricField: Field = {
|
||||||
@@ -90,6 +93,7 @@ export const seriesToRowsTransformer: DataTransformerInfo<SeriesToRowsTransforme
|
|||||||
return [sortDataFrame(dataFrame, 0, true)];
|
return [sortDataFrame(dataFrame, 0, true)];
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
|
usesDynamicRefId: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const copyFieldStructure = (field: Field, name: string): Field => {
|
const copyFieldStructure = (field: Field, name: string): Field => {
|
||||||
|
|||||||
@@ -33,3 +33,7 @@ export function getSpecialValue(specialValue: SpecialValue) {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getTransformationDynamicRefId = (transformationId: string, data: DataFrame[]) => {
|
||||||
|
return `${transformationId}-${data.map((frame) => frame.refId).join('-')}`;
|
||||||
|
};
|
||||||
|
|||||||
@@ -55,6 +55,10 @@ export interface DataTransformerInfo<TOptions = any> extends RegistryItemWithOpt
|
|||||||
* This way descriptions can be tailored relative to the underlying data.
|
* This way descriptions can be tailored relative to the underlying data.
|
||||||
*/
|
*/
|
||||||
isApplicableDescription?: string | ((data: DataFrame[]) => string);
|
isApplicableDescription?: string | ((data: DataFrame[]) => string);
|
||||||
|
/**
|
||||||
|
* Does the transformation generate a dataframe and thus generates a refID automatically based on incoming data
|
||||||
|
*/
|
||||||
|
usesDynamicRefId?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -746,6 +746,10 @@ export interface DataTransformerConfig {
|
|||||||
* Valid options depend on the transformer id
|
* Valid options depend on the transformer id
|
||||||
*/
|
*/
|
||||||
options: unknown;
|
options: unknown;
|
||||||
|
/**
|
||||||
|
* Unique identifier of the instance of the transformer
|
||||||
|
*/
|
||||||
|
refId?: string;
|
||||||
/**
|
/**
|
||||||
* Where to pull DataFrames from as input to transformation
|
* Where to pull DataFrames from as input to transformation
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -176,6 +176,8 @@ export const defaultTransformationKind = (): TransformationKind => ({
|
|||||||
export interface DataTransformerConfig {
|
export interface DataTransformerConfig {
|
||||||
// Unique identifier of transformer
|
// Unique identifier of transformer
|
||||||
id: string;
|
id: string;
|
||||||
|
// Unique identifier of the instance of the transformer
|
||||||
|
refId?: string;
|
||||||
// Disabled transformations are skipped
|
// Disabled transformations are skipped
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
// Optional frame matcher. When missing it will be applied to all results
|
// Optional frame matcher. When missing it will be applied to all results
|
||||||
|
|||||||
@@ -312,6 +312,8 @@ const DashboardLinkPlacement = "inControlsMenu"
|
|||||||
type DataTransformerConfig struct {
|
type DataTransformerConfig struct {
|
||||||
// Unique identifier of transformer
|
// Unique identifier of transformer
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
|
// Unique identifier of the instance of the transformer
|
||||||
|
RefId *string `json:"refId,omitempty"`
|
||||||
// Disabled transformations are skipped
|
// Disabled transformations are skipped
|
||||||
Disabled *bool `json:"disabled,omitempty"`
|
Disabled *bool `json:"disabled,omitempty"`
|
||||||
// Optional frame matcher. When missing it will be applied to all results
|
// Optional frame matcher. When missing it will be applied to all results
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
getFrameMatchers,
|
getFrameMatchers,
|
||||||
transformDataFrame,
|
transformDataFrame,
|
||||||
DataFrame,
|
DataFrame,
|
||||||
|
getTransformationDynamicRefId,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import { t } from '@grafana/i18n';
|
import { t } from '@grafana/i18n';
|
||||||
@@ -26,6 +27,7 @@ import { PluginStateInfo } from 'app/features/plugins/components/PluginStateInfo
|
|||||||
import { TransformationEditor } from './TransformationEditor';
|
import { TransformationEditor } from './TransformationEditor';
|
||||||
import { TransformationEditorHelpDisplay } from './TransformationEditorHelpDisplay';
|
import { TransformationEditorHelpDisplay } from './TransformationEditorHelpDisplay';
|
||||||
import { TransformationFilter } from './TransformationFilter';
|
import { TransformationFilter } from './TransformationFilter';
|
||||||
|
import { TransformationOperationRowHeader } from './TransformationOperationRowHeader';
|
||||||
import { TransformationData } from './TransformationsEditor';
|
import { TransformationData } from './TransformationsEditor';
|
||||||
import { TransformationsEditorTransformation } from './types';
|
import { TransformationsEditorTransformation } from './types';
|
||||||
|
|
||||||
@@ -60,6 +62,8 @@ export const TransformationOperationRow = ({
|
|||||||
// output of previous transformation
|
// output of previous transformation
|
||||||
const [prevOutput, setPrevOutput] = useState<DataFrame[]>([]);
|
const [prevOutput, setPrevOutput] = useState<DataFrame[]>([]);
|
||||||
|
|
||||||
|
const dynamicRefId = getTransformationDynamicRefId(uiConfig.id, data.series);
|
||||||
|
|
||||||
const onDisableToggle = useCallback(
|
const onDisableToggle = useCallback(
|
||||||
(index: number) => {
|
(index: number) => {
|
||||||
const current = configs[index].transformation;
|
const current = configs[index].transformation;
|
||||||
@@ -157,6 +161,20 @@ export const TransformationOperationRow = ({
|
|||||||
};
|
};
|
||||||
}, [index, data, configs]);
|
}, [index, data, configs]);
|
||||||
|
|
||||||
|
const renderHeader = () => {
|
||||||
|
return (
|
||||||
|
<TransformationOperationRowHeader
|
||||||
|
index={index}
|
||||||
|
transformation={configs[index].transformation}
|
||||||
|
transformations={configs.map((config) => config.transformation)}
|
||||||
|
transformationTypeName={`${index + 1} - ${uiConfig.name}`}
|
||||||
|
disabled
|
||||||
|
onChange={onChange}
|
||||||
|
dynamicRefId={uiConfig.transformation.usesDynamicRefId ? dynamicRefId : undefined}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const renderActions = () => {
|
const renderActions = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -228,10 +246,9 @@ export const TransformationOperationRow = ({
|
|||||||
<QueryOperationRow
|
<QueryOperationRow
|
||||||
id={id}
|
id={id}
|
||||||
index={index}
|
index={index}
|
||||||
// eslint-disable-next-line @grafana/i18n/no-untranslated-strings
|
|
||||||
title={`${index + 1} - ${uiConfig.name}`}
|
|
||||||
draggable
|
draggable
|
||||||
actions={renderActions}
|
actions={renderActions}
|
||||||
|
headerElement={renderHeader}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
expanderMessages={{
|
expanderMessages={{
|
||||||
close: 'Collapse transformation row',
|
close: 'Collapse transformation row',
|
||||||
@@ -247,7 +264,6 @@ export const TransformationOperationRow = ({
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<TransformationEditor
|
<TransformationEditor
|
||||||
input={input}
|
input={input}
|
||||||
output={output}
|
output={output}
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
|
||||||
|
import { DataTransformerConfig, DataTransformerID } from '@grafana/data';
|
||||||
|
import { LabelsToFieldsMode, LabelsToFieldsOptions, MergeTransformerOptions } from '@grafana/data/internal';
|
||||||
|
|
||||||
|
import { TransformationOperationRowHeader } from './TransformationOperationRowHeader';
|
||||||
|
|
||||||
|
const mergeTransform: DataTransformerConfig<MergeTransformerOptions> = {
|
||||||
|
id: DataTransformerID.merge,
|
||||||
|
options: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const labelsToFieldsTransform: DataTransformerConfig<LabelsToFieldsOptions> = {
|
||||||
|
id: DataTransformerID.labelsToFields,
|
||||||
|
options: {
|
||||||
|
mode: LabelsToFieldsMode.Rows,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const labelsToFieldsRefId = { ...labelsToFieldsTransform, refId: 'test' };
|
||||||
|
|
||||||
|
describe('TransformationOperationRowHeader', () => {
|
||||||
|
it('renders the modal with the title and empty refId for ', () => {
|
||||||
|
const { unmount } = render(
|
||||||
|
<TransformationOperationRowHeader
|
||||||
|
index={0}
|
||||||
|
transformation={mergeTransform}
|
||||||
|
transformations={[mergeTransform, labelsToFieldsTransform]}
|
||||||
|
transformationTypeName="1 - Labels to fields"
|
||||||
|
onChange={() => {}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if the modal title is rendered with the correct text
|
||||||
|
expect(screen.getByText('1 - Labels to fields')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('(Auto)')).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Unmount the component to clean up
|
||||||
|
unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls onChange when the the refId is changed', async () => {
|
||||||
|
const mockOnChange = jest.fn();
|
||||||
|
const { unmount } = render(
|
||||||
|
<TransformationOperationRowHeader
|
||||||
|
index={0}
|
||||||
|
transformation={mergeTransform}
|
||||||
|
transformations={[mergeTransform, labelsToFieldsTransform]}
|
||||||
|
transformationTypeName="1 - Labels to fields"
|
||||||
|
onChange={mockOnChange}
|
||||||
|
dynamicRefId=""
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Find and click the modal's close button
|
||||||
|
const beforeEditField = screen.getByTestId('transformation-refid-div');
|
||||||
|
await userEvent.click(beforeEditField);
|
||||||
|
const refIdInput = screen.getByTestId('transformation-refid-input');
|
||||||
|
await userEvent.click(refIdInput);
|
||||||
|
await userEvent.type(refIdInput, 'test refid');
|
||||||
|
|
||||||
|
// blur the field
|
||||||
|
await userEvent.click(document.body);
|
||||||
|
expect(mockOnChange).toHaveBeenCalledWith(0, { id: 'merge', options: {}, refId: 'test refid' });
|
||||||
|
|
||||||
|
unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows an error message if the refID is already used', async () => {
|
||||||
|
const mockOnChange = jest.fn();
|
||||||
|
|
||||||
|
const { unmount } = render(
|
||||||
|
<TransformationOperationRowHeader
|
||||||
|
index={0}
|
||||||
|
transformation={mergeTransform}
|
||||||
|
transformations={[mergeTransform, labelsToFieldsRefId]}
|
||||||
|
transformationTypeName="1 - Labels to fields"
|
||||||
|
onChange={mockOnChange}
|
||||||
|
dynamicRefId=""
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Find and click the modal's close button
|
||||||
|
const beforeEditField = screen.getByTestId('transformation-refid-div');
|
||||||
|
await userEvent.click(beforeEditField);
|
||||||
|
const refIdInput = screen.getByTestId('transformation-refid-input');
|
||||||
|
await userEvent.click(refIdInput);
|
||||||
|
await userEvent.type(refIdInput, 'test');
|
||||||
|
expect(screen.getByText('Transformation name already exists')).toBeInTheDocument();
|
||||||
|
// blur the field
|
||||||
|
await userEvent.click(document.body);
|
||||||
|
expect(mockOnChange).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays the dynamic id if provided', async () => {
|
||||||
|
const { unmount } = render(
|
||||||
|
<TransformationOperationRowHeader
|
||||||
|
index={0}
|
||||||
|
transformation={mergeTransform}
|
||||||
|
transformations={[mergeTransform, labelsToFieldsTransform]}
|
||||||
|
transformationTypeName="1 - Labels to fields"
|
||||||
|
onChange={() => {}}
|
||||||
|
dynamicRefId="test-A-B"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText('test-A-B')).toBeInTheDocument();
|
||||||
|
unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays the static id if provided, even if dynamic ref id is also provided', async () => {
|
||||||
|
const { unmount } = render(
|
||||||
|
<TransformationOperationRowHeader
|
||||||
|
index={0}
|
||||||
|
transformation={labelsToFieldsRefId}
|
||||||
|
transformations={[labelsToFieldsRefId, mergeTransform]}
|
||||||
|
transformationTypeName="1 - Labels to fields"
|
||||||
|
onChange={() => {}}
|
||||||
|
dynamicRefId="refId"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText('test')).toBeInTheDocument();
|
||||||
|
unmount();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
import { css, cx } from '@emotion/css';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useToggle } from 'react-use';
|
||||||
|
|
||||||
|
import { GrafanaTheme2, DataTransformerConfig } from '@grafana/data';
|
||||||
|
import { t } from '@grafana/i18n';
|
||||||
|
import { FieldValidationMessage, Icon, Input, useStyles2 } from '@grafana/ui';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
index: number;
|
||||||
|
transformation: DataTransformerConfig;
|
||||||
|
transformations: DataTransformerConfig[];
|
||||||
|
transformationTypeName: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
onChange: (index: number, config: DataTransformerConfig) => void;
|
||||||
|
dynamicRefId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TransformationOperationRowHeader = (props: Props) => {
|
||||||
|
const { index, transformation, transformations, onChange, disabled, transformationTypeName, dynamicRefId } = props;
|
||||||
|
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
const [isRefIdEditing, toggleIsRefIdEditing] = useToggle(false);
|
||||||
|
const [isStaticRefId, setIsStaticRefId] = useState(transformation.refId !== undefined);
|
||||||
|
const [validationError, setValidationError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const onEndEditRefId = (newRefId: string) => {
|
||||||
|
const trimmedNewRefId = newRefId.trim();
|
||||||
|
toggleIsRefIdEditing(false);
|
||||||
|
|
||||||
|
// Ignore change if invalid
|
||||||
|
if (validationError) {
|
||||||
|
setValidationError(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transformation.refId !== trimmedNewRefId && trimmedNewRefId !== '') {
|
||||||
|
setIsStaticRefId(true);
|
||||||
|
onChange(index, {
|
||||||
|
...transformation,
|
||||||
|
refId: trimmedNewRefId,
|
||||||
|
});
|
||||||
|
} else if (trimmedNewRefId === '') {
|
||||||
|
// if it was previously custom and is now empty, it is being cleared out and we want to save it as undefined
|
||||||
|
if (isStaticRefId) {
|
||||||
|
onChange(index, {
|
||||||
|
...transformation,
|
||||||
|
refId: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// either way, if it is empty, we want it to display as not static
|
||||||
|
setIsStaticRefId(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onInputChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
|
||||||
|
const newRefId = event.currentTarget.value.trim();
|
||||||
|
|
||||||
|
for (const otherTransformation of transformations) {
|
||||||
|
if (otherTransformation !== transformation && newRefId === otherTransformation.refId) {
|
||||||
|
setValidationError('Transformation name already exists');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validationError) {
|
||||||
|
setValidationError(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onEditRefIdBlur = (event: React.SyntheticEvent<HTMLInputElement>) => {
|
||||||
|
onEndEditRefId(event.currentTarget.value.trim());
|
||||||
|
};
|
||||||
|
|
||||||
|
const onKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
onEndEditRefId(event.currentTarget.value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFocus = (event: React.FocusEvent<HTMLInputElement>) => {
|
||||||
|
event.target.select();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.wrapper}>
|
||||||
|
{!isRefIdEditing && (
|
||||||
|
<button
|
||||||
|
className={styles.refIdWrapper}
|
||||||
|
title={t(
|
||||||
|
'dashboard.transformation-operation-row.transformation-editor-row-header.edit-refId',
|
||||||
|
'Edit transformation name'
|
||||||
|
)}
|
||||||
|
onClick={() => toggleIsRefIdEditing()}
|
||||||
|
data-testid="transformation-refid-div"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span className={cx(styles.refIdStyle, !isStaticRefId && styles.placeholderText)}>
|
||||||
|
{transformation.refId ||
|
||||||
|
dynamicRefId ||
|
||||||
|
t(
|
||||||
|
'dashboard.transformation-operation-row.transformation-editor-row-header.edit-refId-placeholder',
|
||||||
|
'(Auto)'
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<Icon name="pen" className={styles.refIdEditIcon} size="sm" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isRefIdEditing && (
|
||||||
|
<>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
defaultValue={transformation.refId}
|
||||||
|
onBlur={onEditRefIdBlur}
|
||||||
|
autoFocus
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
onFocus={onFocus}
|
||||||
|
invalid={validationError !== null}
|
||||||
|
onChange={onInputChange}
|
||||||
|
className={styles.refIdInput}
|
||||||
|
data-testid="transformation-refid-input"
|
||||||
|
/>
|
||||||
|
{validationError && <FieldValidationMessage horizontal>{validationError}</FieldValidationMessage>}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<div className={cx(styles.title, disabled && styles.disabled)}>{transformationTypeName}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => {
|
||||||
|
return {
|
||||||
|
wrapper: css({
|
||||||
|
label: 'Wrapper',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}),
|
||||||
|
refIdWrapper: css({
|
||||||
|
display: 'flex',
|
||||||
|
cursor: 'pointer',
|
||||||
|
border: '1px solid transparent',
|
||||||
|
borderRadius: theme.shape.radius.default,
|
||||||
|
alignItems: 'center',
|
||||||
|
margin: 0,
|
||||||
|
background: 'transparent',
|
||||||
|
overflow: 'hidden',
|
||||||
|
|
||||||
|
'&:hover': {
|
||||||
|
background: theme.colors.action.hover,
|
||||||
|
border: `1px dashed ${theme.colors.border.strong}`,
|
||||||
|
},
|
||||||
|
|
||||||
|
'&:focus': {
|
||||||
|
border: `2px solid ${theme.colors.primary.border}`,
|
||||||
|
},
|
||||||
|
|
||||||
|
'&:hover, &:focus': {
|
||||||
|
'.transformation-refid-edit-icon': {
|
||||||
|
visibility: 'visible',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
refIdStyle: css({
|
||||||
|
fontWeight: theme.typography.fontWeightMedium,
|
||||||
|
color: theme.colors.primary.text,
|
||||||
|
cursor: 'pointer',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}),
|
||||||
|
refIdEditIcon: cx(
|
||||||
|
css({
|
||||||
|
marginLeft: theme.spacing(1),
|
||||||
|
visibility: 'hidden',
|
||||||
|
}),
|
||||||
|
'transformation-refid-edit-icon'
|
||||||
|
),
|
||||||
|
refIdInput: css({
|
||||||
|
maxWidth: '300px',
|
||||||
|
margin: '-4px 0',
|
||||||
|
}),
|
||||||
|
placeholderText: css({
|
||||||
|
fontWeight: theme.typography.fontWeightRegular,
|
||||||
|
fontSize: theme.typography.bodySmall.fontSize,
|
||||||
|
color: theme.colors.text.secondary,
|
||||||
|
alignItems: 'center',
|
||||||
|
fontStyle: 'italic',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
}),
|
||||||
|
title: css({
|
||||||
|
fontWeight: theme.typography.fontWeightBold,
|
||||||
|
color: theme.colors.text.link,
|
||||||
|
marginLeft: theme.spacing(0.5),
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
}),
|
||||||
|
disabled: css({
|
||||||
|
color: theme.colors.text.disabled,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -276,6 +276,16 @@ class UnThemedTransformationsEditor extends React.PureComponent<TransformationsE
|
|||||||
renderTransformationEditors = () => {
|
renderTransformationEditors = () => {
|
||||||
const { data, transformations } = this.state;
|
const { data, transformations } = this.state;
|
||||||
|
|
||||||
|
const transformationNoRefIdIdxs = transformations
|
||||||
|
.map((t, i) => {
|
||||||
|
return t.refId === undefined ? i : undefined;
|
||||||
|
})
|
||||||
|
.filter((idx) => idx !== undefined);
|
||||||
|
|
||||||
|
transformationNoRefIdIdxs.forEach((tIdx, i) => {
|
||||||
|
transformations[tIdx].refId = `T-${i}`;
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DragDropContext onDragEnd={this.onDragEnd}>
|
<DragDropContext onDragEnd={this.onDragEnd}>
|
||||||
<Droppable droppableId="transformations-list" direction="vertical">
|
<Droppable droppableId="transformations-list" direction="vertical">
|
||||||
|
|||||||
@@ -2,5 +2,6 @@ import { DataTransformerConfig } from '@grafana/data';
|
|||||||
|
|
||||||
export interface TransformationsEditorTransformation {
|
export interface TransformationsEditorTransformation {
|
||||||
transformation: DataTransformerConfig;
|
transformation: DataTransformerConfig;
|
||||||
|
refId?: string;
|
||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,58 +3,62 @@ import { toDataFrame, FieldType, DataFrame } from '@grafana/data';
|
|||||||
import { joinByLabels } from './joinByLabels';
|
import { joinByLabels } from './joinByLabels';
|
||||||
|
|
||||||
describe('Join by labels', () => {
|
describe('Join by labels', () => {
|
||||||
it('Simple join', () => {
|
const input = [
|
||||||
const input = [
|
toDataFrame({
|
||||||
toDataFrame({
|
refId: 'A',
|
||||||
fields: [
|
fields: [
|
||||||
{ name: 'Time', type: FieldType.time, values: [1, 2] },
|
{ name: 'Time', type: FieldType.time, values: [1, 2] },
|
||||||
{
|
{
|
||||||
name: 'Value',
|
name: 'Value',
|
||||||
type: FieldType.number,
|
type: FieldType.number,
|
||||||
config: {
|
config: {
|
||||||
displayNameFromDS: '111',
|
displayNameFromDS: '111',
|
||||||
},
|
|
||||||
values: [10, 200],
|
|
||||||
labels: { what: 'Temp', cluster: 'A', job: 'J1' },
|
|
||||||
},
|
},
|
||||||
],
|
values: [10, 200],
|
||||||
}),
|
labels: { what: 'Temp', cluster: 'A', job: 'J1' },
|
||||||
toDataFrame({
|
},
|
||||||
fields: [
|
],
|
||||||
{ name: 'Time', type: FieldType.time, values: [1, 2] },
|
}),
|
||||||
{
|
toDataFrame({
|
||||||
name: 'Value',
|
refId: 'B',
|
||||||
type: FieldType.number,
|
fields: [
|
||||||
config: {
|
{ name: 'Time', type: FieldType.time, values: [1, 2] },
|
||||||
displayNameFromDS: '222',
|
{
|
||||||
},
|
name: 'Value',
|
||||||
values: [10, 200],
|
type: FieldType.number,
|
||||||
labels: { what: 'Temp', cluster: 'B', job: 'J1' },
|
config: {
|
||||||
|
displayNameFromDS: '222',
|
||||||
},
|
},
|
||||||
],
|
values: [10, 200],
|
||||||
}),
|
labels: { what: 'Temp', cluster: 'B', job: 'J1' },
|
||||||
toDataFrame({
|
},
|
||||||
fields: [
|
],
|
||||||
{ name: 'Time', type: FieldType.time, values: [22, 28] },
|
}),
|
||||||
{
|
toDataFrame({
|
||||||
name: 'Value',
|
refId: 'C',
|
||||||
type: FieldType.number,
|
fields: [
|
||||||
config: {
|
{ name: 'Time', type: FieldType.time, values: [22, 28] },
|
||||||
displayNameFromDS: '333',
|
{
|
||||||
},
|
name: 'Value',
|
||||||
values: [22, 77],
|
type: FieldType.number,
|
||||||
labels: { what: 'Speed', cluster: 'B', job: 'J1' },
|
config: {
|
||||||
|
displayNameFromDS: '333',
|
||||||
},
|
},
|
||||||
],
|
values: [22, 77],
|
||||||
}),
|
labels: { what: 'Speed', cluster: 'B', job: 'J1' },
|
||||||
];
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
it('Simple join with dynamic refId', () => {
|
||||||
const result = joinByLabels(
|
const result = joinByLabels(
|
||||||
{
|
{
|
||||||
value: 'what',
|
value: 'what',
|
||||||
},
|
},
|
||||||
input
|
input
|
||||||
);
|
);
|
||||||
|
expect(result.refId).toBe('joinByLabels-A-B-C');
|
||||||
expect(result.fields[result.fields.length - 1].config).toMatchInlineSnapshot(`{}`);
|
expect(result.fields[result.fields.length - 1].config).toMatchInlineSnapshot(`{}`);
|
||||||
expect(toRowsSnapshow(result)).toMatchInlineSnapshot(`
|
expect(toRowsSnapshow(result)).toMatchInlineSnapshot(`
|
||||||
{
|
{
|
||||||
@@ -94,6 +98,17 @@ describe('Join by labels', () => {
|
|||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Simple join with static refId', () => {
|
||||||
|
const result = joinByLabels(
|
||||||
|
{
|
||||||
|
value: 'what',
|
||||||
|
refId: 'test',
|
||||||
|
},
|
||||||
|
input
|
||||||
|
);
|
||||||
|
expect(result.refId).toBe('test');
|
||||||
|
});
|
||||||
|
|
||||||
it('Error handling (no labels)', () => {
|
it('Error handling (no labels)', () => {
|
||||||
const input = [
|
const input = [
|
||||||
toDataFrame({
|
toDataFrame({
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
import { DataFrame, DataTransformerID, Field, FieldType, SynchronousDataTransformerInfo } from '@grafana/data';
|
import {
|
||||||
|
DataFrame,
|
||||||
|
DataTransformerID,
|
||||||
|
Field,
|
||||||
|
FieldType,
|
||||||
|
getTransformationDynamicRefId,
|
||||||
|
SynchronousDataTransformerInfo,
|
||||||
|
} from '@grafana/data';
|
||||||
import { t } from '@grafana/i18n';
|
import { t } from '@grafana/i18n';
|
||||||
|
|
||||||
import { getDistinctLabels } from '../utils';
|
import { getDistinctLabels } from '../utils';
|
||||||
@@ -8,6 +15,7 @@ import { getDistinctLabels } from '../utils';
|
|||||||
export interface JoinByLabelsTransformOptions {
|
export interface JoinByLabelsTransformOptions {
|
||||||
value: string; // something must be defined
|
value: string; // something must be defined
|
||||||
join?: string[];
|
join?: string[];
|
||||||
|
refId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getJoinByLabelsTransformer: () => SynchronousDataTransformerInfo<JoinByLabelsTransformOptions> = () => ({
|
export const getJoinByLabelsTransformer: () => SynchronousDataTransformerInfo<JoinByLabelsTransformOptions> = () => ({
|
||||||
@@ -30,6 +38,7 @@ export const getJoinByLabelsTransformer: () => SynchronousDataTransformerInfo<Jo
|
|||||||
return [joinByLabels(options, data)];
|
return [joinByLabels(options, data)];
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
usesDynamicRefId: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
interface JoinValues {
|
interface JoinValues {
|
||||||
@@ -111,7 +120,7 @@ export function joinByLabels(options: JoinByLabelsTransformOptions, data: DataFr
|
|||||||
const frame: DataFrame = {
|
const frame: DataFrame = {
|
||||||
fields: [],
|
fields: [],
|
||||||
length: nameValues[0].length,
|
length: nameValues[0].length,
|
||||||
refId: `${DataTransformerID.joinByLabels}-${data.map((frame) => frame.refId).join('-')}`,
|
refId: options.refId ?? getTransformationDynamicRefId(DataTransformerID.joinByLabels, data),
|
||||||
};
|
};
|
||||||
for (let i = 0; i < join.length; i++) {
|
for (let i = 0; i < join.length; i++) {
|
||||||
frame.fields.push({
|
frame.fields.push({
|
||||||
|
|||||||
@@ -5447,7 +5447,11 @@
|
|||||||
"title-remove": "Remove",
|
"title-remove": "Remove",
|
||||||
"title-show-transform-help": "Show transform help"
|
"title-show-transform-help": "Show transform help"
|
||||||
},
|
},
|
||||||
"title-delete": "Delete {{name}}?"
|
"title-delete": "Delete {{name}}?",
|
||||||
|
"transformation-editor-row-header": {
|
||||||
|
"edit-refId": "Edit transformation name",
|
||||||
|
"edit-refId-placeholder": "(Auto)"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"transformation-picker": {
|
"transformation-picker": {
|
||||||
"info": "Transformations allow you to join, calculate, re-order, hide, and rename your query results before they are visualized.",
|
"info": "Transformations allow you to join, calculate, re-order, hide, and rename your query results before they are visualized.",
|
||||||
|
|||||||
Reference in New Issue
Block a user