Commit 58257a95 by Dominik Prokop Committed by GitHub

Transformations: Refactor to use single registry for transformations (#23502)

* Use single registry for transformations

* Fix transformations tests

* Added documentation comments and minor refactor

* Added documentation comments and minor refactor
Minor misunderstanding between me and Typescript. We should be good friends back now.

* Fix registry import
parent 7f61f3cc
export * from './matchers/ids'; export * from './matchers/ids';
export * from './transformers/ids'; export * from './transformers/ids';
export * from './matchers'; export * from './matchers';
export * from './transformers'; export { standardTransformers } from './transformers';
export * from './fieldReducer'; export * from './fieldReducer';
export { FilterFieldsByNameTransformerOptions } from './transformers/filterByName'; export { FilterFieldsByNameTransformerOptions } from './transformers/filterByName';
export { FilterFramesByRefIdTransformerOptions } from './transformers/filterByRefId'; export { FilterFramesByRefIdTransformerOptions } from './transformers/filterByRefId';
export { ReduceTransformerOptions } from './transformers/reduce'; export { ReduceTransformerOptions } from './transformers/reduce';
export { OrganizeFieldsTransformerOptions } from './transformers/organize'; export { OrganizeFieldsTransformerOptions } from './transformers/organize';
export { createOrderFieldsComparer } from './transformers/order'; export { createOrderFieldsComparer } from './transformers/order';
export { transformDataFrame } from './transformDataFrame';
export {
TransformerRegistyItem,
TransformerUIProps,
standardTransformersRegistry,
} from './standardTransformersRegistry';
import React from 'react';
import { DataFrame, DataTransformerInfo } from '../types';
import { Registry, RegistryItem } from '../utils/Registry';
export interface TransformerUIProps<T> {
/**
* Transformer configuration, persisted on panel's model
*/
options: T;
/**
* Pre-transform data rames
*/
input: DataFrame[];
onChange: (options: T) => void;
}
export interface TransformerRegistyItem<TOptions> extends RegistryItem {
/**
* Object describing transformer configuration
*/
transformation: DataTransformerInfo<TOptions>;
/**
* React component used as UI for the transformer
*/
editor: React.ComponentType<TransformerUIProps<TOptions>>;
}
/**
* Registry of transformation options that can be driven by
* stored configuration files.
*/
export const standardTransformersRegistry = new Registry<TransformerRegistyItem<any>>();
import { DataFrame, DataTransformerConfig } from '../types';
import { standardTransformersRegistry } from './standardTransformersRegistry';
/**
* Apply configured transformations to the input data
*/
export function transformDataFrame(options: DataTransformerConfig[], data: DataFrame[]): DataFrame[] {
let processed = data;
for (const config of options) {
const info = standardTransformersRegistry.get(config.id);
if (!info) {
return data;
}
const transformer = info.transformation.transformer(config.options);
const after = transformer(processed);
// Add a key to the metadata if the data changed
if (after && after !== processed) {
for (const series of after) {
if (!series.meta) {
series.meta = {};
}
if (!series.meta.transformations) {
series.meta.transformations = [info.id];
} else {
series.meta.transformations = [...series.meta.transformations, info.id];
}
}
processed = after;
}
}
return processed;
}
import { DataTransformerID } from './transformers/ids';
import { transformersRegistry } from './transformers';
import { toDataFrame } from '../dataframe/processDataFrame';
import { ReducerID } from './fieldReducer';
import { DataFrameView } from '../dataframe/DataFrameView';
describe('Transformers', () => {
it('should load all transformeres', () => {
for (const name of Object.keys(DataTransformerID)) {
const calc = transformersRegistry.get(name);
expect(calc.id).toBe(name);
}
});
const seriesWithValues = toDataFrame({
fields: [
{ name: 'A', values: [1, 2, 3, 4] }, // Numbers
{ name: 'B', values: ['a', 'b', 'c', 'd'] }, // Strings
],
});
it('should use fluent API', () => {
const results = transformersRegistry.reduce([seriesWithValues], {
reducers: [ReducerID.first],
});
expect(results.length).toBe(1);
const view = new DataFrameView(results[0]).toJSON();
expect(view).toEqual([
{ Field: 'A', first: 1 }, // Row 0
{ Field: 'B', first: 'a' }, // Row 1
]);
});
});
import { DataFrame } from '../types/dataFrame'; import { appendTransformer } from './transformers/append';
import { Registry } from '../utils/Registry'; import { reduceTransformer } from './transformers/reduce';
import { AppendOptions, appendTransformer } from './transformers/append';
import { reduceTransformer, ReduceTransformerOptions } from './transformers/reduce';
import { filterFieldsTransformer, filterFramesTransformer } from './transformers/filter'; import { filterFieldsTransformer, filterFramesTransformer } from './transformers/filter';
import { filterFieldsByNameTransformer, FilterFieldsByNameTransformerOptions } from './transformers/filterByName'; import { filterFieldsByNameTransformer } from './transformers/filterByName';
import { noopTransformer } from './transformers/noop'; import { noopTransformer } from './transformers/noop';
import { DataTransformerConfig, DataTransformerInfo } from '../types/transformations';
import { filterFramesByRefIdTransformer } from './transformers/filterByRefId'; import { filterFramesByRefIdTransformer } from './transformers/filterByRefId';
import { orderFieldsTransformer } from './transformers/order'; import { orderFieldsTransformer } from './transformers/order';
import { organizeFieldsTransformer } from './transformers/organize'; import { organizeFieldsTransformer } from './transformers/organize';
import { seriesToColumnsTransformer } from './transformers/seriesToColumns'; import { seriesToColumnsTransformer } from './transformers/seriesToColumns';
import { renameFieldsTransformer } from './transformers/rename'; import { renameFieldsTransformer } from './transformers/rename';
// Initalize the Registry export const standardTransformers = {
/**
* Apply configured transformations to the input data
*/
export function transformDataFrame(options: DataTransformerConfig[], data: DataFrame[]): DataFrame[] {
let processed = data;
for (const config of options) {
const info = transformersRegistry.get(config.id);
const transformer = info.transformer(config.options);
const after = transformer(processed);
// Add a key to the metadata if the data changed
if (after && after !== processed) {
for (const series of after) {
if (!series.meta) {
series.meta = {};
}
if (!series.meta.transformations) {
series.meta.transformations = [info.id];
} else {
series.meta.transformations = [...series.meta.transformations, info.id];
}
}
processed = after;
}
}
return processed;
}
/**
* Registry of transformation options that can be driven by
* stored configuration files.
*/
class TransformerRegistry extends Registry<DataTransformerInfo> {
// ------------------------------------------------------------
// Nacent options for more functional programming
// The API to these functions should change to match the actual
// needs of people trying to use it.
// filterFields|Frames is left off since it is likely easier to
// support with `frames.filter( f => {...} )`
// ------------------------------------------------------------
append(data: DataFrame[], options?: AppendOptions): DataFrame | undefined {
return appendTransformer.transformer(options || appendTransformer.defaultOptions)(data)[0];
}
reduce(data: DataFrame[], options: ReduceTransformerOptions): DataFrame[] {
return reduceTransformer.transformer(options)(data);
}
}
export const transformersRegistry = new TransformerRegistry(() => [
noopTransformer, noopTransformer,
filterFieldsTransformer, filterFieldsTransformer,
filterFieldsByNameTransformer, filterFieldsByNameTransformer,
...@@ -76,6 +21,4 @@ export const transformersRegistry = new TransformerRegistry(() => [ ...@@ -76,6 +21,4 @@ export const transformersRegistry = new TransformerRegistry(() => [
reduceTransformer, reduceTransformer,
seriesToColumnsTransformer, seriesToColumnsTransformer,
renameFieldsTransformer, renameFieldsTransformer,
]); };
export { ReduceTransformerOptions, FilterFieldsByNameTransformerOptions };
import { DataTransformerID } from './ids'; import { DataTransformerID } from './ids';
import { toDataFrame } from '../../dataframe/processDataFrame'; import { toDataFrame } from '../../dataframe/processDataFrame';
import { transformDataFrame } from '../transformers'; import { mockTransformationsRegistry } from '../../utils/tests/mockTransformationsRegistry';
import { transformersRegistry } from '../transformers'; import { appendTransformer } from './append';
import { transformDataFrame } from '../transformDataFrame';
const seriesAB = toDataFrame({ const seriesAB = toDataFrame({
columns: [{ text: 'A' }, { text: 'B' }], columns: [{ text: 'A' }, { text: 'B' }],
...@@ -20,13 +21,14 @@ const seriesBC = toDataFrame({ ...@@ -20,13 +21,14 @@ const seriesBC = toDataFrame({
}); });
describe('Append Transformer', () => { describe('Append Transformer', () => {
beforeAll(() => {
mockTransformationsRegistry([appendTransformer]);
});
it('filters by include', () => { it('filters by include', () => {
const cfg = { const cfg = {
id: DataTransformerID.append, id: DataTransformerID.append,
options: {}, options: {},
}; };
const x = transformersRegistry.get(DataTransformerID.append);
expect(x.id).toBe(cfg.id);
const processed = transformDataFrame([cfg], [seriesAB, seriesBC])[0]; const processed = transformDataFrame([cfg], [seriesAB, seriesBC])[0];
expect(processed.fields.length).toBe(3); expect(processed.fields.length).toBe(3);
......
...@@ -2,7 +2,9 @@ import { FieldType } from '../../types/dataFrame'; ...@@ -2,7 +2,9 @@ import { FieldType } from '../../types/dataFrame';
import { DataTransformerID } from './ids'; import { DataTransformerID } from './ids';
import { toDataFrame } from '../../dataframe/processDataFrame'; import { toDataFrame } from '../../dataframe/processDataFrame';
import { FieldMatcherID } from '../matchers/ids'; import { FieldMatcherID } from '../matchers/ids';
import { transformDataFrame } from '../transformers'; import { mockTransformationsRegistry } from '../../utils/tests/mockTransformationsRegistry';
import { filterFieldsTransformer } from './filter';
import { transformDataFrame } from '../transformDataFrame';
export const simpleSeriesWithTypes = toDataFrame({ export const simpleSeriesWithTypes = toDataFrame({
fields: [ fields: [
...@@ -14,6 +16,10 @@ export const simpleSeriesWithTypes = toDataFrame({ ...@@ -14,6 +16,10 @@ export const simpleSeriesWithTypes = toDataFrame({
}); });
describe('Filter Transformer', () => { describe('Filter Transformer', () => {
beforeAll(() => {
mockTransformationsRegistry([filterFieldsTransformer]);
});
it('filters by include', () => { it('filters by include', () => {
const cfg = { const cfg = {
id: DataTransformerID.filterFields, id: DataTransformerID.filterFields,
......
import { DataTransformerID } from './ids'; import { DataTransformerID } from './ids';
import { transformDataFrame } from '../transformers';
import { toDataFrame } from '../../dataframe/processDataFrame'; import { toDataFrame } from '../../dataframe/processDataFrame';
import { FieldType } from '../../types/dataFrame'; import { FieldType } from '../../types/dataFrame';
import { mockTransformationsRegistry } from '../../utils/tests/mockTransformationsRegistry';
import { filterFieldsByNameTransformer } from './filterByName';
import { filterFieldsTransformer } from './filter';
import { transformDataFrame } from '../transformDataFrame';
export const seriesWithNamesToMatch = toDataFrame({ export const seriesWithNamesToMatch = toDataFrame({
fields: [ fields: [
...@@ -13,6 +16,10 @@ export const seriesWithNamesToMatch = toDataFrame({ ...@@ -13,6 +16,10 @@ export const seriesWithNamesToMatch = toDataFrame({
}); });
describe('filterByName transformer', () => { describe('filterByName transformer', () => {
beforeAll(() => {
mockTransformationsRegistry([filterFieldsByNameTransformer, filterFieldsTransformer]);
});
it('returns original series if no options provided', () => { it('returns original series if no options provided', () => {
const cfg = { const cfg = {
id: DataTransformerID.filterFields, id: DataTransformerID.filterFields,
......
import { DataTransformerID } from './ids'; import { DataTransformerID } from './ids';
import { transformDataFrame } from '../transformers';
import { toDataFrame } from '../../dataframe/processDataFrame'; import { toDataFrame } from '../../dataframe/processDataFrame';
import { mockTransformationsRegistry } from '../../utils/tests/mockTransformationsRegistry';
import { filterFramesByRefIdTransformer } from './filterByRefId';
import { transformDataFrame } from '../transformDataFrame';
export const allSeries = [ export const allSeries = [
toDataFrame({ toDataFrame({
...@@ -18,6 +20,9 @@ export const allSeries = [ ...@@ -18,6 +20,9 @@ export const allSeries = [
]; ];
describe('filterByRefId transformer', () => { describe('filterByRefId transformer', () => {
beforeAll(() => {
mockTransformationsRegistry([filterFramesByRefIdTransformer]);
});
it('returns all series if no options provided', () => { it('returns all series if no options provided', () => {
const cfg = { const cfg = {
id: DataTransformerID.filterByRefId, id: DataTransformerID.filterByRefId,
......
...@@ -6,9 +6,13 @@ import { ...@@ -6,9 +6,13 @@ import {
toDataFrame, toDataFrame,
transformDataFrame, transformDataFrame,
} from '@grafana/data'; } from '@grafana/data';
import { OrderFieldsTransformerOptions } from './order'; import { orderFieldsTransformer, OrderFieldsTransformerOptions } from './order';
import { mockTransformationsRegistry } from '../../utils/tests/mockTransformationsRegistry';
describe('Order Transformer', () => { describe('Order Transformer', () => {
beforeAll(() => {
mockTransformationsRegistry([orderFieldsTransformer]);
});
describe('when consistent data is received', () => { describe('when consistent data is received', () => {
const data = toDataFrame({ const data = toDataFrame({
name: 'A', name: 'A',
......
...@@ -6,9 +6,14 @@ import { ...@@ -6,9 +6,14 @@ import {
toDataFrame, toDataFrame,
transformDataFrame, transformDataFrame,
} from '@grafana/data'; } from '@grafana/data';
import { OrganizeFieldsTransformerOptions } from './organize'; import { organizeFieldsTransformer, OrganizeFieldsTransformerOptions } from './organize';
import { mockTransformationsRegistry } from '../../utils/tests/mockTransformationsRegistry';
describe('OrganizeFields Transformer', () => { describe('OrganizeFields Transformer', () => {
beforeAll(() => {
mockTransformationsRegistry([organizeFieldsTransformer]);
});
describe('when consistent data is received', () => { describe('when consistent data is received', () => {
const data = toDataFrame({ const data = toDataFrame({
name: 'A', name: 'A',
......
import { ReducerID } from '../fieldReducer'; import { ReducerID } from '../fieldReducer';
import { DataTransformerID } from './ids'; import { DataTransformerID } from './ids';
import { toDataFrame, toDataFrameDTO } from '../../dataframe/processDataFrame'; import { toDataFrame, toDataFrameDTO } from '../../dataframe/processDataFrame';
import { transformDataFrame } from '../transformers'; import { mockTransformationsRegistry } from '../../utils/tests/mockTransformationsRegistry';
import { reduceTransformer } from './reduce';
import { transformDataFrame } from '../transformDataFrame';
const seriesWithValues = toDataFrame({ const seriesWithValues = toDataFrame({
fields: [ fields: [
...@@ -11,6 +13,9 @@ const seriesWithValues = toDataFrame({ ...@@ -11,6 +13,9 @@ const seriesWithValues = toDataFrame({
}); });
describe('Reducer Transformer', () => { describe('Reducer Transformer', () => {
beforeAll(() => {
mockTransformationsRegistry([reduceTransformer]);
});
it('filters by include', () => { it('filters by include', () => {
const cfg = { const cfg = {
id: DataTransformerID.reduce, id: DataTransformerID.reduce,
......
...@@ -6,9 +6,14 @@ import { ...@@ -6,9 +6,14 @@ import {
toDataFrame, toDataFrame,
transformDataFrame, transformDataFrame,
} from '@grafana/data'; } from '@grafana/data';
import { RenameFieldsTransformerOptions } from './rename'; import { RenameFieldsTransformerOptions, renameFieldsTransformer } from './rename';
import { mockTransformationsRegistry } from '../../utils/tests/mockTransformationsRegistry';
describe('Rename Transformer', () => { describe('Rename Transformer', () => {
beforeAll(() => {
mockTransformationsRegistry([renameFieldsTransformer]);
});
describe('when consistent data is received', () => { describe('when consistent data is received', () => {
const data = toDataFrame({ const data = toDataFrame({
name: 'A', name: 'A',
......
...@@ -6,9 +6,13 @@ import { ...@@ -6,9 +6,13 @@ import {
toDataFrame, toDataFrame,
transformDataFrame, transformDataFrame,
} from '@grafana/data'; } from '@grafana/data';
import { SeriesToColumnsOptions } from './seriesToColumns'; import { SeriesToColumnsOptions, seriesToColumnsTransformer } from './seriesToColumns';
import { mockTransformationsRegistry } from '../../utils/tests/mockTransformationsRegistry';
describe('SeriesToColumns Transformer', () => { describe('SeriesToColumns Transformer', () => {
beforeAll(() => {
mockTransformationsRegistry([seriesToColumnsTransformer]);
});
const everySecondSeries = toDataFrame({ const everySecondSeries = toDataFrame({
name: 'even', name: 'even',
fields: [ fields: [
......
...@@ -2,16 +2,26 @@ import { DataFrame, Field } from './dataFrame'; ...@@ -2,16 +2,26 @@ import { DataFrame, Field } from './dataFrame';
import { RegistryItemWithOptions } from '../utils/Registry'; import { RegistryItemWithOptions } from '../utils/Registry';
/** /**
* Immutable data transformation * Function that transform data frames (AKA transformer)
*/ */
export type DataTransformer = (data: DataFrame[]) => DataFrame[]; export type DataTransformer = (data: DataFrame[]) => DataFrame[];
export interface DataTransformerInfo<TOptions = any> extends RegistryItemWithOptions { export interface DataTransformerInfo<TOptions = any> extends RegistryItemWithOptions {
/**
* Function that configures transformation and returns a transformer
* @param options
*/
transformer: (options: TOptions) => DataTransformer; transformer: (options: TOptions) => DataTransformer;
} }
export interface DataTransformerConfig<TOptions = any> { export interface DataTransformerConfig<TOptions = any> {
/**
* Unique identifier of transformer
*/
id: string; id: string;
/**
* Options to be passed to the transformer
*/
options: TOptions; options: TOptions;
} }
......
import { standardTransformersRegistry } from '../../transformations';
import { DataTransformerInfo } from '../../types';
export const mockTransformationsRegistry = (transformers: Array<DataTransformerInfo<any>>) => {
standardTransformersRegistry.setInit(() => {
return transformers.map(t => {
return {
id: t.id,
name: t.name,
transformation: t,
description: t.description,
editor: () => null,
};
});
});
};
...@@ -25,15 +25,13 @@ const buttonVariantStyles = (from: string, to: string, textColor: string) => css ...@@ -25,15 +25,13 @@ const buttonVariantStyles = (from: string, to: string, textColor: string) => css
const getPropertiesForVariant = (theme: GrafanaTheme, variant: ButtonVariant) => { const getPropertiesForVariant = (theme: GrafanaTheme, variant: ButtonVariant) => {
switch (variant) { switch (variant) {
case 'secondary': case 'secondary':
const from = selectThemeVariant({ light: theme.colors.gray7, dark: theme.colors.gray15 }, theme.type) as string; const from = selectThemeVariant({ light: theme.colors.gray7, dark: theme.colors.gray10 }, theme.type) as string;
const to = selectThemeVariant( const to = selectThemeVariant(
{ {
light: tinycolor(from) light: tinycolor(from)
.darken(5) .darken(5)
.toString(), .toString(),
dark: tinycolor(from) dark: theme.colors.gray05,
.lighten(4)
.toString(),
}, },
theme.type theme.type
) as string; ) as string;
......
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import { FilterFieldsByNameTransformerOptions, DataTransformerID, transformersRegistry, KeyValue } from '@grafana/data'; import {
import { TransformerUIProps, TransformerUIRegistyItem } from './types'; DataTransformerID,
FilterFieldsByNameTransformerOptions,
KeyValue,
standardTransformers,
TransformerRegistyItem,
TransformerUIProps,
} from '@grafana/data';
import { ThemeContext } from '../../themes/ThemeContext'; import { ThemeContext } from '../../themes/ThemeContext';
import { css, cx } from 'emotion'; import { css, cx } from 'emotion';
import { InlineList } from '../List/InlineList'; import { InlineList } from '../List/InlineList';
...@@ -154,10 +160,10 @@ const FilterPill: React.FC<FilterPillProps> = ({ label, selected, onClick }) => ...@@ -154,10 +160,10 @@ const FilterPill: React.FC<FilterPillProps> = ({ label, selected, onClick }) =>
); );
}; };
export const filterFieldsByNameTransformRegistryItem: TransformerUIRegistyItem<FilterFieldsByNameTransformerOptions> = { export const filterFieldsByNameTransformRegistryItem: TransformerRegistyItem<FilterFieldsByNameTransformerOptions> = {
id: DataTransformerID.filterFieldsByName, id: DataTransformerID.filterFieldsByName,
component: FilterByNameTransformerEditor, editor: FilterByNameTransformerEditor,
transformer: transformersRegistry.get(DataTransformerID.filterFieldsByName), transformation: standardTransformers.filterFieldsByNameTransformer,
name: 'Filter by name', name: 'Filter by name',
description: 'UI for filter by name transformation', description: 'Filter fields by name',
}; };
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import { import {
FilterFramesByRefIdTransformerOptions,
DataTransformerID, DataTransformerID,
transformersRegistry, FilterFramesByRefIdTransformerOptions,
KeyValue, KeyValue,
standardTransformers,
TransformerRegistyItem,
TransformerUIProps,
} from '@grafana/data'; } from '@grafana/data';
import { TransformerUIProps, TransformerUIRegistyItem } from './types';
import { ThemeContext } from '../../themes/ThemeContext'; import { ThemeContext } from '../../themes/ThemeContext';
import { css, cx } from 'emotion'; import { css, cx } from 'emotion';
import { InlineList } from '../List/InlineList'; import { InlineList } from '../List/InlineList';
...@@ -159,10 +160,10 @@ const FilterPill: React.FC<FilterPillProps> = ({ label, selected, onClick }) => ...@@ -159,10 +160,10 @@ const FilterPill: React.FC<FilterPillProps> = ({ label, selected, onClick }) =>
); );
}; };
export const filterFramesByRefIdTransformRegistryItem: TransformerUIRegistyItem<FilterFramesByRefIdTransformerOptions> = { export const filterFramesByRefIdTransformRegistryItem: TransformerRegistyItem<FilterFramesByRefIdTransformerOptions> = {
id: DataTransformerID.filterByRefId, id: DataTransformerID.filterByRefId,
component: FilterByRefIdTransformerEditor, editor: FilterByRefIdTransformerEditor,
transformer: transformersRegistry.get(DataTransformerID.filterByRefId), transformation: standardTransformers.filterFramesByRefIdTransformer,
name: 'Filter by refId', name: 'Filter by refId',
description: 'Filter results by refId', description: 'Filter results by refId',
}; };
import React, { useMemo, useCallback } from 'react'; import React, { useCallback, useMemo } from 'react';
import { css, cx } from 'emotion'; import { css, cx } from 'emotion';
import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd'; import { DragDropContext, Draggable, Droppable, DropResult } from 'react-beautiful-dnd';
import { import {
DataTransformerID, createOrderFieldsComparer,
transformersRegistry,
DataFrame, DataFrame,
DataTransformerID,
GrafanaTheme, GrafanaTheme,
createOrderFieldsComparer,
OrganizeFieldsTransformerOptions, OrganizeFieldsTransformerOptions,
standardTransformers,
TransformerRegistyItem,
TransformerUIProps,
} from '@grafana/data'; } from '@grafana/data';
import { TransformerUIRegistyItem, TransformerUIProps } from './types';
import { stylesFactory, useTheme } from '../../themes'; import { stylesFactory, useTheme } from '../../themes';
import { Button } from '../Button/Button'; import { Button } from '../Button/Button';
import { VerticalGroup } from '../Layout/Layout'; import { VerticalGroup } from '../Layout/Layout';
...@@ -217,10 +218,10 @@ const fieldNamesFromInput = (input: DataFrame[]): string[] => { ...@@ -217,10 +218,10 @@ const fieldNamesFromInput = (input: DataFrame[]): string[] => {
); );
}; };
export const organizeFieldsTransformRegistryItem: TransformerUIRegistyItem<OrganizeFieldsTransformerOptions> = { export const organizeFieldsTransformRegistryItem: TransformerRegistyItem<OrganizeFieldsTransformerOptions> = {
id: DataTransformerID.organize, id: DataTransformerID.organize,
component: OrganizeFieldsTransformerEditor, editor: OrganizeFieldsTransformerEditor,
transformer: transformersRegistry.get(DataTransformerID.organize), transformation: standardTransformers.organizeFieldsTransformer,
name: 'Organize fields', name: 'Organize fields',
description: 'UI for organizing fields', description: 'Order, filter and rename fields',
}; };
import React from 'react'; import React from 'react';
import { StatsPicker } from '../StatsPicker/StatsPicker'; import { StatsPicker } from '../StatsPicker/StatsPicker';
import { ReduceTransformerOptions, DataTransformerID, ReducerID, transformersRegistry } from '@grafana/data'; import {
import { TransformerUIRegistyItem, TransformerUIProps } from './types'; ReduceTransformerOptions,
DataTransformerID,
ReducerID,
standardTransformers,
TransformerRegistyItem,
TransformerUIProps,
} from '@grafana/data';
// TODO: Minimal implementation, needs some <3 // TODO: Minimal implementation, needs some <3
export const ReduceTransformerEditor: React.FC<TransformerUIProps<ReduceTransformerOptions>> = ({ export const ReduceTransformerEditor: React.FC<TransformerUIProps<ReduceTransformerOptions>> = ({
options, options,
onChange, onChange,
input,
}) => { }) => {
return ( return (
<StatsPicker <StatsPicker
...@@ -25,10 +30,10 @@ export const ReduceTransformerEditor: React.FC<TransformerUIProps<ReduceTransfor ...@@ -25,10 +30,10 @@ export const ReduceTransformerEditor: React.FC<TransformerUIProps<ReduceTransfor
); );
}; };
export const reduceTransformRegistryItem: TransformerUIRegistyItem<ReduceTransformerOptions> = { export const reduceTransformRegistryItem: TransformerRegistyItem<ReduceTransformerOptions> = {
id: DataTransformerID.reduce, id: DataTransformerID.reduce,
component: ReduceTransformerEditor, editor: ReduceTransformerEditor,
transformer: transformersRegistry.get(DataTransformerID.reduce), transformation: standardTransformers.reduceTransformer,
name: 'Reduce', name: 'Reduce',
description: 'UI for reduce transformation', description: 'Return a DataFrame with the reduction results',
}; };
import { Registry } from '@grafana/data';
import { reduceTransformRegistryItem } from './ReduceTransformerEditor';
import { filterFieldsByNameTransformRegistryItem } from './FilterByNameTransformerEditor';
import { filterFramesByRefIdTransformRegistryItem } from './FilterByRefIdTransformerEditor';
import { TransformerUIRegistyItem } from './types';
import { organizeFieldsTransformRegistryItem } from './OrganizeFieldsTransformerEditor';
export const transformersUIRegistry = new Registry<TransformerUIRegistyItem<any>>(() => {
return [
reduceTransformRegistryItem,
filterFieldsByNameTransformRegistryItem,
filterFramesByRefIdTransformRegistryItem,
organizeFieldsTransformRegistryItem,
];
});
import React from 'react';
import { DataFrame, RegistryItem, DataTransformerInfo } from '@grafana/data';
export interface TransformerUIRegistyItem<TOptions> extends RegistryItem {
component: React.ComponentType<TransformerUIProps<TOptions>>;
transformer: DataTransformerInfo<TOptions>;
}
export interface TransformerUIProps<T> {
// Transformer configuration, persisted on panel's model
options: T;
// Pre-transformation data frame
input: DataFrame[];
onChange: (options: T) => void;
}
...@@ -4,6 +4,7 @@ import { SelectableValue } from '@grafana/data'; ...@@ -4,6 +4,7 @@ import { SelectableValue } from '@grafana/data';
import { Button, ButtonVariant } from '../Button'; import { Button, ButtonVariant } from '../Button';
import { Select } from '../Select/Select'; import { Select } from '../Select/Select';
import { FullWidthButtonContainer } from '../Button/FullWidthButtonContainer'; import { FullWidthButtonContainer } from '../Button/FullWidthButtonContainer';
import { ComponentSize } from '../../types/size';
interface ValuePickerProps<T> { interface ValuePickerProps<T> {
/** Label to display on the picker button */ /** Label to display on the picker button */
...@@ -14,20 +15,29 @@ interface ValuePickerProps<T> { ...@@ -14,20 +15,29 @@ interface ValuePickerProps<T> {
options: Array<SelectableValue<T>>; options: Array<SelectableValue<T>>;
onChange: (value: SelectableValue<T>) => void; onChange: (value: SelectableValue<T>) => void;
variant?: ButtonVariant; variant?: ButtonVariant;
size?: ComponentSize;
isFullWidth?: boolean;
} }
export function ValuePicker<T>({ label, icon, options, onChange, variant }: ValuePickerProps<T>) { export function ValuePicker<T>({
label,
icon,
options,
onChange,
variant,
size,
isFullWidth = true,
}: ValuePickerProps<T>) {
const [isPicking, setIsPicking] = useState(false); const [isPicking, setIsPicking] = useState(false);
const buttonEl = (
<Button size={size || 'sm'} icon={icon || 'plus-circle'} onClick={() => setIsPicking(true)} variant={variant}>
{label}
</Button>
);
return ( return (
<> <>
{!isPicking && ( {!isPicking && (isFullWidth ? <FullWidthButtonContainer>{buttonEl}</FullWidthButtonContainer> : buttonEl)}
<FullWidthButtonContainer>
<Button size="sm" icon={icon || 'plus-circle'} onClick={() => setIsPicking(true)} variant={variant}>
{label}
</Button>
</FullWidthButtonContainer>
)}
{isPicking && ( {isPicking && (
<Select <Select
......
...@@ -100,7 +100,7 @@ export { DataLinksInlineEditor } from './DataLinks/DataLinksInlineEditor/DataLin ...@@ -100,7 +100,7 @@ export { DataLinksInlineEditor } from './DataLinks/DataLinksInlineEditor/DataLin
export { DataLinkInput } from './DataLinks/DataLinkInput'; export { DataLinkInput } from './DataLinks/DataLinkInput';
export { DataLinksContextMenu } from './DataLinks/DataLinksContextMenu'; export { DataLinksContextMenu } from './DataLinks/DataLinksContextMenu';
export { SeriesIcon } from './Legend/SeriesIcon'; export { SeriesIcon } from './Legend/SeriesIcon';
export { transformersUIRegistry } from './TransformersUI/transformers';
export { JSONFormatter } from './JSONFormatter/JSONFormatter'; export { JSONFormatter } from './JSONFormatter/JSONFormatter';
export { JsonExplorer } from './JSONFormatter/json_explorer/json_explorer'; export { JsonExplorer } from './JSONFormatter/json_explorer/json_explorer';
export { ErrorBoundary, ErrorBoundaryAlert } from './ErrorBoundary/ErrorBoundary'; export { ErrorBoundary, ErrorBoundaryAlert } from './ErrorBoundary/ErrorBoundary';
......
...@@ -8,8 +8,9 @@ export { default as ansicolor } from './ansicolor'; ...@@ -8,8 +8,9 @@ export { default as ansicolor } from './ansicolor';
import * as DOMUtil from './dom'; // includes Element.closest polyfil import * as DOMUtil from './dom'; // includes Element.closest polyfil
export { DOMUtil }; export { DOMUtil };
export { renderOrCallToRender } from './renderOrCallToRender';
// Exposes standard editors for registries of optionsUi config and panel options UI // Exposes standard editors for registries of optionsUi config and panel options UI
export { getStandardFieldConfigs, getStandardOptionEditors } from './standardEditors'; export { getStandardFieldConfigs, getStandardOptionEditors } from './standardEditors';
// Exposes standard transformers for registry of Transformations
export { renderOrCallToRender } from './renderOrCallToRender'; export { getStandardTransformers } from './standardTransformers';
import { TransformerRegistyItem } from '@grafana/data';
import { reduceTransformRegistryItem } from '../components/TransformersUI/ReduceTransformerEditor';
import { filterFieldsByNameTransformRegistryItem } from '../components/TransformersUI/FilterByNameTransformerEditor';
import { filterFramesByRefIdTransformRegistryItem } from '../components/TransformersUI/FilterByRefIdTransformerEditor';
import { organizeFieldsTransformRegistryItem } from '../components/TransformersUI/OrganizeFieldsTransformerEditor';
export const getStandardTransformers = (): Array<TransformerRegistyItem<any>> => {
return [
reduceTransformRegistryItem,
filterFieldsByNameTransformRegistryItem,
filterFramesByRefIdTransformRegistryItem,
organizeFieldsTransformRegistryItem,
];
};
...@@ -31,6 +31,7 @@ import { ...@@ -31,6 +31,7 @@ import {
setMarkdownOptions, setMarkdownOptions,
standardEditorsRegistry, standardEditorsRegistry,
standardFieldConfigEditorRegistry, standardFieldConfigEditorRegistry,
standardTransformersRegistry,
} from '@grafana/data'; } from '@grafana/data';
import appEvents from 'app/core/app_events'; import appEvents from 'app/core/app_events';
import { addClassIfNoOverlayScrollbar } from 'app/core/utils/scrollbar'; import { addClassIfNoOverlayScrollbar } from 'app/core/utils/scrollbar';
...@@ -45,7 +46,7 @@ import { reportPerformance } from './core/services/echo/EchoSrv'; ...@@ -45,7 +46,7 @@ import { reportPerformance } from './core/services/echo/EchoSrv';
import { PerformanceBackend } from './core/services/echo/backends/PerformanceBackend'; import { PerformanceBackend } from './core/services/echo/backends/PerformanceBackend';
import 'app/routes/GrafanaCtrl'; import 'app/routes/GrafanaCtrl';
import 'app/features/all'; import 'app/features/all';
import { getStandardFieldConfigs, getStandardOptionEditors } from '@grafana/ui'; import { getStandardFieldConfigs, getStandardOptionEditors, getStandardTransformers } from '@grafana/ui';
import { getDefaultVariableAdapters, variableAdapters } from './features/variables/adapters'; import { getDefaultVariableAdapters, variableAdapters } from './features/variables/adapters';
import { initDevFeatures } from './dev'; import { initDevFeatures } from './dev';
...@@ -97,6 +98,7 @@ export class GrafanaApp { ...@@ -97,6 +98,7 @@ export class GrafanaApp {
standardEditorsRegistry.setInit(getStandardOptionEditors); standardEditorsRegistry.setInit(getStandardOptionEditors);
standardFieldConfigEditorRegistry.setInit(getStandardFieldConfigs); standardFieldConfigEditorRegistry.setInit(getStandardFieldConfigs);
standardTransformersRegistry.setInit(getStandardTransformers);
variableAdapters.setInit(getDefaultVariableAdapters); variableAdapters.setInit(getDefaultVariableAdapters);
app.config( app.config(
......
import { css } from 'emotion';
import React from 'react'; import React from 'react';
import { transformersUIRegistry } from '@grafana/ui'; import { Container, CustomScrollbar, ValuePicker } from '@grafana/ui';
import { DataTransformerConfig, DataFrame, transformDataFrame, SelectableValue } from '@grafana/data'; import {
import { Button, CustomScrollbar, Select, Container } from '@grafana/ui'; DataFrame,
DataTransformerConfig,
SelectableValue,
standardTransformersRegistry,
transformDataFrame,
} from '@grafana/data';
import { TransformationOperationRow } from './TransformationOperationRow'; import { TransformationOperationRow } from './TransformationOperationRow';
interface Props { interface Props {
...@@ -11,13 +15,7 @@ interface Props { ...@@ -11,13 +15,7 @@ interface Props {
dataFrames: DataFrame[]; dataFrames: DataFrame[];
} }
interface State { export class TransformationsEditor extends React.PureComponent<Props> {
addingTransformation: boolean;
}
export class TransformationsEditor extends React.PureComponent<Props, State> {
state = { addingTransformation: false };
onTransformationAdd = (selectable: SelectableValue<string>) => { onTransformationAdd = (selectable: SelectableValue<string>) => {
const { transformations, onChange } = this.props; const { transformations, onChange } = this.props;
onChange([ onChange([
...@@ -27,7 +25,6 @@ export class TransformationsEditor extends React.PureComponent<Props, State> { ...@@ -27,7 +25,6 @@ export class TransformationsEditor extends React.PureComponent<Props, State> {
options: {}, options: {},
}, },
]); ]);
this.setState({ addingTransformation: false });
}; };
onTransformationChange = (idx: number, config: DataTransformerConfig) => { onTransformationChange = (idx: number, config: DataTransformerConfig) => {
...@@ -45,32 +42,23 @@ export class TransformationsEditor extends React.PureComponent<Props, State> { ...@@ -45,32 +42,23 @@ export class TransformationsEditor extends React.PureComponent<Props, State> {
}; };
renderTransformationSelector = () => { renderTransformationSelector = () => {
if (!this.state.addingTransformation) { const availableTransformers = standardTransformersRegistry.list().map(t => {
return null;
}
const availableTransformers = transformersUIRegistry.list().map(t => {
return { return {
value: t.transformer.id, value: t.transformation.id,
label: t.transformer.name, label: t.name,
description: t.description,
}; };
}); });
return ( return (
<div <ValuePicker
className={css` size="md"
margin-bottom: 10px; variant="secondary"
max-width: 300px; label="Add transformation"
`} options={availableTransformers}
> onChange={this.onTransformationAdd}
<Select isFullWidth={false}
options={availableTransformers} />
placeholder="Select transformation"
onChange={this.onTransformationAdd}
autoFocus={true}
openMenuOnFocus={true}
/>
</div>
); );
}; };
...@@ -83,7 +71,7 @@ export class TransformationsEditor extends React.PureComponent<Props, State> { ...@@ -83,7 +71,7 @@ export class TransformationsEditor extends React.PureComponent<Props, State> {
{transformations.map((t, i) => { {transformations.map((t, i) => {
let editor; let editor;
const transformationUI = transformersUIRegistry.getIfExists(t.id); const transformationUI = standardTransformersRegistry.getIfExists(t.id);
if (!transformationUI) { if (!transformationUI) {
return null; return null;
} }
...@@ -92,8 +80,8 @@ export class TransformationsEditor extends React.PureComponent<Props, State> { ...@@ -92,8 +80,8 @@ export class TransformationsEditor extends React.PureComponent<Props, State> {
const output = transformDataFrame(transformations.slice(i), input); const output = transformDataFrame(transformations.slice(i), input);
if (transformationUI) { if (transformationUI) {
editor = React.createElement(transformationUI.component, { editor = React.createElement(transformationUI.editor, {
options: { ...transformationUI.transformer.defaultOptions, ...t.options }, options: { ...transformationUI.transformation.defaultOptions, ...t.options },
input, input,
onChange: (options: any) => { onChange: (options: any) => {
this.onTransformationChange(i, { this.onTransformationChange(i, {
...@@ -130,9 +118,6 @@ export class TransformationsEditor extends React.PureComponent<Props, State> { ...@@ -130,9 +118,6 @@ export class TransformationsEditor extends React.PureComponent<Props, State> {
</p> </p>
{this.renderTransformationEditors()} {this.renderTransformationEditors()}
{this.renderTransformationSelector()} {this.renderTransformationSelector()}
<Button variant="secondary" icon="plus" onClick={() => this.setState({ addingTransformation: true })}>
Add transformation
</Button>
</Container> </Container>
</CustomScrollbar> </CustomScrollbar>
); );
......
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