Transformations: break add field from calculation transformation UI into sub components (#77874)

* break out subcomponents

* prettify

* break out editors into separate files

* update betterer since this is only moving around lint issues

* Fix import
This commit is contained in:
Oscar Kilhed 2023-11-10 16:12:34 +01:00 committed by GitHub
parent 225614a513
commit 721988e931
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 746 additions and 681 deletions

View File

@ -5083,12 +5083,16 @@ exports[`better eslint`] = {
"public/app/features/transformers/configFromQuery/ConfigFromQueryTransformerEditor.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"]
],
"public/app/features/transformers/editors/CalculateFieldTransformerEditor.tsx:5381": [
"public/app/features/transformers/editors/CalculateFieldTransformerEditor/CumulativeOptionsEditor.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/features/transformers/editors/CalculateFieldTransformerEditor/ReduceRowOptionsEditor.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/features/transformers/editors/CalculateFieldTransformerEditor/WindowOptionsEditor.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"],
[0, 0, 0, "Do not use any type assertions.", "3"],
[0, 0, 0, "Do not use any type assertions.", "4"]
[0, 0, 0, "Do not use any type assertions.", "2"]
],
"public/app/features/transformers/editors/ConvertFieldTypeTransformerEditor.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]

View File

@ -1,677 +0,0 @@
import { defaults } from 'lodash';
import React, { ChangeEvent, useEffect, useState } from 'react';
import { identity, of, OperatorFunction } from 'rxjs';
import { map } from 'rxjs/operators';
import {
BinaryOperationID,
binaryOperators,
unaryOperators,
DataFrame,
DataTransformerID,
FieldType,
getFieldDisplayName,
KeyValue,
ReducerID,
SelectableValue,
standardTransformers,
TransformerRegistryItem,
TransformerUIProps,
TransformerCategory,
UnaryOperationID,
} from '@grafana/data';
import {
BinaryOptions,
UnaryOptions,
CalculateFieldMode,
WindowAlignment,
CalculateFieldTransformerOptions,
getNameFromOptions,
IndexOptions,
ReduceOptions,
CumulativeOptions,
WindowOptions,
WindowSizeMode,
defaultWindowOptions,
} from '@grafana/data/src/transformations/transformers/calculateField';
import { getTemplateSrv, config as cfg } from '@grafana/runtime';
import {
FilterPill,
HorizontalGroup,
InlineField,
InlineFieldRow,
InlineLabel,
InlineSwitch,
Input,
RadioButtonGroup,
Select,
StatsPicker,
} from '@grafana/ui';
import { NumberInput } from 'app/core/components/OptionsUI/NumberInput';
import { getTransformationContent } from '../docs/getTransformationContent';
interface CalculateFieldTransformerEditorProps extends TransformerUIProps<CalculateFieldTransformerOptions> {}
interface CalculateFieldTransformerEditorState {
names: string[];
selected: string[];
}
const calculationModes = [
{ value: CalculateFieldMode.BinaryOperation, label: 'Binary operation' },
{ value: CalculateFieldMode.UnaryOperation, label: 'Unary operation' },
{ value: CalculateFieldMode.ReduceRow, label: 'Reduce row' },
{ value: CalculateFieldMode.Index, label: 'Row index' },
];
if (cfg.featureToggles.addFieldFromCalculationStatFunctions) {
calculationModes.push(
{ value: CalculateFieldMode.CumulativeFunctions, label: 'Cumulative functions' },
{ value: CalculateFieldMode.WindowFunctions, label: 'Window functions' }
);
}
const okTypes = new Set<FieldType>([FieldType.time, FieldType.number, FieldType.string]);
const labelWidth = 16;
export const CalculateFieldTransformerEditor = (props: CalculateFieldTransformerEditorProps) => {
const { options, onChange, input } = props;
const configuredOptions = options?.reduce?.include;
const [state, setState] = useState<CalculateFieldTransformerEditorState>({ names: [], selected: [] });
useEffect(() => {
const ctx = { interpolate: (v: string) => v };
const subscription = of(input)
.pipe(
standardTransformers.ensureColumnsTransformer.operator(null, ctx),
extractAllNames(),
getVariableNames(),
extractNamesAndSelected(configuredOptions || [])
)
.subscribe(({ selected, names }) => {
setState({ names, selected });
});
return () => {
subscription.unsubscribe();
};
}, [input, configuredOptions]);
const getVariableNames = (): OperatorFunction<string[], string[]> => {
if (!cfg.featureToggles.transformationsVariableSupport) {
return identity;
}
const templateSrv = getTemplateSrv();
return (source) =>
source.pipe(
map((input) => {
input.push(...templateSrv.getVariables().map((v) => '$' + v.name));
return input;
})
);
};
const extractAllNames = (): OperatorFunction<DataFrame[], string[]> => {
return (source) =>
source.pipe(
map((input) => {
const allNames: string[] = [];
const byName: KeyValue<boolean> = {};
for (const frame of input) {
for (const field of frame.fields) {
if (!okTypes.has(field.type)) {
continue;
}
const displayName = getFieldDisplayName(field, frame, input);
if (!byName[displayName]) {
byName[displayName] = true;
allNames.push(displayName);
}
}
}
return allNames;
})
);
};
const extractNamesAndSelected = (
configuredOptions: string[]
): OperatorFunction<string[], { names: string[]; selected: string[] }> => {
return (source) =>
source.pipe(
map((allNames) => {
if (!configuredOptions.length) {
return { names: allNames, selected: [] };
}
const names: string[] = [];
const selected: string[] = [];
for (const v of allNames) {
if (configuredOptions.includes(v)) {
selected.push(v);
}
names.push(v);
}
return { names, selected };
})
);
};
const onToggleReplaceFields = (e: React.FormEvent<HTMLInputElement>) => {
onChange({
...options,
replaceFields: e.currentTarget.checked,
});
};
const onModeChanged = (value: SelectableValue<CalculateFieldMode>) => {
const mode = value.value ?? CalculateFieldMode.BinaryOperation;
if (mode === CalculateFieldMode.WindowFunctions) {
options.window = options.window ?? defaultWindowOptions;
}
onChange({
...options,
mode,
});
};
const onAliasChanged = (evt: ChangeEvent<HTMLInputElement>) => {
onChange({
...options,
alias: evt.target.value,
});
};
//---------------------------------------------------------
// Row index
//---------------------------------------------------------
const onToggleRowIndexAsPercentile = (e: React.FormEvent<HTMLInputElement>) => {
onChange({
...options,
index: {
asPercentile: e.currentTarget.checked,
},
});
};
const renderRowIndex = (options?: IndexOptions) => {
return (
<>
<InlineField labelWidth={labelWidth} label="As percentile" tooltip="Transform the row index as a percentile.">
<InlineSwitch value={!!options?.asPercentile} onChange={onToggleRowIndexAsPercentile} />
</InlineField>
</>
);
};
//---------------------------------------------------------
// Window functions
//---------------------------------------------------------
const updateWindowOptions = (v: WindowOptions) => {
const { options, onChange } = props;
onChange({
...options,
mode: CalculateFieldMode.WindowFunctions,
window: v,
});
};
const onWindowFieldChange = (v: SelectableValue<string>) => {
const { window } = options;
updateWindowOptions({
...window!,
field: v.value!,
});
};
const onWindowSizeChange = (v?: number) => {
const { window } = options;
updateWindowOptions({
...window!,
windowSize: v && window?.windowSizeMode === WindowSizeMode.Percentage ? v / 100 : v,
});
};
const onWindowSizeModeChange = (val: string) => {
const { window } = options;
const mode = val as WindowSizeMode;
updateWindowOptions({
...window!,
windowSize: window?.windowSize
? mode === WindowSizeMode.Percentage
? window!.windowSize! / 100
: window!.windowSize! * 100
: undefined,
windowSizeMode: mode,
});
};
const onWindowStatsChange = (stats: string[]) => {
const reducer = stats.length ? (stats[0] as ReducerID) : ReducerID.sum;
const { window } = options;
updateWindowOptions({ ...window, reducer });
};
const onTypeChange = (val: string) => {
const { window } = options;
updateWindowOptions({
...window!,
windowAlignment: val as WindowAlignment,
});
};
const renderWindowFunctions = (options?: WindowOptions) => {
const { names } = state;
options = defaults(options, { reducer: ReducerID.sum });
const selectOptions = names.map((v) => ({ label: v, value: v }));
const typeOptions = [
{ label: 'Trailing', value: WindowAlignment.Trailing },
{ label: 'Centered', value: WindowAlignment.Centered },
];
const windowSizeModeOptions = [
{ label: 'Percentage', value: WindowSizeMode.Percentage },
{ label: 'Fixed', value: WindowSizeMode.Fixed },
];
return (
<>
<InlineField label="Field" labelWidth={labelWidth}>
<Select
placeholder="Field"
options={selectOptions}
className="min-width-18"
value={options?.field}
onChange={onWindowFieldChange}
/>
</InlineField>
<InlineField label="Calculation" labelWidth={labelWidth}>
<StatsPicker
allowMultiple={false}
className="width-18"
stats={[options.reducer]}
onChange={onWindowStatsChange}
defaultStat={ReducerID.mean}
filterOptions={(ext) =>
ext.id === ReducerID.mean || ext.id === ReducerID.variance || ext.id === ReducerID.stdDev
}
/>
</InlineField>
<InlineField label="Type" labelWidth={labelWidth}>
<RadioButtonGroup
value={options.windowAlignment ?? WindowAlignment.Trailing}
options={typeOptions}
onChange={onTypeChange}
/>
</InlineField>
<InlineField label="Window size mode">
<RadioButtonGroup
value={options.windowSizeMode ?? WindowSizeMode.Percentage}
options={windowSizeModeOptions}
onChange={onWindowSizeModeChange}
></RadioButtonGroup>
</InlineField>
<InlineField
label={options.windowSizeMode === WindowSizeMode.Percentage ? 'Window size %' : 'Window size'}
labelWidth={labelWidth}
tooltip={
options.windowSizeMode === WindowSizeMode.Percentage
? 'Set the window size as a percentage of the total data'
: 'Window size'
}
>
<NumberInput
placeholder="Auto"
min={0.1}
value={
options.windowSize && options.windowSizeMode === WindowSizeMode.Percentage
? options.windowSize * 100
: options.windowSize
}
onChange={onWindowSizeChange}
></NumberInput>
</InlineField>
</>
);
};
//---------------------------------------------------------
// Reduce by Row
//---------------------------------------------------------
const updateReduceOptions = (v: ReduceOptions) => {
const { onChange } = props;
onChange({
...options,
reduce: v,
});
};
const onFieldToggle = (fieldName: string) => {
const { selected } = state;
if (selected.indexOf(fieldName) > -1) {
onReduceFieldsChanged(selected.filter((s) => s !== fieldName));
} else {
onReduceFieldsChanged([...selected, fieldName]);
}
};
const onReduceFieldsChanged = (selected: string[]) => {
setState({ ...state, ...{ selected } });
const { reduce } = options;
updateReduceOptions({
...reduce!,
include: selected,
});
};
const onStatsChange = (stats: string[]) => {
const reducer = stats.length ? (stats[0] as ReducerID) : ReducerID.sum;
const { reduce } = options;
updateReduceOptions({ ...reduce, reducer });
};
const renderReduceRow = (options?: ReduceOptions) => {
const { names, selected } = state;
options = defaults(options, { reducer: ReducerID.sum });
return (
<>
<InlineField label="Operation" labelWidth={labelWidth} grow={true}>
<HorizontalGroup spacing="xs" align="flex-start" wrap>
{names.map((o, i) => {
return (
<FilterPill
key={`${o}/${i}`}
onClick={() => {
onFieldToggle(o);
}}
label={o}
selected={selected.indexOf(o) > -1}
/>
);
})}
</HorizontalGroup>
</InlineField>
<InlineField label="Calculation" labelWidth={labelWidth}>
<StatsPicker
allowMultiple={false}
className="width-18"
stats={[options.reducer]}
onChange={onStatsChange}
defaultStat={ReducerID.sum}
/>
</InlineField>
</>
);
};
//---------------------------------------------------------
// Cumulative Operator
//---------------------------------------------------------
const onCumulativeStatsChange = (stats: string[]) => {
const reducer = stats.length ? (stats[0] as ReducerID) : ReducerID.sum;
const { cumulative } = options;
updateCumulativeOptions({ ...cumulative, reducer });
};
const updateCumulativeOptions = (v: CumulativeOptions) => {
onChange({
...options,
mode: CalculateFieldMode.CumulativeFunctions,
cumulative: v,
});
};
const onCumulativeFieldChange = (v: SelectableValue<string>) => {
const { cumulative } = options;
updateCumulativeOptions({
...cumulative!,
field: v.value!,
});
};
const renderCumulativeFunctions = (options?: CumulativeOptions) => {
const { names } = state;
options = defaults(options, { reducer: ReducerID.sum });
const selectOptions = names.map((v) => ({ label: v, value: v }));
return (
<>
<InlineField label="Field" labelWidth={labelWidth}>
<Select
placeholder="Field"
options={selectOptions}
className="min-width-18"
value={options?.field}
onChange={onCumulativeFieldChange}
/>
</InlineField>
<InlineField label="Calculation" labelWidth={labelWidth}>
<StatsPicker
allowMultiple={false}
className="width-18"
stats={[options.reducer]}
onChange={onCumulativeStatsChange}
defaultStat={ReducerID.sum}
filterOptions={(ext) => ext.id === ReducerID.sum || ext.id === ReducerID.mean}
/>
</InlineField>
</>
);
};
//---------------------------------------------------------
// Binary Operator
//---------------------------------------------------------
const updateBinaryOptions = (v: BinaryOptions) => {
onChange({
...options,
mode: CalculateFieldMode.BinaryOperation,
binary: v,
});
};
const onBinaryLeftChanged = (v: SelectableValue<string>) => {
const { binary } = options;
updateBinaryOptions({
...binary!,
left: v.value!,
});
};
const onBinaryRightChanged = (v: SelectableValue<string>) => {
const { binary } = options;
updateBinaryOptions({
...binary!,
right: v.value!,
});
};
const onBinaryOperationChanged = (v: SelectableValue<BinaryOperationID>) => {
const { binary } = options;
updateBinaryOptions({
...binary!,
operator: v.value!,
});
};
const renderBinaryOperation = (options?: BinaryOptions) => {
options = defaults(options, { operator: BinaryOperationID.Add });
let foundLeft = !options?.left;
let foundRight = !options?.right;
const names = state.names.map((v) => {
if (v === options?.left) {
foundLeft = true;
}
if (v === options?.right) {
foundRight = true;
}
return { label: v, value: v };
});
const leftNames = foundLeft ? names : [...names, { label: options?.left, value: options?.left }];
const rightNames = foundRight ? names : [...names, { label: options?.right, value: options?.right }];
const ops = binaryOperators.list().map((v) => {
return { label: v.binaryOperationID, value: v.binaryOperationID };
});
return (
<>
<InlineFieldRow>
<InlineField label="Operation" labelWidth={labelWidth}>
<Select
allowCustomValue={true}
placeholder="Field or number"
options={leftNames}
className="min-width-18"
value={options?.left}
onChange={onBinaryLeftChanged}
/>
</InlineField>
<InlineField>
<Select
className="width-4"
options={ops}
value={options.operator ?? ops[0].value}
onChange={onBinaryOperationChanged}
/>
</InlineField>
<InlineField>
<Select
allowCustomValue={true}
placeholder="Field or number"
className="min-width-10"
options={rightNames}
value={options?.right}
onChange={onBinaryRightChanged}
/>
</InlineField>
</InlineFieldRow>
</>
);
};
//---------------------------------------------------------
// Unary Operator
//---------------------------------------------------------
const updateUnaryOptions = (v: UnaryOptions) => {
onChange({
...options,
mode: CalculateFieldMode.UnaryOperation,
unary: v,
});
};
const onUnaryOperationChanged = (v: SelectableValue<UnaryOperationID>) => {
const { unary } = options;
updateUnaryOptions({
...unary!,
operator: v.value!,
});
};
const onUnaryValueChanged = (v: SelectableValue<string>) => {
const { unary } = options;
updateUnaryOptions({
...unary!,
fieldName: v.value!,
});
};
const renderUnaryOperation = (options?: UnaryOptions) => {
options = defaults(options, { operator: UnaryOperationID.Abs });
let found = !options?.fieldName;
const names = state.names.map((v) => {
if (v === options?.fieldName) {
found = true;
}
return { label: v, value: v };
});
const ops = unaryOperators.list().map((v) => {
return { label: v.unaryOperationID, value: v.unaryOperationID };
});
const fieldName = found ? names : [...names, { label: options?.fieldName, value: options?.fieldName }];
return (
<>
<InlineFieldRow>
<InlineField label="Operation" labelWidth={labelWidth}>
<Select options={ops} value={options.operator ?? ops[0].value} onChange={onUnaryOperationChanged} />
</InlineField>
<InlineField label="(" labelWidth={2}>
<Select
placeholder="Field"
className="min-width-11"
options={fieldName}
value={options?.fieldName}
onChange={onUnaryValueChanged}
/>
</InlineField>
<InlineLabel width={2}>)</InlineLabel>
</InlineFieldRow>
</>
);
};
const mode = options.mode ?? CalculateFieldMode.BinaryOperation;
return (
<>
<InlineField labelWidth={labelWidth} label="Mode">
<Select
className="width-18"
options={calculationModes}
value={calculationModes.find((v) => v.value === mode)}
onChange={onModeChanged}
/>
</InlineField>
{mode === CalculateFieldMode.BinaryOperation && renderBinaryOperation(options.binary)}
{mode === CalculateFieldMode.UnaryOperation && renderUnaryOperation(options.unary)}
{mode === CalculateFieldMode.ReduceRow && renderReduceRow(options.reduce)}
{mode === CalculateFieldMode.CumulativeFunctions && renderCumulativeFunctions(options.cumulative)}
{mode === CalculateFieldMode.WindowFunctions && renderWindowFunctions(options.window)}
{mode === CalculateFieldMode.Index && renderRowIndex(options.index)}
<InlineField labelWidth={labelWidth} label="Alias">
<Input
className="width-18"
value={options.alias ?? ''}
placeholder={getNameFromOptions(options)}
onChange={onAliasChanged}
/>
</InlineField>
<InlineField labelWidth={labelWidth} label="Replace all fields">
<InlineSwitch value={!!options.replaceFields} onChange={onToggleReplaceFields} />
</InlineField>
</>
);
};
export const calculateFieldTransformRegistryItem: TransformerRegistryItem<CalculateFieldTransformerOptions> = {
id: DataTransformerID.calculateField,
editor: CalculateFieldTransformerEditor,
transformation: standardTransformers.calculateFieldTransformer,
name: standardTransformers.calculateFieldTransformer.name,
description: 'Use the row values to calculate a new field.',
categories: new Set([TransformerCategory.CalculateNewFields]),
help: getTransformationContent(DataTransformerID.calculateField).helperDocs,
};

