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:
@@ -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.", "0"],
|
||||||
[0, 0, 0, "Styles should be written using objects.", "1"]
|
[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": [
|
"public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx:5381": [
|
||||||
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"]
|
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"]
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -1,48 +1,27 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { compact, fill } from 'lodash';
|
import React from 'react';
|
||||||
import React, { useState } from 'react';
|
|
||||||
import { useFormContext } from 'react-hook-form';
|
import { useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import {
|
import { Button, FieldArray, Stack, useStyles2 } from '@grafana/ui';
|
||||||
Button,
|
|
||||||
Field,
|
|
||||||
FieldArray,
|
|
||||||
Icon,
|
|
||||||
IconButton,
|
|
||||||
Input,
|
|
||||||
InputControl,
|
|
||||||
Label,
|
|
||||||
Select,
|
|
||||||
Tooltip,
|
|
||||||
useStyles2,
|
|
||||||
Stack,
|
|
||||||
} from '@grafana/ui';
|
|
||||||
|
|
||||||
import { getSupportedTransTypeDetails, getTransformOptions } from './types';
|
import TransformationsEditorRow from './TransformationEditorRow';
|
||||||
|
|
||||||
type Props = { readOnly: boolean };
|
type Props = { readOnly: boolean };
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => ({
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
heading: css`
|
heading: css({
|
||||||
font-size: ${theme.typography.h5.fontSize};
|
fontSize: theme.typography.h5.fontSize,
|
||||||
font-weight: ${theme.typography.fontWeightRegular};
|
fontWeight: 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) => {
|
export const TransformationsEditor = (props: Props) => {
|
||||||
const { control, formState, register, setValue, watch, getValues } = useFormContext();
|
const { control, register } = useFormContext();
|
||||||
const { readOnly } = props;
|
const { readOnly } = props;
|
||||||
const [keptVals, setKeptVals] = useState<Array<{ expression?: string; mapValue?: string }>>([]);
|
|
||||||
|
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
const transformOptions = getTransformOptions();
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<input type="hidden" {...register('id')} />
|
<input type="hidden" {...register('id')} />
|
||||||
@@ -56,198 +35,13 @@ export const TransformationsEditor = (props: Props) => {
|
|||||||
<div>
|
<div>
|
||||||
{fields.map((fieldVal, index) => {
|
{fields.map((fieldVal, index) => {
|
||||||
return (
|
return (
|
||||||
<Stack direction="row" key={fieldVal.id} alignItems="flex-start">
|
<TransformationsEditorRow
|
||||||
<Field
|
key={index}
|
||||||
label={
|
value={fieldVal}
|
||||||
<Stack gap={0.5}>
|
index={index}
|
||||||
<Label htmlFor={`config.transformations.${fieldVal.id}-${index}.type`}>Type</Label>
|
readOnly={readOnly}
|
||||||
<Tooltip
|
remove={remove}
|
||||||
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>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user