mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Transformations: add Concatenate fields transformer (#28237)
This commit is contained in:
parent
cb72242d95
commit
fe423644ae
@ -1,5 +1,6 @@
|
||||
import { appendTransformer } from './transformers/append';
|
||||
import { reduceTransformer } from './transformers/reduce';
|
||||
import { concatenateTransformer } from './transformers/concat';
|
||||
import { calculateFieldTransformer } from './transformers/calculateField';
|
||||
import { filterFieldsTransformer, filterFramesTransformer } from './transformers/filter';
|
||||
import { filterFieldsByNameTransformer } from './transformers/filterByName';
|
||||
@ -25,6 +26,7 @@ export const standardTransformers = {
|
||||
organizeFieldsTransformer,
|
||||
appendTransformer,
|
||||
reduceTransformer,
|
||||
concatenateTransformer,
|
||||
calculateFieldTransformer,
|
||||
seriesToColumnsTransformer,
|
||||
seriesToRowsTransformer,
|
||||
|
@ -0,0 +1,136 @@
|
||||
import { toDataFrame } from '../../dataframe/processDataFrame';
|
||||
import { concatenateFields, ConcatenateFrameNameMode } from './concat';
|
||||
|
||||
export const simpleABC = toDataFrame({
|
||||
name: 'ABC',
|
||||
fields: [
|
||||
{ name: 'A', values: [1, 2] },
|
||||
{ name: 'B', values: [1, 2] },
|
||||
{ name: 'C', values: [1, 2] },
|
||||
],
|
||||
});
|
||||
|
||||
export const simpleXYZ = toDataFrame({
|
||||
name: 'XYZ',
|
||||
fields: [
|
||||
{ name: 'X', values: [1, 2, 3] },
|
||||
{ name: 'Y', values: [1, 2, 3] },
|
||||
{ name: 'Z', values: [1, 2, 3] },
|
||||
],
|
||||
});
|
||||
|
||||
describe('Concat Transformer', () => {
|
||||
it('dropping frame name', () => {
|
||||
const frame = concatenateFields([simpleABC, simpleXYZ], { frameNameMode: ConcatenateFrameNameMode.Drop });
|
||||
expect(frame.length).toBe(3);
|
||||
expect(frame.fields.map(f => ({ name: f.name, labels: f.labels }))).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"labels": undefined,
|
||||
"name": "A",
|
||||
},
|
||||
Object {
|
||||
"labels": undefined,
|
||||
"name": "B",
|
||||
},
|
||||
Object {
|
||||
"labels": undefined,
|
||||
"name": "C",
|
||||
},
|
||||
Object {
|
||||
"labels": undefined,
|
||||
"name": "X",
|
||||
},
|
||||
Object {
|
||||
"labels": undefined,
|
||||
"name": "Y",
|
||||
},
|
||||
Object {
|
||||
"labels": undefined,
|
||||
"name": "Z",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('using field name', () => {
|
||||
const frame = concatenateFields([simpleABC, simpleXYZ], { frameNameMode: ConcatenateFrameNameMode.FieldName });
|
||||
expect(frame.length).toBe(3);
|
||||
expect(frame.fields.map(f => ({ name: f.name, labels: f.labels }))).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"labels": undefined,
|
||||
"name": "ABC · A",
|
||||
},
|
||||
Object {
|
||||
"labels": undefined,
|
||||
"name": "ABC · B",
|
||||
},
|
||||
Object {
|
||||
"labels": undefined,
|
||||
"name": "ABC · C",
|
||||
},
|
||||
Object {
|
||||
"labels": undefined,
|
||||
"name": "XYZ · X",
|
||||
},
|
||||
Object {
|
||||
"labels": undefined,
|
||||
"name": "XYZ · Y",
|
||||
},
|
||||
Object {
|
||||
"labels": undefined,
|
||||
"name": "XYZ · Z",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('using field label', () => {
|
||||
const frame = concatenateFields([simpleABC, simpleXYZ], {
|
||||
frameNameMode: ConcatenateFrameNameMode.Label,
|
||||
frameNameLabel: 'sensor',
|
||||
});
|
||||
expect(frame.length).toBe(3);
|
||||
expect(frame.fields.map(f => ({ name: f.name, labels: f.labels }))).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"labels": Object {
|
||||
"sensor": "ABC",
|
||||
},
|
||||
"name": "A",
|
||||
},
|
||||
Object {
|
||||
"labels": Object {
|
||||
"sensor": "ABC",
|
||||
},
|
||||
"name": "B",
|
||||
},
|
||||
Object {
|
||||
"labels": Object {
|
||||
"sensor": "ABC",
|
||||
},
|
||||
"name": "C",
|
||||
},
|
||||
Object {
|
||||
"labels": Object {
|
||||
"sensor": "XYZ",
|
||||
},
|
||||
"name": "X",
|
||||
},
|
||||
Object {
|
||||
"labels": Object {
|
||||
"sensor": "XYZ",
|
||||
},
|
||||
"name": "Y",
|
||||
},
|
||||
Object {
|
||||
"labels": Object {
|
||||
"sensor": "XYZ",
|
||||
},
|
||||
"name": "Z",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
105
packages/grafana-data/src/transformations/transformers/concat.ts
Normal file
105
packages/grafana-data/src/transformations/transformers/concat.ts
Normal file
@ -0,0 +1,105 @@
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
import { DataTransformerID } from './ids';
|
||||
import { DataTransformerInfo } from '../../types/transformations';
|
||||
import { DataFrame, Field } from '../../types/dataFrame';
|
||||
import { ArrayVector } from '../../vector';
|
||||
|
||||
export enum ConcatenateFrameNameMode {
|
||||
/**
|
||||
* Ignore the source frame name when moving to the destination
|
||||
*/
|
||||
Drop = 'drop',
|
||||
|
||||
/**
|
||||
* Copy the source frame name to the destination field. The final field will contain
|
||||
* both the frame and field name
|
||||
*/
|
||||
FieldName = 'field',
|
||||
|
||||
/**
|
||||
* Copy the source frame name to a label on the field. The label key is controlled
|
||||
* by frameNameLabel
|
||||
*/
|
||||
Label = 'label',
|
||||
}
|
||||
|
||||
export interface ConcatenateTransformerOptions {
|
||||
frameNameMode?: ConcatenateFrameNameMode;
|
||||
frameNameLabel?: string;
|
||||
}
|
||||
|
||||
export const concatenateTransformer: DataTransformerInfo<ConcatenateTransformerOptions> = {
|
||||
id: DataTransformerID.concatenate,
|
||||
name: 'Concatenate fields',
|
||||
description:
|
||||
'Combine all fields into a single frame. Values will be appended with undefined values if not the same length.',
|
||||
defaultOptions: {
|
||||
frameNameMode: ConcatenateFrameNameMode.FieldName,
|
||||
frameNameLabel: 'frame',
|
||||
},
|
||||
operator: options => source =>
|
||||
source.pipe(
|
||||
map(dataFrames => {
|
||||
if (!Array.isArray(dataFrames) || dataFrames.length < 2) {
|
||||
return dataFrames; // noop with single frame
|
||||
}
|
||||
return [concatenateFields(dataFrames, options)];
|
||||
})
|
||||
),
|
||||
};
|
||||
|
||||
/**
|
||||
* @internal only exported for tests
|
||||
*/
|
||||
export function concatenateFields(data: DataFrame[], opts: ConcatenateTransformerOptions): DataFrame {
|
||||
let sameLength = true;
|
||||
let maxLength = data[0].length;
|
||||
const frameNameLabel = opts.frameNameLabel ?? 'frame';
|
||||
let fields: Field[] = [];
|
||||
|
||||
for (const frame of data) {
|
||||
if (maxLength !== frame.length) {
|
||||
sameLength = false;
|
||||
maxLength = Math.max(maxLength, frame.length);
|
||||
}
|
||||
|
||||
for (const f of frame.fields) {
|
||||
const copy = { ...f };
|
||||
copy.state = undefined;
|
||||
if (frame.name) {
|
||||
if (opts.frameNameMode === ConcatenateFrameNameMode.Drop) {
|
||||
// nothing -- skip the name
|
||||
} else if (opts.frameNameMode === ConcatenateFrameNameMode.Label) {
|
||||
copy.labels = { ...f.labels };
|
||||
copy.labels[frameNameLabel] = frame.name;
|
||||
} else if (!copy.name || copy.name === 'Value') {
|
||||
copy.name = frame.name;
|
||||
} else {
|
||||
copy.name = `${frame.name} · ${f.name}`;
|
||||
}
|
||||
}
|
||||
fields.push(copy);
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure all fields have the same length
|
||||
if (!sameLength) {
|
||||
fields = fields.map(f => {
|
||||
if (f.values.length === maxLength) {
|
||||
return f;
|
||||
}
|
||||
const values = f.values.toArray();
|
||||
values.length = maxLength;
|
||||
return {
|
||||
...f,
|
||||
values: new ArrayVector(values),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
fields,
|
||||
length: maxLength,
|
||||
};
|
||||
}
|
@ -10,6 +10,7 @@ export enum DataTransformerID {
|
||||
seriesToColumns = 'seriesToColumns',
|
||||
seriesToRows = 'seriesToRows',
|
||||
merge = 'merge',
|
||||
concatenate = 'concatenate',
|
||||
labelsToFields = 'labelsToFields',
|
||||
filterFields = 'filterFields',
|
||||
filterFieldsByName = 'filterFieldsByName',
|
||||
|
@ -291,7 +291,7 @@ export abstract class DataSourceApi<
|
||||
*
|
||||
* Note: `plugin.json` must also define `live: true`
|
||||
*
|
||||
* @experimental
|
||||
* @alpha -- experimental
|
||||
*/
|
||||
channelSupport?: LiveChannelSupport;
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ export enum LiveChannelScope {
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* @alpha -- experimental
|
||||
*/
|
||||
export interface LiveChannelConfig<TMessage = any> {
|
||||
/**
|
||||
@ -69,7 +69,7 @@ export enum LiveChannelEventType {
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* @alpha -- experimental
|
||||
*/
|
||||
export interface LiveChannelStatusEvent {
|
||||
type: LiveChannelEventType.Status;
|
||||
@ -101,12 +101,12 @@ export interface LiveChannelStatusEvent {
|
||||
|
||||
export interface LiveChannelJoinEvent {
|
||||
type: LiveChannelEventType.Join;
|
||||
user: any; // @experimental -- will be filled in when we improve the UI
|
||||
user: any; // @alpha -- experimental -- will be filled in when we improve the UI
|
||||
}
|
||||
|
||||
export interface LiveChannelLeaveEvent {
|
||||
type: LiveChannelEventType.Leave;
|
||||
user: any; // @experimental -- will be filled in when we improve the UI
|
||||
user: any; // @alpha -- experimental -- will be filled in when we improve the UI
|
||||
}
|
||||
|
||||
export interface LiveChannelMessageEvent<T> {
|
||||
@ -137,14 +137,14 @@ export function isLiveChannelMessageEvent<T>(evt: LiveChannelEvent<T>): evt is L
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* @alpha -- experimental
|
||||
*/
|
||||
export interface LiveChannelPresenceStatus {
|
||||
users: any; // @experimental -- will be filled in when we improve the UI
|
||||
users: any; // @alpha -- experimental -- will be filled in when we improve the UI
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* @alpha -- experimental
|
||||
*/
|
||||
export interface LiveChannelAddress {
|
||||
scope: LiveChannelScope;
|
||||
@ -160,7 +160,7 @@ export function isValidLiveChannelAddress(addr?: LiveChannelAddress): addr is Li
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* @alpha -- experimental
|
||||
*/
|
||||
export interface LiveChannel<TMessage = any, TPublish = any> {
|
||||
/** The fully qualified channel id: ${scope}/${namespace}/${path} */
|
||||
@ -201,7 +201,7 @@ export interface LiveChannel<TMessage = any, TPublish = any> {
|
||||
}
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* @alpha -- experimental
|
||||
*/
|
||||
export interface LiveChannelSupport {
|
||||
/**
|
||||
|
@ -2,7 +2,7 @@ import { LiveChannel, LiveChannelAddress } from '@grafana/data';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
/**
|
||||
* @experimental
|
||||
* @alpha -- experimental
|
||||
*/
|
||||
export interface GrafanaLiveSrv {
|
||||
/**
|
||||
@ -42,7 +42,7 @@ export const setGrafanaLiveSrv = (instance: GrafanaLiveSrv) => {
|
||||
* Used to retrieve the {@link GrafanaLiveSrv} that allows you to subscribe to
|
||||
* server side events and streams
|
||||
*
|
||||
* @experimental
|
||||
* @alpha -- experimental
|
||||
* @public
|
||||
*/
|
||||
export const getGrafanaLiveSrv = (): GrafanaLiveSrv => singletonInstance;
|
||||
|
@ -14,7 +14,7 @@ export interface CodeEditorProps {
|
||||
/**
|
||||
* Callback after the editor has mounted that gives you raw access to monaco
|
||||
*
|
||||
* @experimental - real type is: monaco.editor.IStandaloneCodeEditor
|
||||
* @alpha -- experimental - real type is: monaco.editor.IStandaloneCodeEditor
|
||||
*/
|
||||
onEditorDidMount?: (editor: any) => void;
|
||||
|
||||
|
@ -0,0 +1,93 @@
|
||||
import React, { ChangeEvent } from 'react';
|
||||
import {
|
||||
DataTransformerID,
|
||||
SelectableValue,
|
||||
standardTransformers,
|
||||
TransformerRegistyItem,
|
||||
TransformerUIProps,
|
||||
} from '@grafana/data';
|
||||
import { Input, Select } from '@grafana/ui';
|
||||
import {
|
||||
ConcatenateTransformerOptions,
|
||||
ConcatenateFrameNameMode,
|
||||
} from '@grafana/data/src/transformations/transformers/concat';
|
||||
|
||||
interface ConcatenateTransformerEditorProps extends TransformerUIProps<ConcatenateTransformerOptions> {}
|
||||
|
||||
const nameModes: Array<SelectableValue<ConcatenateFrameNameMode>> = [
|
||||
{ value: ConcatenateFrameNameMode.FieldName, label: 'Copy frame name to field name' },
|
||||
{ value: ConcatenateFrameNameMode.Label, label: 'Add a label with the frame name' },
|
||||
{ value: ConcatenateFrameNameMode.Drop, label: 'Ignore the frame name' },
|
||||
];
|
||||
|
||||
export class ConcatenateTransformerEditor extends React.PureComponent<ConcatenateTransformerEditorProps> {
|
||||
constructor(props: ConcatenateTransformerEditorProps) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
onModeChanged = (value: SelectableValue<ConcatenateFrameNameMode>) => {
|
||||
const { options, onChange } = this.props;
|
||||
const frameNameMode = value.value ?? ConcatenateFrameNameMode.FieldName;
|
||||
onChange({
|
||||
...options,
|
||||
frameNameMode,
|
||||
});
|
||||
};
|
||||
|
||||
onLabelChanged = (evt: ChangeEvent<HTMLInputElement>) => {
|
||||
const { options } = this.props;
|
||||
this.props.onChange({
|
||||
...options,
|
||||
frameNameLabel: evt.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
//---------------------------------------------------------
|
||||
// Render
|
||||
//---------------------------------------------------------
|
||||
|
||||
render() {
|
||||
const { options } = this.props;
|
||||
|
||||
const frameNameMode = options.frameNameMode ?? ConcatenateFrameNameMode.FieldName;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<div className="gf-form-label width-8">Name</div>
|
||||
<Select
|
||||
className="width-18"
|
||||
options={nameModes}
|
||||
value={nameModes.find(v => v.value === frameNameMode)}
|
||||
onChange={this.onModeChanged}
|
||||
menuPlacement="bottom"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{frameNameMode === ConcatenateFrameNameMode.Label && (
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<div className="gf-form-label width-8">Label</div>
|
||||
<Input
|
||||
className="width-18"
|
||||
value={options.frameNameLabel ?? ''}
|
||||
placeholder="frame"
|
||||
onChange={this.onLabelChanged}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const concatenateTransformRegistryItem: TransformerRegistyItem<ConcatenateTransformerOptions> = {
|
||||
id: DataTransformerID.concatenate,
|
||||
editor: ConcatenateTransformerEditor,
|
||||
transformation: standardTransformers.concatenateTransformer,
|
||||
name: 'Concatenate fields',
|
||||
description:
|
||||
'Combine all fields into a single frame. Values will be appended with undefined values if not the same length.',
|
||||
};
|
@ -9,6 +9,7 @@ import { labelsToFieldsTransformerRegistryItem } from '../components/Transformer
|
||||
import { groupByTransformRegistryItem } from '../components/TransformersUI/GroupByTransformerEditor';
|
||||
import { mergeTransformerRegistryItem } from '../components/TransformersUI/MergeTransformerEditor';
|
||||
import { seriesToRowsTransformerRegistryItem } from '../components/TransformersUI/SeriesToRowsTransformerEditor';
|
||||
import { concatenateTransformRegistryItem } from '../components/TransformersUI/ConcatenateTransformerEditor';
|
||||
|
||||
export const getStandardTransformers = (): Array<TransformerRegistyItem<any>> => {
|
||||
return [
|
||||
@ -18,6 +19,7 @@ export const getStandardTransformers = (): Array<TransformerRegistyItem<any>> =>
|
||||
organizeFieldsTransformRegistryItem,
|
||||
seriesToFieldsTransformerRegistryItem,
|
||||
seriesToRowsTransformerRegistryItem,
|
||||
concatenateTransformRegistryItem,
|
||||
calculateFieldTransformRegistryItem,
|
||||
labelsToFieldsTransformerRegistryItem,
|
||||
groupByTransformRegistryItem,
|
||||
|
Loading…
Reference in New Issue
Block a user