View File

@ -0,0 +1,102 @@
import React from 'react';
import { BinaryOperationID, binaryOperators, SelectableValue } from '@grafana/data';
import {
BinaryOptions,
CalculateFieldMode,
CalculateFieldTransformerOptions,
} from '@grafana/data/src/transformations/transformers/calculateField';
import { InlineField, InlineFieldRow, Select } from '@grafana/ui';
import { LABEL_WIDTH } from './constants';
export const BinaryOperationOptionsEditor = (props: {
options: CalculateFieldTransformerOptions;
onChange: (options: CalculateFieldTransformerOptions) => void;
names: string[];
}) => {
const { options, onChange } = props;
const { binary } = options;
let foundLeft = !binary?.left;
let foundRight = !binary?.right;
const names = props.names.map((v) => {
if (v === binary?.left) {
foundLeft = true;
}
if (v === binary?.right) {
foundRight = true;
}
return { label: v, value: v };
});
const leftNames = foundLeft ? names : [...names, { label: binary?.left, value: binary?.left }];
const rightNames = foundRight ? names : [...names, { label: binary?.right, value: binary?.right }];
const ops = binaryOperators.list().map((v) => {
return { label: v.binaryOperationID, value: v.binaryOperationID };
});
const updateBinaryOptions = (v: BinaryOptions) => {
onChange({
...options,
mode: CalculateFieldMode.BinaryOperation,
binary: v,
});
};
const onBinaryLeftChanged = (v: SelectableValue<string>) => {
updateBinaryOptions({
...binary!,
left: v.value!,
});
};
const onBinaryRightChanged = (v: SelectableValue<string>) => {
updateBinaryOptions({
...binary!,
right: v.value!,
});
};
const onBinaryOperationChanged = (v: SelectableValue<BinaryOperationID>) => {
updateBinaryOptions({
...binary!,
operator: v.value!,
});
};
return (
<>
<InlineFieldRow>
<InlineField label="Operation" labelWidth={LABEL_WIDTH}>
<Select
allowCustomValue={true}
placeholder="Field or number"
options={leftNames}
className="min-width-18"
value={binary?.left}
onChange={onBinaryLeftChanged}
/>
</InlineField>
<InlineField>
<Select
className="width-4"
options={ops}
value={binary?.operator ?? ops[0].value}
onChange={onBinaryOperationChanged}
/>
</InlineField>
<InlineField>
<Select
allowCustomValue={true}
placeholder="Field or number"
className="min-width-10"
options={rightNames}
value={binary?.right}
onChange={onBinaryRightChanged}
/>
</InlineField>
</InlineFieldRow>
</>
);
};

