Commit fe423644 by Ryan McKinley Committed by GitHub

Transformations: add Concatenate fields transformer (#28237)

parent cb72242d
import { appendTransformer } from './transformers/append'; import { appendTransformer } from './transformers/append';
import { reduceTransformer } from './transformers/reduce'; import { reduceTransformer } from './transformers/reduce';
import { concatenateTransformer } from './transformers/concat';
import { calculateFieldTransformer } from './transformers/calculateField'; import { calculateFieldTransformer } from './transformers/calculateField';
import { filterFieldsTransformer, filterFramesTransformer } from './transformers/filter'; import { filterFieldsTransformer, filterFramesTransformer } from './transformers/filter';
import { filterFieldsByNameTransformer } from './transformers/filterByName'; import { filterFieldsByNameTransformer } from './transformers/filterByName';
...@@ -25,6 +26,7 @@ export const standardTransformers = { ...@@ -25,6 +26,7 @@ export const standardTransformers = {
organizeFieldsTransformer, organizeFieldsTransformer,
appendTransformer, appendTransformer,
reduceTransformer, reduceTransformer,
concatenateTransformer,
calculateFieldTransformer, calculateFieldTransformer,
seriesToColumnsTransformer, seriesToColumnsTransformer,
seriesToRowsTransformer, seriesToRowsTransformer,
......
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",
},
]
`);
});
});
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 { ...@@ -10,6 +10,7 @@ export enum DataTransformerID {
seriesToColumns = 'seriesToColumns', seriesToColumns = 'seriesToColumns',
seriesToRows = 'seriesToRows', seriesToRows = 'seriesToRows',
merge = 'merge', merge = 'merge',
concatenate = 'concatenate',
labelsToFields = 'labelsToFields', labelsToFields = 'labelsToFields',
filterFields = 'filterFields', filterFields = 'filterFields',
filterFieldsByName = 'filterFieldsByName', filterFieldsByName = 'filterFieldsByName',
......
...@@ -291,7 +291,7 @@ export abstract class DataSourceApi< ...@@ -291,7 +291,7 @@ export abstract class DataSourceApi<
* *
* Note: `plugin.json` must also define `live: true` * Note: `plugin.json` must also define `live: true`
* *
* @experimental * @alpha -- experimental
*/ */
channelSupport?: LiveChannelSupport; channelSupport?: LiveChannelSupport;
} }
......
...@@ -15,7 +15,7 @@ export enum LiveChannelScope { ...@@ -15,7 +15,7 @@ export enum LiveChannelScope {
} }
/** /**
* @experimental * @alpha -- experimental
*/ */
export interface LiveChannelConfig<TMessage = any> { export interface LiveChannelConfig<TMessage = any> {
/** /**
...@@ -69,7 +69,7 @@ export enum LiveChannelEventType { ...@@ -69,7 +69,7 @@ export enum LiveChannelEventType {
} }
/** /**
* @experimental * @alpha -- experimental
*/ */
export interface LiveChannelStatusEvent { export interface LiveChannelStatusEvent {
type: LiveChannelEventType.Status; type: LiveChannelEventType.Status;
...@@ -101,12 +101,12 @@ export interface LiveChannelStatusEvent { ...@@ -101,12 +101,12 @@ export interface LiveChannelStatusEvent {
export interface LiveChannelJoinEvent { export interface LiveChannelJoinEvent {
type: LiveChannelEventType.Join; 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 { export interface LiveChannelLeaveEvent {
type: LiveChannelEventType.Leave; 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> { export interface LiveChannelMessageEvent<T> {
...@@ -137,14 +137,14 @@ export function isLiveChannelMessageEvent<T>(evt: LiveChannelEvent<T>): evt is L ...@@ -137,14 +137,14 @@ export function isLiveChannelMessageEvent<T>(evt: LiveChannelEvent<T>): evt is L
} }
/** /**
* @experimental * @alpha -- experimental
*/ */
export interface LiveChannelPresenceStatus { 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 { export interface LiveChannelAddress {
scope: LiveChannelScope; scope: LiveChannelScope;
...@@ -160,7 +160,7 @@ export function isValidLiveChannelAddress(addr?: LiveChannelAddress): addr is Li ...@@ -160,7 +160,7 @@ export function isValidLiveChannelAddress(addr?: LiveChannelAddress): addr is Li
} }
/** /**
* @experimental * @alpha -- experimental
*/ */
export interface LiveChannel<TMessage = any, TPublish = any> { export interface LiveChannel<TMessage = any, TPublish = any> {
/** The fully qualified channel id: ${scope}/${namespace}/${path} */ /** The fully qualified channel id: ${scope}/${namespace}/${path} */
...@@ -201,7 +201,7 @@ export interface LiveChannel<TMessage = any, TPublish = any> { ...@@ -201,7 +201,7 @@ export interface LiveChannel<TMessage = any, TPublish = any> {
} }
/** /**
* @experimental * @alpha -- experimental
*/ */
export interface LiveChannelSupport { export interface LiveChannelSupport {
/** /**
......
...@@ -2,7 +2,7 @@ import { LiveChannel, LiveChannelAddress } from '@grafana/data'; ...@@ -2,7 +2,7 @@ import { LiveChannel, LiveChannelAddress } from '@grafana/data';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
/** /**
* @experimental * @alpha -- experimental
*/ */
export interface GrafanaLiveSrv { export interface GrafanaLiveSrv {
/** /**
...@@ -42,7 +42,7 @@ export const setGrafanaLiveSrv = (instance: GrafanaLiveSrv) => { ...@@ -42,7 +42,7 @@ export const setGrafanaLiveSrv = (instance: GrafanaLiveSrv) => {
* Used to retrieve the {@link GrafanaLiveSrv} that allows you to subscribe to * Used to retrieve the {@link GrafanaLiveSrv} that allows you to subscribe to
* server side events and streams * server side events and streams
* *
* @experimental * @alpha -- experimental
* @public * @public
*/ */
export const getGrafanaLiveSrv = (): GrafanaLiveSrv => singletonInstance; export const getGrafanaLiveSrv = (): GrafanaLiveSrv => singletonInstance;
...@@ -14,7 +14,7 @@ export interface CodeEditorProps { ...@@ -14,7 +14,7 @@ export interface CodeEditorProps {
/** /**
* Callback after the editor has mounted that gives you raw access to monaco * 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; onEditorDidMount?: (editor: any) => void;
......
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 ...@@ -9,6 +9,7 @@ import { labelsToFieldsTransformerRegistryItem } from '../components/Transformer
import { groupByTransformRegistryItem } from '../components/TransformersUI/GroupByTransformerEditor'; import { groupByTransformRegistryItem } from '../components/TransformersUI/GroupByTransformerEditor';
import { mergeTransformerRegistryItem } from '../components/TransformersUI/MergeTransformerEditor'; import { mergeTransformerRegistryItem } from '../components/TransformersUI/MergeTransformerEditor';
import { seriesToRowsTransformerRegistryItem } from '../components/TransformersUI/SeriesToRowsTransformerEditor'; import { seriesToRowsTransformerRegistryItem } from '../components/TransformersUI/SeriesToRowsTransformerEditor';
import { concatenateTransformRegistryItem } from '../components/TransformersUI/ConcatenateTransformerEditor';
export const getStandardTransformers = (): Array<TransformerRegistyItem<any>> => { export const getStandardTransformers = (): Array<TransformerRegistyItem<any>> => {
return [ return [
...@@ -18,6 +19,7 @@ export const getStandardTransformers = (): Array<TransformerRegistyItem<any>> => ...@@ -18,6 +19,7 @@ export const getStandardTransformers = (): Array<TransformerRegistyItem<any>> =>
organizeFieldsTransformRegistryItem, organizeFieldsTransformRegistryItem,
seriesToFieldsTransformerRegistryItem, seriesToFieldsTransformerRegistryItem,
seriesToRowsTransformerRegistryItem, seriesToRowsTransformerRegistryItem,
concatenateTransformRegistryItem,
calculateFieldTransformRegistryItem, calculateFieldTransformRegistryItem,
labelsToFieldsTransformerRegistryItem, labelsToFieldsTransformerRegistryItem,
groupByTransformRegistryItem, groupByTransformRegistryItem,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment