mirror of https://github.com/grafana/grafana
Correlations: Add transformation editor (#66217)
* There was an attempt * Change disabled state based on transformation type * Add validation to transformation type * Revert "Add validation to transformation type" This reverts commit 2188a3d9a93aec5eeafcdd40510391ba1a53671a. * Add validation to transformation type * Move transformations editor to a separate file * Make name more descriptive * Ensure type dropdown has always the same width * Add tooltips around transformation options * Slight style changes * Remove autofocus on append, integrate read only to transformationeditor, save values that disappear so they come back * Remove yaml changes * Have variable background color work with alternating colors on different themes * Make expression required for regular expressions * Remove unused empty form object * Fix bug about transformation’s values saved in memory * Better validation formatting for expression * Add labels and (for now) non working test, attempt to fix saved transformation delete/add bug * Fix datalink comment * Remove fancy CSS due to background change * Fix deleting saved transformation bug, finish tests * Consolidate transformation types * Double check aria labels * Change aria labels, fix tests * Add a transformation with the create correlation test --------- Co-authored-by: Piotr Jamróz <pm.jamroz@gmail.com>pull/66760/head
parent
15c9ced944
commit
9d69d3173f
@ -0,0 +1,315 @@ |
|||||||
|
import { css } from '@emotion/css'; |
||||||
|
import { compact, fill } from 'lodash'; |
||||||
|
import React, { useState } from 'react'; |
||||||
|
import { useFormContext } from 'react-hook-form'; |
||||||
|
|
||||||
|
import { GrafanaTheme2, SupportedTransformationType } from '@grafana/data'; |
||||||
|
import { Stack } from '@grafana/experimental'; |
||||||
|
import { |
||||||
|
Button, |
||||||
|
Field, |
||||||
|
FieldArray, |
||||||
|
Icon, |
||||||
|
IconButton, |
||||||
|
Input, |
||||||
|
InputControl, |
||||||
|
Label, |
||||||
|
Select, |
||||||
|
Tooltip, |
||||||
|
useStyles2, |
||||||
|
} from '@grafana/ui'; |
||||||
|
|
||||||
|
type Props = { readOnly: boolean }; |
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => ({ |
||||||
|
heading: css` |
||||||
|
font-size: ${theme.typography.h5.fontSize}; |
||||||
|
font-weight: ${theme.typography.fontWeightRegular}; |
||||||
|
`,
|
||||||
|
// set fixed position from the top instead of centring as the container
|
||||||
|
// may get bigger when the for is invalid
|
||||||
|
removeButton: css` |
||||||
|
margin-top: 25px; |
||||||
|
`,
|
||||||
|
}); |
||||||
|
|
||||||
|
export const TransformationsEditor = (props: Props) => { |
||||||
|
const { control, formState, register, setValue, watch, getValues } = useFormContext(); |
||||||
|
const { readOnly } = props; |
||||||
|
const [keptVals, setKeptVals] = useState<Array<{ expression?: string; mapValue?: string }>>([]); |
||||||
|
|
||||||
|
const styles = useStyles2(getStyles); |
||||||
|
|
||||||
|
const transformOptions = getTransformOptions(); |
||||||
|
return ( |
||||||
|
<> |
||||||
|
<input type="hidden" {...register('id')} /> |
||||||
|
<FieldArray name="config.transformations" control={control}> |
||||||
|
{({ fields, append, remove }) => ( |
||||||
|
<> |
||||||
|
<Stack direction="column" alignItems="flex-start"> |
||||||
|
<div className={styles.heading}>Transformations</div> |
||||||
|
{fields.length === 0 && <div> No transformations defined.</div>} |
||||||
|
{fields.length > 0 && ( |
||||||
|
<div> |
||||||
|
{fields.map((fieldVal, index) => { |
||||||
|
return ( |
||||||
|
<Stack direction="row" key={fieldVal.id} alignItems="top"> |
||||||
|
<Field |
||||||
|
label={ |
||||||
|
<Stack gap={0.5}> |
||||||
|
<Label htmlFor={`config.transformations.${fieldVal.id}-${index}.type`}>Type</Label> |
||||||
|
<Tooltip |
||||||
|
content={ |
||||||
|
<div> |
||||||
|
<p>The type of transformation that will be applied to the source data.</p> |
||||||
|
</div> |
||||||
|
} |
||||||
|
> |
||||||
|
<Icon name="info-circle" size="sm" /> |
||||||
|
</Tooltip> |
||||||
|
</Stack> |
||||||
|
} |
||||||
|
invalid={!!formState.errors?.config?.transformations?.[index]?.type} |
||||||
|
error={formState.errors?.config?.transformations?.[index]?.type?.message} |
||||||
|
validationMessageHorizontalOverflow={true} |
||||||
|
> |
||||||
|
<InputControl |
||||||
|
render={({ field: { onChange, ref, ...field } }) => { |
||||||
|
// input control field is not manipulated with remove, use value from control
|
||||||
|
return ( |
||||||
|
<Select |
||||||
|
{...field} |
||||||
|
value={fieldVal.type} |
||||||
|
onChange={(value) => { |
||||||
|
if (!readOnly) { |
||||||
|
const currentValues = getValues().config.transformations[index]; |
||||||
|
let keptValsCopy = fill(Array(index + 1), {}); |
||||||
|
keptVals.forEach((keptVal, i) => (keptValsCopy[i] = keptVal)); |
||||||
|
keptValsCopy[index] = { |
||||||
|
expression: currentValues.expression, |
||||||
|
mapValue: currentValues.mapValue, |
||||||
|
}; |
||||||
|
|
||||||
|
setKeptVals(keptValsCopy); |
||||||
|
|
||||||
|
const newValueDetails = getSupportedTransTypeDetails(value.value); |
||||||
|
|
||||||
|
if (newValueDetails.showExpression) { |
||||||
|
setValue( |
||||||
|
`config.transformations.${index}.expression`, |
||||||
|
keptVals[index]?.expression || '' |
||||||
|
); |
||||||
|
} else { |
||||||
|
setValue(`config.transformations.${index}.expression`, ''); |
||||||
|
} |
||||||
|
|
||||||
|
if (newValueDetails.showMapValue) { |
||||||
|
setValue( |
||||||
|
`config.transformations.${index}.mapValue`, |
||||||
|
keptVals[index]?.mapValue || '' |
||||||
|
); |
||||||
|
} else { |
||||||
|
setValue(`config.transformations.${index}.mapValue`, ''); |
||||||
|
} |
||||||
|
|
||||||
|
onChange(value.value); |
||||||
|
} |
||||||
|
}} |
||||||
|
options={transformOptions} |
||||||
|
width={25} |
||||||
|
inputId={`config.transformations.${fieldVal.id}-${index}.type`} |
||||||
|
/> |
||||||
|
); |
||||||
|
}} |
||||||
|
control={control} |
||||||
|
name={`config.transformations.${index}.type`} |
||||||
|
rules={{ required: { value: true, message: 'Please select a transformation type' } }} |
||||||
|
/> |
||||||
|
</Field> |
||||||
|
<Field |
||||||
|
label={ |
||||||
|
<Stack gap={0.5}> |
||||||
|
<Label htmlFor={`config.transformations.${fieldVal.id}.field`}>Field</Label> |
||||||
|
<Tooltip |
||||||
|
content={ |
||||||
|
<div> |
||||||
|
<p> |
||||||
|
Optional. The field to transform. If not specified, the transformation will be |
||||||
|
applied to the results field. |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
} |
||||||
|
> |
||||||
|
<Icon name="info-circle" size="sm" /> |
||||||
|
</Tooltip> |
||||||
|
</Stack> |
||||||
|
} |
||||||
|
> |
||||||
|
<Input |
||||||
|
{...register(`config.transformations.${index}.field`)} |
||||||
|
readOnly={readOnly} |
||||||
|
defaultValue={fieldVal.field} |
||||||
|
label="field" |
||||||
|
id={`config.transformations.${fieldVal.id}.field`} |
||||||
|
/> |
||||||
|
</Field> |
||||||
|
<Field |
||||||
|
label={ |
||||||
|
<Stack gap={0.5}> |
||||||
|
<Label htmlFor={`config.transformations.${fieldVal.id}.expression`}> |
||||||
|
Expression |
||||||
|
{getSupportedTransTypeDetails(watch(`config.transformations.${index}.type`)) |
||||||
|
.requireExpression |
||||||
|
? ' *' |
||||||
|
: ''} |
||||||
|
</Label> |
||||||
|
<Tooltip |
||||||
|
content={ |
||||||
|
<div> |
||||||
|
<p> |
||||||
|
Required for regular expression. The expression the transformation will use. |
||||||
|
Logfmt does not use further specifications. |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
} |
||||||
|
> |
||||||
|
<Icon name="info-circle" size="sm" /> |
||||||
|
</Tooltip> |
||||||
|
</Stack> |
||||||
|
} |
||||||
|
invalid={!!formState.errors?.config?.transformations?.[index]?.expression} |
||||||
|
error={formState.errors?.config?.transformations?.[index]?.expression?.message} |
||||||
|
> |
||||||
|
<Input |
||||||
|
{...register(`config.transformations.${index}.expression`, { |
||||||
|
required: getSupportedTransTypeDetails(watch(`config.transformations.${index}.type`)) |
||||||
|
.requireExpression |
||||||
|
? 'Please define an expression' |
||||||
|
: undefined, |
||||||
|
})} |
||||||
|
defaultValue={fieldVal.expression} |
||||||
|
readOnly={readOnly} |
||||||
|
disabled={ |
||||||
|
!getSupportedTransTypeDetails(watch(`config.transformations.${index}.type`)) |
||||||
|
.showExpression |
||||||
|
} |
||||||
|
id={`config.transformations.${fieldVal.id}.expression`} |
||||||
|
/> |
||||||
|
</Field> |
||||||
|
<Field |
||||||
|
label={ |
||||||
|
<Stack gap={0.5}> |
||||||
|
<Label htmlFor={`config.transformations.${fieldVal.id}.mapValue`}>Map value</Label> |
||||||
|
<Tooltip |
||||||
|
content={ |
||||||
|
<div> |
||||||
|
<p> |
||||||
|
Optional. Defines the name of the variable. This is currently only valid for |
||||||
|
regular expressions with a single, unnamed capture group. |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
} |
||||||
|
> |
||||||
|
<Icon name="info-circle" size="sm" /> |
||||||
|
</Tooltip> |
||||||
|
</Stack> |
||||||
|
} |
||||||
|
> |
||||||
|
<Input |
||||||
|
{...register(`config.transformations.${index}.mapValue`)} |
||||||
|
defaultValue={fieldVal.mapValue} |
||||||
|
readOnly={readOnly} |
||||||
|
disabled={ |
||||||
|
!getSupportedTransTypeDetails(watch(`config.transformations.${index}.type`)).showMapValue |
||||||
|
} |
||||||
|
id={`config.transformations.${fieldVal.id}.mapValue`} |
||||||
|
/> |
||||||
|
</Field> |
||||||
|
{!readOnly && ( |
||||||
|
<div className={styles.removeButton}> |
||||||
|
<IconButton |
||||||
|
type="button" |
||||||
|
tooltip="Remove transformation" |
||||||
|
name={'trash-alt'} |
||||||
|
onClick={() => { |
||||||
|
remove(index); |
||||||
|
const keptValsCopy: Array<{ expression?: string; mapValue?: string } | undefined> = [ |
||||||
|
...keptVals, |
||||||
|
]; |
||||||
|
keptValsCopy[index] = undefined; |
||||||
|
setKeptVals(compact(keptValsCopy)); |
||||||
|
}} |
||||||
|
ariaLabel="Remove transformation" |
||||||
|
> |
||||||
|
Remove |
||||||
|
</IconButton> |
||||||
|
</div> |
||||||
|
)} |
||||||
|
</Stack> |
||||||
|
); |
||||||
|
})} |
||||||
|
</div> |
||||||
|
)} |
||||||
|
{!readOnly && ( |
||||||
|
<Button |
||||||
|
icon="plus" |
||||||
|
onClick={() => append({ type: undefined }, { shouldFocus: false })} |
||||||
|
variant="secondary" |
||||||
|
type="button" |
||||||
|
> |
||||||
|
Add transformation |
||||||
|
</Button> |
||||||
|
)} |
||||||
|
</Stack> |
||||||
|
</> |
||||||
|
)} |
||||||
|
</FieldArray> |
||||||
|
</> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
interface SupportedTransformationTypeDetails { |
||||||
|
label: string; |
||||||
|
value: string; |
||||||
|
description?: string; |
||||||
|
showExpression: boolean; |
||||||
|
showMapValue: boolean; |
||||||
|
requireExpression?: boolean; |
||||||
|
} |
||||||
|
|
||||||
|
function getSupportedTransTypeDetails(transType: SupportedTransformationType): SupportedTransformationTypeDetails { |
||||||
|
switch (transType) { |
||||||
|
case SupportedTransformationType.Logfmt: |
||||||
|
return { |
||||||
|
label: 'Logfmt', |
||||||
|
value: SupportedTransformationType.Logfmt, |
||||||
|
description: 'Parse provided field with logfmt to get variables', |
||||||
|
showExpression: false, |
||||||
|
showMapValue: false, |
||||||
|
}; |
||||||
|
case SupportedTransformationType.Regex: |
||||||
|
return { |
||||||
|
label: 'Regular expression', |
||||||
|
value: SupportedTransformationType.Regex, |
||||||
|
description: |
||||||
|
'Field will be parsed with regex. Use named capture groups to return multiple variables, or a single unnamed capture group to add variable to named map value.', |
||||||
|
showExpression: true, |
||||||
|
showMapValue: true, |
||||||
|
requireExpression: true, |
||||||
|
}; |
||||||
|
default: |
||||||
|
return { label: transType, value: transType, showExpression: false, showMapValue: false }; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const getTransformOptions = () => { |
||||||
|
return Object.values(SupportedTransformationType).map((transformationType) => { |
||||||
|
const transType = getSupportedTransTypeDetails(transformationType); |
||||||
|
return { |
||||||
|
label: transType.label, |
||||||
|
value: transType.value, |
||||||
|
description: transType.description, |
||||||
|
}; |
||||||
|
}); |
||||||
|
}; |
||||||
Loading…
Reference in new issue