View File

@ -0,0 +1,238 @@
import React, { ChangeEvent, useEffect, useState } from 'react';
import { identity, of, OperatorFunction } from 'rxjs';
import { map } from 'rxjs/operators';
import {
DataFrame,
DataTransformerID,
FieldType,
getFieldDisplayName,
KeyValue,
SelectableValue,
standardTransformers,
TransformerRegistryItem,
TransformerUIProps,
TransformerCategory,
} from '@grafana/data';
import {
CalculateFieldMode,
CalculateFieldTransformerOptions,
getNameFromOptions,
defaultWindowOptions,
} from '@grafana/data/src/transformations/transformers/calculateField';
import { getTemplateSrv, config as cfg } from '@grafana/runtime';
import { InlineField, InlineSwitch, Input, Select } from '@grafana/ui';
import { getTransformationContent } from '../../docs/getTransformationContent';
import { BinaryOperationOptionsEditor } from './BinaryOperationOptionsEditor';
import { CumulativeOptionsEditor } from './CumulativeOptionsEditor';
import { IndexOptionsEditor } from './IndexOptionsEditor';
import { ReduceRowOptionsEditor } from './ReduceRowOptionsEditor';
import { UnaryOperationEditor } from './UnaryOperationEditor';
import { WindowOptionsEditor } from './WindowOptionsEditor';
import { LABEL_WIDTH } from './constants';
interface CalculateFieldTransformerEditorProps extends TransformerUIProps<CalculateFieldTransformerOptions> {}
interface CalculateFieldTransformerEditorState {
names: string[];
selected: string[];
}
const calculationModes = [
{ value: CalculateFieldMode.BinaryOperation, label: 'Binary operation' },
{ value: CalculateFieldMode.UnaryOperation, label: 'Unary operation' },
{ value: CalculateFieldMode.ReduceRow, label: 'Reduce row' },
{ value: CalculateFieldMode.Index, label: 'Row index' },
];
if (cfg.featureToggles.addFieldFromCalculationStatFunctions) {
calculationModes.push(
{ value: CalculateFieldMode.CumulativeFunctions, label: 'Cumulative functions' },
{ value: CalculateFieldMode.WindowFunctions, label: 'Window functions' }
);
}
const okTypes = new Set<FieldType>([FieldType.time, FieldType.number, FieldType.string]);
export const CalculateFieldTransformerEditor = (props: CalculateFieldTransformerEditorProps) => {
const { options, onChange, input } = props;
const configuredOptions = options?.reduce?.include;
const [state, setState] = useState<CalculateFieldTransformerEditorState>({ names: [], selected: [] });
useEffect(() => {
const ctx = { interpolate: (v: string) => v };
const subscription = of(input)
.pipe(
standardTransformers.ensureColumnsTransformer.operator(null, ctx),
extractAllNames(),
getVariableNames(),
extractNamesAndSelected(configuredOptions || [])
)
.subscribe(({ selected, names }) => {
setState({ names, selected });
});
return () => {
subscription.unsubscribe();
};
}, [input, configuredOptions]);
const getVariableNames = (): OperatorFunction<string[], string[]> => {
if (!cfg.featureToggles.transformationsVariableSupport) {
return identity;
}
const templateSrv = getTemplateSrv();
return (source) =>
source.pipe(
map((input) => {
input.push(...templateSrv.getVariables().map((v) => '$' + v.name));
return input;
})
);
};
const extractAllNames = (): OperatorFunction<DataFrame[], string[]> => {
return (source) =>
source.pipe(
map((input) => {
const allNames: string[] = [];
const byName: KeyValue<boolean> = {};
for (const frame of input) {
for (const field of frame.fields) {
if (!okTypes.has(field.type)) {
continue;
}
const displayName = getFieldDisplayName(field, frame, input);
if (!byName[displayName]) {
byName[displayName] = true;
allNames.push(displayName);
}
}
}
return allNames;
})
);
};
const extractNamesAndSelected = (
configuredOptions: string[]
): OperatorFunction<string[], { names: string[]; selected: string[] }> => {
return (source) =>
source.pipe(
map((allNames) => {
if (!configuredOptions.length) {
return { names: allNames, selected: [] };
}
const names: string[] = [];
const selected: string[] = [];
for (const v of allNames) {
if (configuredOptions.includes(v)) {
selected.push(v);
}
names.push(v);
}
return { names, selected };
})
);
};
const onToggleReplaceFields = (e: React.FormEvent<HTMLInputElement>) => {
onChange({
...options,
replaceFields: e.currentTarget.checked,
});
};
const onModeChanged = (value: SelectableValue<CalculateFieldMode>) => {
const mode = value.value ?? CalculateFieldMode.BinaryOperation;
if (mode === CalculateFieldMode.WindowFunctions) {
options.window = options.window ?? defaultWindowOptions;
}
onChange({
...options,
mode,
});
};
const onAliasChanged = (evt: ChangeEvent<HTMLInputElement>) => {
onChange({
...options,
alias: evt.target.value,
});
};
const mode = options.mode ?? CalculateFieldMode.BinaryOperation;
return (
<>
<InlineField labelWidth={LABEL_WIDTH} label="Mode">
<Select
className="width-18"
options={calculationModes}
value={calculationModes.find((v) => v.value === mode)}
onChange={onModeChanged}
/>
</InlineField>
{mode === CalculateFieldMode.BinaryOperation && (
<BinaryOperationOptionsEditor
options={options}
names={state.names}
onChange={props.onChange}
></BinaryOperationOptionsEditor>
)}
{mode === CalculateFieldMode.UnaryOperation && (
<UnaryOperationEditor names={state.names} options={options} onChange={props.onChange}></UnaryOperationEditor>
)}
{mode === CalculateFieldMode.ReduceRow && (
<ReduceRowOptionsEditor
names={state.names}
selected={state.selected}
options={options}
onChange={props.onChange}
></ReduceRowOptionsEditor>
)}
{mode === CalculateFieldMode.CumulativeFunctions && (
<CumulativeOptionsEditor
names={state.names}
options={options}
onChange={props.onChange}
></CumulativeOptionsEditor>
)}
{mode === CalculateFieldMode.WindowFunctions && (
<WindowOptionsEditor names={state.names} options={options} onChange={props.onChange}></WindowOptionsEditor>
)}
{mode === CalculateFieldMode.Index && (
<IndexOptionsEditor options={options} onChange={props.onChange}></IndexOptionsEditor>
)}
<InlineField labelWidth={LABEL_WIDTH} label="Alias">
<Input
className="width-18"
value={options.alias ?? ''}
placeholder={getNameFromOptions(options)}
onChange={onAliasChanged}
/>
</InlineField>
<InlineField labelWidth={LABEL_WIDTH} label="Replace all fields">
<InlineSwitch value={!!options.replaceFields} onChange={onToggleReplaceFields} />
</InlineField>
</>
);
};
export const calculateFieldTransformRegistryItem: TransformerRegistryItem<CalculateFieldTransformerOptions> = {
id: DataTransformerID.calculateField,
editor: CalculateFieldTransformerEditor,
transformation: standardTransformers.calculateFieldTransformer,
name: standardTransformers.calculateFieldTransformer.name,
description: 'Use the row values to calculate a new field.',
categories: new Set([TransformerCategory.CalculateNewFields]),
help: getTransformationContent(DataTransformerID.calculateField).helperDocs,
};

