mirror of https://github.com/grafana/grafana
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 importpull/78006/head
parent
225614a513
commit
721988e931
@ -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, |
|
||||||
}; |
|
@ -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> |
||||||
|
</> |
||||||
|
); |
||||||
|
}; |
@ -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, |
||||||
|
}; |
@ -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> |
||||||
|
</> |
||||||
|
); |
||||||
|
}; |
@ -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> |
||||||
|
</> |
||||||
|
); |
||||||
|
}; |
@ -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> |
||||||
|
</> |
||||||
|
); |
||||||
|
}; |
@ -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> |
||||||
|
</> |
||||||
|
); |
||||||
|
}; |
@ -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> |
||||||
|
</> |
||||||
|
); |
||||||
|
}; |
@ -0,0 +1 @@ |
|||||||
|
export const LABEL_WIDTH = 16; |
@ -0,0 +1,6 @@ |
|||||||
|
import { |
||||||
|
CalculateFieldTransformerEditor, |
||||||
|
calculateFieldTransformRegistryItem, |
||||||
|
} from './CalculateFieldTransformerEditor'; |
||||||
|
|
||||||
|
export { CalculateFieldTransformerEditor, calculateFieldTransformRegistryItem }; |
Loading…
Reference in new issue