Correlations: Fix incorrect state transitions in transformations editor (#77434)

* Extract transformations editor row to a seprate component

* Use css object instead of string literals

* Update .betterer.results

* Post merge fixes

* Bring back validation rules

* Switch to css/object

* Post-merge fixes

Ensuring Stack from grafana-ui is used after conflicts with 25779bb6e5
This commit is contained in:
Piotr Jamróz 2023-11-07 12:17:02 +01:00 committed by GitHub
parent 662bc286c2
commit b7f854a06c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 217 additions and 225 deletions

View File

@ -2869,10 +2869,6 @@ exports[`better eslint`] = {
[0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"]
],
"public/app/features/correlations/Forms/TransformationsEditor.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"]
],
"public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx:5381": [
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"]
],

View File

@ -0,0 +1,202 @@
import { css } from '@emotion/css';
import React, { useState } from 'react';
import { useFormContext, useWatch } from 'react-hook-form';
import { Field, Icon, IconButton, Input, Label, Select, Stack, Tooltip, useStyles2 } from '@grafana/ui';
import { getSupportedTransTypeDetails, getTransformOptions } from './types';
type Props = {
index: number;
value: Record<string, string>;
readOnly: boolean;
remove: (index?: number | number[]) => void;
};
const getStyles = () => ({
// set fixed position from the top instead of centring as the container
// may get bigger when the for is invalid
removeButton: css({
marginTop: '25px',
}),
});
const TransformationEditorRow = (props: Props) => {
const { index, value: defaultValue, readOnly, remove } = props;
const { control, formState, register, setValue, watch, getValues } = useFormContext();
const [keptVals, setKeptVals] = useState<{ expression?: string; mapValue?: string }>({});
register(`config.transformations.${index}.type`, {
required: { value: true, message: 'Please select a transformation type' },
});
const typeValue = useWatch({ name: `config.transformations.${index}.type`, control });
const styles = useStyles2(getStyles);
const transformOptions = getTransformOptions();
return (
<Stack direction="row" key={defaultValue.id} alignItems="flex-start">
<Field
label={
<Stack gap={0.5}>
<Label htmlFor={`config.transformations.${defaultValue.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}
>
<Select
value={typeValue}
onChange={(value) => {
if (!readOnly) {
const currentValues = getValues().config.transformations[index];
setKeptVals({
expression: currentValues.expression,
mapValue: currentValues.mapValue,
});
const newValueDetails = getSupportedTransTypeDetails(value.value);
if (newValueDetails.expressionDetails.show) {
setValue(`config.transformations.${index}.expression`, keptVals?.expression || '');
} else {
setValue(`config.transformations.${index}.expression`, '');
}
if (newValueDetails.mapValueDetails.show) {
setValue(`config.transformations.${index}.mapValue`, keptVals?.mapValue || '');
} else {
setValue(`config.transformations.${index}.mapValue`, '');
}
setValue(`config.transformations.${index}.type`, value.value);
}
}}
options={transformOptions}
width={25}
inputId={`config.transformations.${defaultValue.id}-${index}.type`}
/>
</Field>
<Field
label={
<Stack gap={0.5}>
<Label htmlFor={`config.transformations.${defaultValue.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={defaultValue.field}
label="field"
id={`config.transformations.${defaultValue.id}.field`}
/>
</Field>
<Field
label={
<Stack gap={0.5}>
<Label htmlFor={`config.transformations.${defaultValue.id}.expression`}>
Expression
{getSupportedTransTypeDetails(watch(`config.transformations.${index}.type`)).expressionDetails.required
? ' *'
: ''}
</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`)).expressionDetails
.required
? 'Please define an expression'
: undefined,
})}
defaultValue={defaultValue.expression}
readOnly={readOnly}
disabled={!getSupportedTransTypeDetails(watch(`config.transformations.${index}.type`)).expressionDetails.show}
id={`config.transformations.${defaultValue.id}.expression`}
/>
</Field>
<Field
label={
<Stack gap={0.5}>
<Label htmlFor={`config.transformations.${defaultValue.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={defaultValue.mapValue}
readOnly={readOnly}
disabled={!getSupportedTransTypeDetails(watch(`config.transformations.${index}.type`)).mapValueDetails.show}
id={`config.transformations.${defaultValue.id}.mapValue`}
/>
</Field>
{!readOnly && (
<div className={styles.removeButton}>
<IconButton
tooltip="Remove transformation"
name="trash-alt"
onClick={() => {
remove(index);
}}
>
Remove
</IconButton>
</div>
)}
</Stack>
);
};
export default TransformationEditorRow;

View File

@ -1,48 +1,27 @@
import { css } from '@emotion/css';
import { compact, fill } from 'lodash';
import React, { useState } from 'react';
import React from 'react';
import { useFormContext } from 'react-hook-form';
import { GrafanaTheme2 } from '@grafana/data';
import {
Button,
Field,
FieldArray,
Icon,
IconButton,
Input,
InputControl,
Label,
Select,
Tooltip,
useStyles2,
Stack,
} from '@grafana/ui';
import { Button, FieldArray, Stack, useStyles2 } from '@grafana/ui';
import { getSupportedTransTypeDetails, getTransformOptions } from './types';
import TransformationsEditorRow from './TransformationEditorRow';
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;
`,
heading: css({
fontSize: theme.typography.h5.fontSize,
fontWeight: theme.typography.fontWeightRegular,
}),
});
export const TransformationsEditor = (props: Props) => {
const { control, formState, register, setValue, watch, getValues } = useFormContext();
const { control, register } = 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')} />
@ -56,198 +35,13 @@ export const TransformationsEditor = (props: Props) => {
<div>
{fields.map((fieldVal, index) => {
return (
<Stack direction="row" key={fieldVal.id} alignItems="flex-start">
<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.expressionDetails.show) {
setValue(
`config.transformations.${index}.expression`,
keptVals[index]?.expression || ''
);
} else {
setValue(`config.transformations.${index}.expression`, '');
}
if (newValueDetails.mapValueDetails.show) {
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`))
.expressionDetails.required
? ' *'
: ''}
</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`))
.expressionDetails.required
? 'Please define an expression'
: undefined,
})}
defaultValue={fieldVal.expression}
readOnly={readOnly}
disabled={
!getSupportedTransTypeDetails(watch(`config.transformations.${index}.type`))
.expressionDetails.show
}
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`))
.mapValueDetails.show
}
id={`config.transformations.${fieldVal.id}.mapValue`}
/>
</Field>
{!readOnly && (
<div className={styles.removeButton}>
<IconButton
tooltip="Remove transformation"
name="trash-alt"
onClick={() => {
remove(index);
const keptValsCopy: Array<{ expression?: string; mapValue?: string } | undefined> = [
...keptVals,
];
keptValsCopy[index] = undefined;
setKeptVals(compact(keptValsCopy));
}}
>
Remove
</IconButton>
</div>
)}
</Stack>
<TransformationsEditorRow
key={index}
value={fieldVal}
index={index}
readOnly={readOnly}
remove={remove}
/>
);
})}
</div>