View File

@ -0,0 +1,66 @@
import React from 'react';
import { ReducerID, SelectableValue } from '@grafana/data';
import {
CalculateFieldMode,
CalculateFieldTransformerOptions,
CumulativeOptions,
} from '@grafana/data/src/transformations/transformers/calculateField';
import { InlineField, Select, StatsPicker } from '@grafana/ui';
import { LABEL_WIDTH } from './constants';
export const CumulativeOptionsEditor = (props: {
options: CalculateFieldTransformerOptions;
names: string[];
onChange: (options: CalculateFieldTransformerOptions) => void;
}) => {
const { names, onChange, options } = props;
const { cumulative } = options;
const selectOptions = names.map((v) => ({ label: v, value: v }));
const onCumulativeStatsChange = (stats: string[]) => {
const reducer = stats.length ? (stats[0] as ReducerID) : ReducerID.sum;
updateCumulativeOptions({ ...cumulative, reducer });
};
const updateCumulativeOptions = (v: CumulativeOptions) => {
onChange({
...options,
mode: CalculateFieldMode.CumulativeFunctions,
cumulative: v,
});
};
const onCumulativeFieldChange = (v: SelectableValue<string>) => {
updateCumulativeOptions({
...cumulative!,
field: v.value!,
});
};
return (
<>
<InlineField label="Field" labelWidth={LABEL_WIDTH}>
<Select
placeholder="Field"
options={selectOptions}
className="min-width-18"
value={cumulative?.field}
onChange={onCumulativeFieldChange}
/>
</InlineField>
<InlineField label="Calculation" labelWidth={LABEL_WIDTH}>
<StatsPicker
allowMultiple={false}
className="width-18"
stats={[cumulative?.reducer || ReducerID.sum]}
onChange={onCumulativeStatsChange}
defaultStat={ReducerID.sum}
filterOptions={(ext) => ext.id === ReducerID.sum || ext.id === ReducerID.mean}
/>
</InlineField>
</>
);
};

