Handle Json in Set and Append Value field (#204336)

Closes [#193186](https://github.com/elastic/kibana/issues/193186)

## Sumary

This PR introduces changes in two processors: Set y Append. The aim of
this changes is to allow introducing the Value field in both processors
in Json format. Now, Set accepts Text format or Json and Append a list
of values or Json. The toggle between the format is done by the new
Label Append button.
<img width="705" alt="Screenshot 2024-12-16 at 07 30 11"
src="https://github.com/user-attachments/assets/0f6283ed-6117-48d8-bdd8-e0685ae1b42d"
/>


Also, since the Set processor already had a Label Append button for
toggling between the `Value` and the `Copy_from` field, this now has
been replaced by a toggle button that enables a field for the
`Copy_from` field (see the last comments in the related Issue for see
the most updated mocks).
<img width="708" alt="Screenshot 2024-12-16 at 07 30 49"
src="https://github.com/user-attachments/assets/4a632bda-4b4b-49b1-b3d4-d11579d7c27c"
/>

Note: the Set copies still pending review so probably will be changed.

### Demo


https://github.com/user-attachments/assets/f9d1da6a-782d-4197-836b-fab46b4476b7
This commit is contained in:
Sonia Sanz Vivas 2025-01-03 07:34:57 +01:00 committed by GitHub
parent fd63ee5e28
commit 59b229280d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 453 additions and 164 deletions

View file

@ -25485,7 +25485,6 @@
"xpack.ingestPipelines.pipelineEditor.uppercaseForm.fieldNameHelpText": "Champ à mettre en majuscules. Pour un tableau de chaînes, chaque élément est mis en majuscules.",
"xpack.ingestPipelines.pipelineEditor.uriPartsForm.fieldNameHelpText": "Champ contenant la chaîne d'URI.",
"xpack.ingestPipelines.pipelineEditor.urlDecodeForm.fieldNameHelpText": "Champ à décoder. Pour un tableau de chaînes, chaque élément est décodé.",
"xpack.ingestPipelines.pipelineEditor.useCopyFromLabel": "Utiliser le champ Copier à partir de",
"xpack.ingestPipelines.pipelineEditor.userAgentForm.extractDeviceNameFieldText": "Extraire le type d'appareil",
"xpack.ingestPipelines.pipelineEditor.userAgentForm.extractDeviceNameTooltipText": "Cette fonctionnalité est en version bêta et susceptible d'être modifiée.",
"xpack.ingestPipelines.pipelineEditor.userAgentForm.extractDeviceTypeFieldHelpText": "Extrait le type d'appareil à partir de la chaîne d'agent utilisateur.",
@ -25494,7 +25493,6 @@
"xpack.ingestPipelines.pipelineEditor.userAgentForm.regexFileFieldHelpText": "Fichier contenant les expressions régulières utilisées pour analyser la chaîne d'agent utilisateur.",
"xpack.ingestPipelines.pipelineEditor.userAgentForm.regexFileFieldLabel": "Fichier d'expression régulière (facultatif)",
"xpack.ingestPipelines.pipelineEditor.userAgentForm.targetFieldHelpText": "Champ de sortie. La valeur par défaut est {defaultField}.",
"xpack.ingestPipelines.pipelineEditor.useValueLabel": "Utiliser le champ de valeur",
"xpack.ingestPipelines.pipelineEditorItem.droppedStatusAriaLabel": "Abandonné",
"xpack.ingestPipelines.pipelineEditorItem.errorIgnoredStatusAriaLabel": "Erreur ignorée",
"xpack.ingestPipelines.pipelineEditorItem.errorStatusAriaLabel": "Erreur",

View file

@ -25344,7 +25344,6 @@
"xpack.ingestPipelines.pipelineEditor.uppercaseForm.fieldNameHelpText": "大文字にするフィールド。文字列の配列の場合、各エレメントが大文字にされます。",
"xpack.ingestPipelines.pipelineEditor.uriPartsForm.fieldNameHelpText": "URI文字列を含むフィールド。",
"xpack.ingestPipelines.pipelineEditor.urlDecodeForm.fieldNameHelpText": "デコードするフィールド。文字列の配列の場合、各エレメントがデコードされます。",
"xpack.ingestPipelines.pipelineEditor.useCopyFromLabel": "フィールドからコピーを使用",
"xpack.ingestPipelines.pipelineEditor.userAgentForm.extractDeviceNameFieldText": "デバイスタイプを抽出",
"xpack.ingestPipelines.pipelineEditor.userAgentForm.extractDeviceNameTooltipText": "この機能はベータ段階で、変更される可能性があります。",
"xpack.ingestPipelines.pipelineEditor.userAgentForm.extractDeviceTypeFieldHelpText": "ユーザーエージェント文字列からデバイスタイプを抽出します。",
@ -25353,7 +25352,6 @@
"xpack.ingestPipelines.pipelineEditor.userAgentForm.regexFileFieldHelpText": "ユーザーエージェント文字列を解析するために使用される正規表現を含むファイル。",
"xpack.ingestPipelines.pipelineEditor.userAgentForm.regexFileFieldLabel": "正規表現ファイル(任意)",
"xpack.ingestPipelines.pipelineEditor.userAgentForm.targetFieldHelpText": "出力フィールド。デフォルトは{defaultField}です。",
"xpack.ingestPipelines.pipelineEditor.useValueLabel": "値フィールドを使用",
"xpack.ingestPipelines.pipelineEditorItem.droppedStatusAriaLabel": "ドロップ",
"xpack.ingestPipelines.pipelineEditorItem.errorIgnoredStatusAriaLabel": "エラーを無視",
"xpack.ingestPipelines.pipelineEditorItem.errorStatusAriaLabel": "エラー",

View file

@ -24951,7 +24951,6 @@
"xpack.ingestPipelines.pipelineEditor.uppercaseForm.fieldNameHelpText": "要大写的字段。对于字符串数组,每个元素都为大写。",
"xpack.ingestPipelines.pipelineEditor.uriPartsForm.fieldNameHelpText": "包含 URI 字符串的字段。",
"xpack.ingestPipelines.pipelineEditor.urlDecodeForm.fieldNameHelpText": "要解码的字段。对于字符串数组,每个元素都要解码。",
"xpack.ingestPipelines.pipelineEditor.useCopyFromLabel": "使用复制位置字段",
"xpack.ingestPipelines.pipelineEditor.userAgentForm.extractDeviceNameFieldText": "确切设备类型",
"xpack.ingestPipelines.pipelineEditor.userAgentForm.extractDeviceNameTooltipText": "此功能为公测版,可能会进行更改。",
"xpack.ingestPipelines.pipelineEditor.userAgentForm.extractDeviceTypeFieldHelpText": "从用户代理字符串中提取设备类型。",
@ -24960,7 +24959,6 @@
"xpack.ingestPipelines.pipelineEditor.userAgentForm.regexFileFieldHelpText": "包含用于解析用户代理字符串的正则表达式的文件。",
"xpack.ingestPipelines.pipelineEditor.userAgentForm.regexFileFieldLabel": "正则表达式文件(可选)",
"xpack.ingestPipelines.pipelineEditor.userAgentForm.targetFieldHelpText": "输出字段。默认为 {defaultField}。",
"xpack.ingestPipelines.pipelineEditor.useValueLabel": "使用值字段",
"xpack.ingestPipelines.pipelineEditorItem.droppedStatusAriaLabel": "已丢弃",
"xpack.ingestPipelines.pipelineEditorItem.errorIgnoredStatusAriaLabel": "错误已忽略",
"xpack.ingestPipelines.pipelineEditorItem.errorStatusAriaLabel": "错误",

View file

@ -144,7 +144,7 @@ describe('Pipeline Editor', () => {
// Open the edit processor form for the set processor
actions.openProcessorEditor('processors>2');
expect(exists('editProcessorForm')).toBeTruthy();
form.setInputValue('editProcessorForm.valueFieldInput', 'test44');
form.setInputValue('editProcessorForm.textValueField.input', 'test44');
jest.advanceTimersByTime(0); // advance timers to allow the form to validate
await actions.submitProcessorForm();
const [onUpdateResult] = onUpdate.mock.calls[onUpdate.mock.calls.length - 1];
@ -175,7 +175,7 @@ describe('Pipeline Editor', () => {
// Change its type to `append` and set the missing required fields
await actions.setProcessorType('append');
await act(async () => {
find('appendValueField.input').simulate('change', [{ label: 'some_value' }]);
find('comboxValueField.input').simulate('change', [{ label: 'some_value' }]);
});
component.update();

View file

@ -75,7 +75,7 @@ describe('Processor: Append', () => {
form.setInputValue('fieldNameField.input', 'field_1');
await act(async () => {
find('appendValueField.input').simulate('change', [{ label: 'Some_Value' }]);
find('comboxValueField.input').simulate('change', [{ label: 'Some_Value' }]);
});
component.update();
@ -102,7 +102,7 @@ describe('Processor: Append', () => {
// Set optional parameteres
await act(async () => {
find('appendValueField.input').simulate('change', [{ label: 'Some_Value' }]);
find('comboxValueField.input').simulate('change', [{ label: 'Some_Value' }]);
component.update();
});
form.toggleEuiSwitch('allowDuplicatesSwitch.input');
@ -134,14 +134,14 @@ describe('Processor: Append', () => {
// Shouldn't be able to set media_type if value is not a template string
await act(async () => {
find('appendValueField.input').simulate('change', [{ label: 'value_1' }]);
find('comboxValueField.input').simulate('change', [{ label: 'value_1' }]);
});
component.update();
expect(exists('mediaTypeSelectorField')).toBe(false);
// Set value to a template snippet and media_type to a non-default value
await act(async () => {
find('appendValueField.input').simulate('change', [{ label: '{{{value_2}}}' }]);
find('comboxValueField.input').simulate('change', [{ label: '{{{value_2}}}' }]);
});
component.update();
form.setSelectValue('mediaTypeSelectorField', 'text/plain');
@ -156,4 +156,42 @@ describe('Processor: Append', () => {
media_type: 'text/plain',
});
});
test('saves with json parameter values', async () => {
const {
actions: { saveNewProcessor },
form,
find,
component,
} = testBed;
// Add "field" value (required)
form.setInputValue('fieldNameField.input', 'field_1');
await act(async () => {
find('comboxValueField.input').simulate('change', [{ label: 'Some_Value' }]);
});
component.update();
find('toggleTextField').simulate('click');
await act(async () => {
find('jsonValueField').simulate('change', {
jsonContent: '{"value_1":"""aaa"bbb""", "value_2":"aaa(bbb"}',
});
// advance timers to allow the form to validate
jest.advanceTimersByTime(0);
});
// Save the field
await saveNewProcessor();
const processors = getProcessorValue(onUpdate, APPEND_TYPE);
expect(processors[0].append).toEqual({
field: 'field_1',
// eslint-disable-next-line prettier/prettier
value: { value_1: 'aaa\"bbb', value_2: 'aaa(bbb' },
});
});
});

View file

@ -122,7 +122,7 @@ type TestSubject =
| 'addProcessorForm.submitButton'
| 'addProcessorButton'
| 'addProcessorForm.submitButton'
| 'appendValueField.input'
| 'comboxValueField.input'
| 'allowDuplicatesSwitch.input'
| 'formatsValueField.input'
| 'timezoneField.input'
@ -157,10 +157,11 @@ type TestSubject =
| 'extractDeviceTypeSwitch.input'
| 'propertiesValueField'
| 'regexFileField.input'
| 'valueFieldInput'
| 'textValueField.input'
| 'mediaTypeSelectorField'
| 'networkDirectionField.input'
| 'toggleCustomField'
| 'toggleCustomField.input'
| 'ignoreEmptyField.input'
| 'overrideField.input'
| 'fieldsValueField.input'
@ -175,7 +176,7 @@ type TestSubject =
| 'ianaField.input'
| 'transportField.input'
| 'seedField.input'
| 'copyFromInput'
| 'copyFromInput.input'
| 'trimSwitch.input'
| 'droppableList.addButton'
| 'droppableList.input-0'
@ -205,4 +206,6 @@ type TestSubject =
| 'scriptSource'
| 'inferenceModelId.input'
| 'inferenceConfig'
| 'fieldMap';
| 'fieldMap'
| 'toggleTextField'
| 'jsonValueField';

View file

@ -67,7 +67,7 @@ describe('Processor: Set', () => {
} = testBed;
// Add required fields
form.setInputValue('valueFieldInput', 'value');
form.setInputValue('textValueField.input', 'value');
form.setInputValue('fieldNameField.input', 'field_1');
// Save the field
await saveNewProcessor();
@ -83,18 +83,17 @@ describe('Processor: Set', () => {
const {
actions: { saveNewProcessor },
form,
find,
} = testBed;
// Add required fields
form.setInputValue('fieldNameField.input', 'field_1');
// Set value field
form.setInputValue('valueFieldInput', 'value');
form.setInputValue('textValueField.input', 'value');
// Toggle to copy_from field and set a random value
find('toggleCustomField').simulate('click');
form.setInputValue('copyFromInput', 'copy_from');
form.toggleEuiSwitch('toggleCustomField.input');
form.setInputValue('copyFromInput.input', 'copy_from');
// Save the field with new changes
await saveNewProcessor();
@ -117,11 +116,11 @@ describe('Processor: Set', () => {
form.setInputValue('fieldNameField.input', 'field_1');
// Shouldnt be able to set mediaType if value is not a template string
form.setInputValue('valueFieldInput', 'hello');
form.setInputValue('textValueField.input', 'hello');
expect(exists('mediaTypeSelectorField')).toBe(false);
// Set value to a template snippet and media_type to a non-default value
form.setInputValue('valueFieldInput', '{{{hello}}}');
form.setInputValue('textValueField.input', '{{{hello}}}');
form.setSelectValue('mediaTypeSelectorField', 'text/plain');
// Save the field with new changes
@ -145,7 +144,7 @@ describe('Processor: Set', () => {
form.setInputValue('fieldNameField.input', 'field_1');
// Set optional parameteres
form.setInputValue('valueFieldInput', '{{{hello}}}');
form.setInputValue('textValueField.input', '{{{hello}}}');
form.toggleEuiSwitch('overrideField.input');
form.toggleEuiSwitch('ignoreEmptyField.input');
@ -160,4 +159,37 @@ describe('Processor: Set', () => {
override: false,
});
});
test('saves with json parameter value', async () => {
const {
actions: { saveNewProcessor },
form,
find,
component,
} = testBed;
form.setInputValue('textValueField.input', 'value');
find('toggleTextField').simulate('click');
form.setInputValue('fieldNameField.input', 'field_1');
await act(async () => {
find('jsonValueField').simulate('change', {
jsonContent: '{"value_1":"""aaa"bbb""", "value_2":"aaa(bbb"}',
});
// advance timers to allow the form to validate
jest.advanceTimersByTime(0);
});
component.update();
// Save the field
await saveNewProcessor();
const processors = getProcessorValue(onUpdate, SET_TYPE);
expect(processors[0][SET_TYPE]).toEqual({
field: 'field_1',
// eslint-disable-next-line prettier/prettier
value: { value_1: 'aaa\"bbb', value_2: 'aaa(bbb' },
});
});
});

View file

@ -9,3 +9,4 @@ export { DragAndDropTextList } from './drag_and_drop_text_list';
export { XJsonEditor } from './xjson_editor';
export { TextEditor } from './text_editor';
export { InputList } from './input_list';
export { XJsonToggle } from './xjson_toggle';

View file

@ -19,9 +19,10 @@ import './text_editor.scss';
interface Props {
field: FieldHook<string>;
editorProps: { [key: string]: any };
euiFieldProps?: Record<string, any>;
}
export const TextEditor: FunctionComponent<Props> = ({ field, editorProps }) => {
export const TextEditor: FunctionComponent<Props> = ({ field, editorProps, euiFieldProps }) => {
const { value, helpText, setValue, label } = field;
const { errorMessage } = getFieldValidityAndErrorMessage(field);
@ -32,11 +33,13 @@ export const TextEditor: FunctionComponent<Props> = ({ field, editorProps }) =>
isInvalid={typeof errorMessage === 'string'}
error={errorMessage}
fullWidth
labelAppend={editorProps.labelAppend}
>
<EuiPanel
className="pipelineProcessorsEditor__form__textEditor__panel"
paddingSize="s"
hasShadow={false}
{...euiFieldProps}
>
<CodeEditor value={value} onChange={setValue} {...(editorProps as any)} />
</EuiPanel>

View file

@ -14,6 +14,7 @@ import { TextEditor } from './text_editor';
interface Props {
field: FieldHook<string>;
editorProps: { [key: string]: any };
disabled?: boolean;
}
const defaultEditorOptions = {
@ -21,7 +22,7 @@ const defaultEditorOptions = {
lineNumbers: 'off',
};
export const XJsonEditor: FunctionComponent<Props> = ({ field, editorProps }) => {
export const XJsonEditor: FunctionComponent<Props> = ({ field, editorProps, disabled }) => {
const { value, setValue } = field;
const onChange = useCallback(
(s: any) => {
@ -29,6 +30,7 @@ export const XJsonEditor: FunctionComponent<Props> = ({ field, editorProps }) =>
},
[setValue]
);
return (
<TextEditor
field={field}
@ -39,6 +41,7 @@ export const XJsonEditor: FunctionComponent<Props> = ({ field, editorProps }) =>
onChange,
...editorProps,
}}
euiFieldProps={{ disabled }}
/>
);
};

View file

@ -0,0 +1,152 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, {
FunctionComponent,
useCallback,
useEffect,
useState,
useMemo,
MouseEventHandler,
} from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiLink, EuiText } from '@elastic/eui';
import { EDITOR_PX_HEIGHT, isXJsonValue } from '../processors/shared';
import { ComboBoxField, Field, FieldHook } from '../../../../../../shared_imports';
import { XJsonEditor } from '.';
type FieldType = 'text' | 'combox';
interface Props {
field: FieldHook;
disabled?: boolean;
handleIsJson: Function;
fieldType: FieldType;
}
interface ToggleProps {
field: FieldHook;
disabled?: boolean;
toggleJson: MouseEventHandler;
fieldType: FieldType;
}
const FieldToToggle: FunctionComponent<ToggleProps> = ({
field,
disabled,
toggleJson,
fieldType,
}) => {
if (fieldType === 'text') {
return (
<Field
data-test-subj="textValueField"
field={field}
euiFieldProps={{ disabled }}
labelAppend={
<EuiText size="xs">
<EuiLink onClick={toggleJson} data-test-subj="toggleTextField" disabled={disabled}>
<FormattedMessage
id="xpack.ingestPipelines.pipelineEditor.toggleJson.useJsonFormat"
defaultMessage="Define as JSON"
/>
</EuiLink>
</EuiText>
}
/>
);
}
if (fieldType === 'combox') {
return (
<ComboBoxField
data-test-subj="comboxValueField"
field={field}
euiFieldProps={{ disabled }}
labelAppend={
<EuiText size="xs">
<EuiLink onClick={toggleJson} data-test-subj="toggleTextField" disabled={disabled}>
<FormattedMessage
id="xpack.ingestPipelines.pipelineEditor.toggleJson.useJsonFormat"
defaultMessage="Define as JSON"
/>
</EuiLink>
</EuiText>
}
/>
);
}
};
export const XJsonToggle: FunctionComponent<Props> = ({
field,
disabled = false,
handleIsJson,
fieldType,
}) => {
const { value, setValue } = field;
const [defineAsJson, setDefineAsJson] = useState<boolean | undefined>(undefined);
const toggleJson = useCallback(() => {
const defaultValue = fieldType === 'text' ? '' : [];
const newValueIsJson = !defineAsJson;
setValue(newValueIsJson ? '{}' : defaultValue);
setDefineAsJson(newValueIsJson);
handleIsJson(newValueIsJson);
}, [defineAsJson, fieldType, handleIsJson, setValue]);
useEffect(() => {
if (defineAsJson === undefined) {
setDefineAsJson(isXJsonValue(value));
handleIsJson(isXJsonValue(value));
}
}, [defineAsJson, handleIsJson, setValue, value]);
const mustRenderXJsonEditor = useMemo(() => {
if (defineAsJson === undefined) {
return isXJsonValue(value);
}
return defineAsJson;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [defineAsJson]);
return mustRenderXJsonEditor ? (
<XJsonEditor
field={field as FieldHook<string>}
disabled={disabled}
editorProps={{
'data-test-subj': 'jsonValueField',
height: disabled ? EDITOR_PX_HEIGHT.extraSmall : EDITOR_PX_HEIGHT.medium,
'aria-label': i18n.translate(
'xpack.ingestPipelines.pipelineEditor.toggleJson.valueAriaLabel',
{
defaultMessage: 'Value editor',
}
),
options: { readOnly: disabled },
labelAppend: (
<EuiText size="xs">
<EuiLink onClick={toggleJson} data-test-subj="toggleJsonField" disabled={disabled}>
<FormattedMessage
id="xpack.ingestPipelines.pipelineEditor.toggleJson.useTextFormat"
defaultMessage="Define as text"
/>
</EuiLink>
</EuiText>
),
}}
/>
) : (
<FieldToToggle
field={field}
disabled={disabled}
toggleJson={toggleJson}
fieldType={fieldType}
/>
);
};

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { FunctionComponent } from 'react';
import React, { FunctionComponent, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
@ -14,22 +14,26 @@ import {
FIELD_TYPES,
fieldValidators,
UseField,
ComboBoxField,
ToggleField,
SelectField,
useFormData,
} from '../../../../../../shared_imports';
import { FieldsConfig, from, to } from './shared';
import { FieldsConfig, from, to, isXJsonValue, isXJsonField } from './shared';
import { FieldNameField } from './common_fields/field_name_field';
import { XJsonToggle } from '../field_components';
const { emptyField } = fieldValidators;
const fieldsConfig: FieldsConfig = {
value: {
defaultValue: [],
type: FIELD_TYPES.COMBO_BOX,
deserializer: to.arrayOfStrings,
defaultValue: (value: string | string[]) => {
return isXJsonValue(value) ? '{}' : [];
},
type: FIELD_TYPES.TEXT,
deserializer: (value: string | string[] | object) => {
return isXJsonValue(value) ? to.xJsonString(value) : to.arrayOfStrings(value);
},
label: i18n.translate('xpack.ingestPipelines.pipelineEditor.appendForm.valueFieldLabel', {
defaultMessage: 'Value',
}),
@ -44,6 +48,26 @@ const fieldsConfig: FieldsConfig = {
})
),
},
{
validator: (args) => {
const {
customData: { value: isJson },
} = args;
if (isJson) {
return isXJsonField(
i18n.translate(
'xpack.ingestPipelines.pipelineEditor.appendForm.valueInvalidJsonError',
{
defaultMessage: 'Invalid JSON',
}
),
{
allowEmptyString: true,
}
)({ ...args });
}
},
},
],
},
allow_duplicates: {
@ -82,6 +106,12 @@ const fieldsConfig: FieldsConfig = {
export const Append: FunctionComponent = () => {
const [{ fields }] = useFormData({ watch: ['fields.value'] });
const [isDefineAsJson, setIsDefineAsJson] = useState<boolean | undefined>(undefined);
const getIsJsonValue = (isJson: boolean) => {
setIsDefineAsJson(isJson);
};
return (
<>
<FieldNameField
@ -90,13 +120,6 @@ export const Append: FunctionComponent = () => {
})}
/>
<UseField
data-test-subj="appendValueField"
config={fieldsConfig.value}
component={ComboBoxField}
path="fields.value"
/>
<UseField
data-test-subj="allowDuplicatesSwitch"
config={fieldsConfig.allow_duplicates}
@ -104,6 +127,17 @@ export const Append: FunctionComponent = () => {
path="fields.allow_duplicates"
/>
<UseField
config={fieldsConfig.value}
component={XJsonToggle}
path="fields.value"
componentProps={{
handleIsJson: getIsJsonValue,
fieldType: 'combox',
}}
validationData={isDefineAsJson}
/>
{hasTemplateSnippet(fields?.value) && (
<UseField
componentProps={{

View file

@ -5,10 +5,10 @@
* 2.0.
*/
import React, { FunctionComponent, useState, useCallback, useMemo } from 'react';
import React, { FunctionComponent, useState, useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import { isEmpty } from 'lodash';
import { EuiCode, EuiLink, EuiText } from '@elastic/eui';
import { isEmpty, isUndefined } from 'lodash';
import { EuiCode } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import {
@ -18,24 +18,14 @@ import {
ToggleField,
UseField,
Field,
FieldHook,
FieldConfig,
useFormContext,
} from '../../../../../../shared_imports';
import { hasTemplateSnippet } from '../../../utils';
import { FieldsConfig, to, from } from './shared';
import { FieldsConfig, to, from, isXJsonField, isXJsonValue } from './shared';
import { FieldNameField } from './common_fields/field_name_field';
interface ValueToggleTypes {
value: string;
copy_from: string;
}
type ValueToggleFields = {
[K in keyof ValueToggleTypes]: FieldHook<ValueToggleTypes[K]>;
};
import { XJsonToggle } from '../field_components';
// Optional fields config
const fieldsConfig: FieldsConfig = {
@ -94,134 +84,131 @@ const fieldsConfig: FieldsConfig = {
/>
),
},
};
// Required fields config
const getValueConfig: (toggleCustom: () => void) => Record<
keyof ValueToggleFields,
{
path: string;
config?: FieldConfig<any>;
euiFieldProps?: Record<string, any>;
labelAppend: JSX.Element;
}
> = (toggleCustom: () => void) => ({
value: {
path: 'fields.value',
euiFieldProps: {
'data-test-subj': 'valueFieldInput',
type: FIELD_TYPES.TEXT,
defaultValue: (value: string) => {
return isXJsonValue(value) ? '{}' : '';
},
config: {
type: FIELD_TYPES.TEXT,
serializer: from.emptyStringToUndefined,
label: i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.valueFieldLabel', {
defaultMessage: 'Value',
}),
helpText: (
<FormattedMessage
id="xpack.ingestPipelines.pipelineEditor.setForm.valueFieldHelpText"
defaultMessage="Value for the field."
/>
),
fieldsToValidateOnChange: ['fields.value', 'fields.copy_from'],
validations: [
{
validator: ({ value, path, formData }) => {
if (isEmpty(value) && isEmpty(formData['fields.copy_from'])) {
return {
path,
message: i18n.translate('xpack.ingestPipelines.pipelineEditor.requiredValue', {
defaultMessage: 'A value is required.',
}),
};
}
},
},
],
deserializer: (value: string | object) => {
return isXJsonValue(value) ? to.xJsonString(value) : value;
},
labelAppend: (
<EuiText size="xs">
<EuiLink onClick={toggleCustom} data-test-subj="toggleCustomField">
<FormattedMessage
id="xpack.ingestPipelines.pipelineEditor.useCopyFromLabel"
defaultMessage="Use copy from field"
/>
</EuiLink>
</EuiText>
serializer: (value: string) => {
return isXJsonValue(value) ? value : from.emptyStringToUndefined(value);
},
label: i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.valueFieldLabel', {
defaultMessage: 'Value',
}),
helpText: (
<FormattedMessage
id="xpack.ingestPipelines.pipelineEditor.setForm.valueFieldHelpText"
defaultMessage="Value for the field."
/>
),
key: 'value',
validations: [
{
validator: ({ value, path, formData }) => {
if (isEmpty(value) && isUndefined(formData['fields.copy_from'])) {
return {
path,
message: i18n.translate('xpack.ingestPipelines.pipelineEditor.requiredValue', {
defaultMessage: 'A value is required.',
}),
};
}
},
},
{
validator: (args) => {
const {
customData: { value: isJson },
} = args;
if (isJson) {
return isXJsonField(
i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.valueInvalidJsonError', {
defaultMessage: 'Invalid JSON',
}),
{
allowEmptyString: true,
}
)({ ...args });
}
},
},
],
},
copy_from: {
path: 'fields.copy_from',
euiFieldProps: {
'data-test-subj': 'copyFromInput',
},
config: {
type: FIELD_TYPES.TEXT,
serializer: from.emptyStringToUndefined,
fieldsToValidateOnChange: ['fields.value', 'fields.copy_from'],
validations: [
{
validator: ({ value, path, formData }) => {
if (isEmpty(value) && isEmpty(formData['fields.value'])) {
return {
path,
message: i18n.translate('xpack.ingestPipelines.pipelineEditor.requiredCopyFrom', {
defaultMessage: 'A copy from value is required.',
}),
};
}
},
type: FIELD_TYPES.TEXT,
serializer: from.emptyStringToUndefined,
fieldsToValidateOnChange: ['fields.value', 'fields.copy_from'],
validations: [
{
validator: ({ value, path }) => {
if (isEmpty(value)) {
return {
path,
message: i18n.translate('xpack.ingestPipelines.pipelineEditor.requiredCopyFrom', {
defaultMessage: 'A copy from value is required.',
}),
};
}
},
],
label: i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.copyFromFieldLabel', {
defaultMessage: 'Copy from',
}),
helpText: (
<FormattedMessage
id="xpack.ingestPipelines.pipelineEditor.setForm.copyFromFieldHelpText"
defaultMessage="Field to copy into {field}."
values={{
field: <EuiCode>{'Field'}</EuiCode>,
}}
/>
),
},
labelAppend: (
<EuiText size="xs">
<EuiLink onClick={toggleCustom} data-test-subj="toggleCustomField">
<FormattedMessage
id="xpack.ingestPipelines.pipelineEditor.useValueLabel"
defaultMessage="Use value field"
/>
</EuiLink>
</EuiText>
},
],
label: i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.copyFromFieldLabel', {
defaultMessage: 'Copy from',
}),
helpText: (
<FormattedMessage
id="xpack.ingestPipelines.pipelineEditor.setForm.copyFromFieldHelpText"
defaultMessage="Field to copy into {field}."
values={{
field: <EuiCode>{'Field'}</EuiCode>,
}}
/>
),
key: 'copy_from',
},
});
toggle_custom_field: {
type: FIELD_TYPES.TOGGLE,
label: i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.enablingCopyFieldLabel', {
defaultMessage: 'Use Copy instead of Value',
}),
fieldsToValidateOnChange: ['fields.value', 'fields.copy_from'],
helpText: (
<FormattedMessage
id="xpack.ingestPipelines.pipelineEditor.setForm.enablingCopydHelpText"
defaultMessage="Define fields to copy into {field} insted of defining {value}."
values={{
field: <EuiCode>{'Field'}</EuiCode>,
value: <EuiCode>{'Value'}</EuiCode>,
}}
/>
),
},
};
/**
* Disambiguate name from the Set data structure
*/
export const SetProcessor: FunctionComponent = () => {
const { getFieldDefaultValue } = useFormContext();
const [{ fields }] = useFormData({ watch: ['fields.value', 'fields.copy_from'] });
const { getFieldDefaultValue, setFieldValue } = useFormContext();
const [{ fields }] = useFormData({
watch: ['fields.value', 'fields.copy_from'],
});
const isCopyFromDefined = getFieldDefaultValue('fields.copy_from') !== undefined;
const [isCopyFromEnabled, setIsCopyFrom] = useState<boolean>(isCopyFromDefined);
const [isDefineAsJson, setIsDefineAsJson] = useState<boolean | undefined>(undefined);
const getIsJsonValue = (isJson: boolean) => {
setIsDefineAsJson(isJson);
};
const toggleCustom = useCallback(() => {
const newIsCopyFrom = !isCopyFromEnabled;
setIsCopyFrom((prev) => !prev);
}, []);
const valueFieldProps = useMemo(
() =>
isCopyFromEnabled
? getValueConfig(toggleCustom).copy_from
: getValueConfig(toggleCustom).value,
[isCopyFromEnabled, toggleCustom]
);
setFieldValue('fields.value', !newIsCopyFrom && isDefineAsJson ? '{}' : '');
setFieldValue('fields.copy_from', '');
}, [isCopyFromEnabled, isDefineAsJson, setFieldValue]);
return (
<>
@ -231,7 +218,17 @@ export const SetProcessor: FunctionComponent = () => {
})}
/>
<UseField {...valueFieldProps} component={Field} />
<UseField
config={fieldsConfig.value}
component={XJsonToggle}
path="fields.value"
componentProps={{
disabled: isCopyFromEnabled,
handleIsJson: getIsJsonValue,
fieldType: 'text',
}}
validationData={isDefineAsJson}
/>
{hasTemplateSnippet(fields?.value) && (
<UseField
@ -260,6 +257,24 @@ export const SetProcessor: FunctionComponent = () => {
/>
)}
<UseField
config={fieldsConfig.toggle_custom_field}
component={ToggleField}
data-test-subj="toggleCustomField"
onChange={toggleCustom}
defaultValue={isCopyFromEnabled}
path=""
/>
{isCopyFromEnabled && (
<UseField
data-test-subj="copyFromInput"
config={fieldsConfig.copy_from}
component={Field}
path="fields.copy_from"
/>
)}
<UseField
config={fieldsConfig.override}
component={ToggleField}

View file

@ -11,7 +11,13 @@ import { i18n } from '@kbn/i18n';
import { isRight } from 'fp-ts/lib/Either';
import { ERROR_CODE } from '@kbn/es-ui-shared-plugin/static/forms/helpers/field_validators/types';
import { FieldConfig, ValidationFunc, fieldValidators } from '../../../../../../shared_imports';
import { isPlainObject } from 'lodash';
import {
FieldConfig,
ValidationFunc,
fieldValidators,
isJSON,
} from '../../../../../../shared_imports';
import { collapseEscapedStrings } from '../../../utils';
const { emptyField, isJsonField } = fieldValidators;
@ -204,3 +210,10 @@ export type FieldsConfig = Record<string, FieldConfig<any>>;
export type FormFieldsComponent = FunctionComponent<{
initialFieldValues?: Record<string, any>;
}>;
export const isXJsonValue = (value: any) => {
if (typeof value === 'string') {
return isJSON(collapseEscapedStrings(value));
}
return isPlainObject(value);
};

View file

@ -154,6 +154,7 @@ const fieldToConvertToJson = [
'params',
'pattern_definitions',
'processor',
'value',
];
export const convertProccesorsToJson = (obj: { [key: string]: any }): { [key: string]: any } => {