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