View File

@ -0,0 +1,30 @@
import React from 'react';
import { CalculateFieldTransformerOptions } from '@grafana/data/src/transformations/transformers/calculateField';
import { InlineField, InlineSwitch } from '@grafana/ui';
import { LABEL_WIDTH } from './constants';
export const IndexOptionsEditor = (props: {
options: CalculateFieldTransformerOptions;
onChange: (options: CalculateFieldTransformerOptions) => void;
}) => {
const { options, onChange } = props;
const { index } = options;
const onToggleRowIndexAsPercentile = (e: React.FormEvent<HTMLInputElement>) => {
onChange({
...options,
index: {
asPercentile: e.currentTarget.checked,
},
});
};
return (
<>
<InlineField labelWidth={LABEL_WIDTH} label="As percentile" tooltip="Transform the row index as a percentile.">
<InlineSwitch value={!!index?.asPercentile} onChange={onToggleRowIndexAsPercentile} />
</InlineField>
</>
);
};

View File

@ -0,0 +1,79 @@
import React from 'react';
import { ReducerID } from '@grafana/data';
import {
CalculateFieldTransformerOptions,
ReduceOptions,
} from '@grafana/data/src/transformations/transformers/calculateField';
import { FilterPill, HorizontalGroup, InlineField, StatsPicker } from '@grafana/ui';
import { LABEL_WIDTH } from './constants';
export const ReduceRowOptionsEditor = (props: {
options: CalculateFieldTransformerOptions;
names: string[];
selected: string[];
onChange: (options: CalculateFieldTransformerOptions) => void;
}) => {
const { names, selected, onChange, options } = props;
const { reduce } = options;
const updateReduceOptions = (v: ReduceOptions) => {
onChange({
...options,
reduce: v,
});
};
const onFieldToggle = (fieldName: string) => {
if (selected.indexOf(fieldName) > -1) {
onReduceFieldsChanged(selected.filter((s) => s !== fieldName));
} else {
onReduceFieldsChanged([...selected, fieldName]);
}
};
const onReduceFieldsChanged = (selected: string[]) => {
updateReduceOptions({
...reduce!,
include: selected,
});
};
const onStatsChange = (stats: string[]) => {
const reducer = stats.length ? (stats[0] as ReducerID) : ReducerID.sum;
const { reduce } = options;
updateReduceOptions({ ...reduce, reducer });
};
return (
<>
<InlineField label="Operation" labelWidth={LABEL_WIDTH} grow={true}>
<HorizontalGroup spacing="xs" align="flex-start" wrap>
{names.map((o, i) => {
return (
<FilterPill
key={`${o}/${i}`}
onClick={() => {
onFieldToggle(o);
}}
label={o}
selected={selected.indexOf(o) > -1}
/>
);
})}
</HorizontalGroup>
</InlineField>
<InlineField label="Calculation" labelWidth={LABEL_WIDTH}>
<StatsPicker
allowMultiple={false}
className="width-18"
stats={[reduce?.reducer || ReducerID.sum]}
onChange={onStatsChange}
defaultStat={ReducerID.sum}
/>
</InlineField>
</>
);
};

View File

@ -0,0 +1,76 @@
import React from 'react';
import { unaryOperators, SelectableValue, UnaryOperationID } from '@grafana/data';
import {
UnaryOptions,
CalculateFieldMode,
CalculateFieldTransformerOptions,
} from '@grafana/data/src/transformations/transformers/calculateField';
import { InlineField, InlineFieldRow, InlineLabel, Select } from '@grafana/ui';
import { LABEL_WIDTH } from './constants';
export const UnaryOperationEditor = (props: {
options: CalculateFieldTransformerOptions;
names: string[];
onChange: (options: CalculateFieldTransformerOptions) => void;
}) => {
const { options, onChange } = props;
const { unary } = options;
const updateUnaryOptions = (v: UnaryOptions) => {
onChange({
...options,
mode: CalculateFieldMode.UnaryOperation,
unary: v,
});
};
const onUnaryOperationChanged = (v: SelectableValue<UnaryOperationID>) => {
updateUnaryOptions({
...unary!,
operator: v.value!,
});
};
const onUnaryValueChanged = (v: SelectableValue<string>) => {
updateUnaryOptions({
...unary!,
fieldName: v.value!,
});
};
let found = !unary?.fieldName;
const names = props.names.map((v) => {
if (v === unary?.fieldName) {
found = true;
}
return { label: v, value: v };
});
const ops = unaryOperators.list().map((v) => {
return { label: v.unaryOperationID, value: v.unaryOperationID };
});
const fieldName = found ? names : [...names, { label: unary?.fieldName, value: unary?.fieldName }];
return (
<>
<InlineFieldRow>
<InlineField label="Operation" labelWidth={LABEL_WIDTH}>
<Select options={ops} value={unary?.operator ?? ops[0].value} onChange={onUnaryOperationChanged} />
</InlineField>
<InlineField label="(" labelWidth={2}>
<Select
placeholder="Field"
className="min-width-11"
options={fieldName}
value={unary?.fieldName}
onChange={onUnaryValueChanged}
/>
</InlineField>
<InlineLabel width={2}>)</InlineLabel>
</InlineFieldRow>
</>
);
};

View File

@ -0,0 +1,140 @@
import React from 'react';
import { ReducerID, SelectableValue } from '@grafana/data';
import {
CalculateFieldMode,
WindowAlignment,
CalculateFieldTransformerOptions,
WindowOptions,
WindowSizeMode,
} from '@grafana/data/src/transformations/transformers/calculateField';
import { InlineField, RadioButtonGroup, Select, StatsPicker } from '@grafana/ui';
import { NumberInput } from 'app/core/components/OptionsUI/NumberInput';
import { LABEL_WIDTH } from './constants';
export const WindowOptionsEditor = (props: {
options: CalculateFieldTransformerOptions;
names: string[];
onChange: (options: CalculateFieldTransformerOptions) => void;
}) => {
const { options, names, onChange } = props;
const { window } = options;
const selectOptions = names.map((v) => ({ label: v, value: v }));
const typeOptions = [
{ label: 'Trailing', value: WindowAlignment.Trailing },
{ label: 'Centered', value: WindowAlignment.Centered },
];
const windowSizeModeOptions = [
{ label: 'Percentage', value: WindowSizeMode.Percentage },
{ label: 'Fixed', value: WindowSizeMode.Fixed },
];
const updateWindowOptions = (v: WindowOptions) => {
onChange({
...options,
mode: CalculateFieldMode.WindowFunctions,
window: v,
});
};
const onWindowFieldChange = (v: SelectableValue<string>) => {
updateWindowOptions({
...window!,
field: v.value!,
});
};
const onWindowSizeChange = (v?: number) => {
updateWindowOptions({
...window!,
windowSize: v && window?.windowSizeMode === WindowSizeMode.Percentage ? v / 100 : v,
});
};
const onWindowSizeModeChange = (val: string) => {
const mode = val as WindowSizeMode;
updateWindowOptions({
...window!,
windowSize: window?.windowSize
? mode === WindowSizeMode.Percentage
? window!.windowSize! / 100
: window!.windowSize! * 100
: undefined,
windowSizeMode: mode,
});
};
const onWindowStatsChange = (stats: string[]) => {
const reducer = stats.length ? (stats[0] as ReducerID) : ReducerID.sum;
updateWindowOptions({ ...window, reducer });
};
const onTypeChange = (val: string) => {
updateWindowOptions({
...window!,
windowAlignment: val as WindowAlignment,
});
};
return (
<>
<InlineField label="Field" labelWidth={LABEL_WIDTH}>
<Select
placeholder="Field"
options={selectOptions}
className="min-width-18"
value={window?.field}
onChange={onWindowFieldChange}
/>
</InlineField>
<InlineField label="Calculation" labelWidth={LABEL_WIDTH}>
<StatsPicker
allowMultiple={false}
className="width-18"
stats={[window?.reducer || ReducerID.mean]}
onChange={onWindowStatsChange}
defaultStat={ReducerID.mean}
filterOptions={(ext) =>
ext.id === ReducerID.mean || ext.id === ReducerID.variance || ext.id === ReducerID.stdDev
}
/>
</InlineField>
<InlineField label="Type" labelWidth={LABEL_WIDTH}>
<RadioButtonGroup
value={window?.windowAlignment ?? WindowAlignment.Trailing}
options={typeOptions}
onChange={onTypeChange}
/>
</InlineField>
<InlineField label="Window size mode">
<RadioButtonGroup
value={window?.windowSizeMode ?? WindowSizeMode.Percentage}
options={windowSizeModeOptions}
onChange={onWindowSizeModeChange}
></RadioButtonGroup>
</InlineField>
<InlineField
label={window?.windowSizeMode === WindowSizeMode.Percentage ? 'Window size %' : 'Window size'}
labelWidth={LABEL_WIDTH}
tooltip={
window?.windowSizeMode === WindowSizeMode.Percentage
? 'Set the window size as a percentage of the total data'
: 'Window size'
}
>
<NumberInput
placeholder="Auto"
min={0.1}
value={
window?.windowSize && window.windowSizeMode === WindowSizeMode.Percentage
? window.windowSize * 100
: window?.windowSize
}
onChange={onWindowSizeChange}
></NumberInput>
</InlineField>
</>
);
};

View File

@ -0,0 +1 @@
export const LABEL_WIDTH = 16;

View File

@ -0,0 +1,6 @@
import {
CalculateFieldTransformerEditor,
calculateFieldTransformRegistryItem,
} from './CalculateFieldTransformerEditor';
export { CalculateFieldTransformerEditor, calculateFieldTransformRegistryItem };