mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
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:
parent
fd63ee5e28
commit
59b229280d
15 changed files with 453 additions and 164 deletions
|
@ -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",
|
||||
|
|
|
@ -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": "エラー",
|
||||
|
|
|
@ -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": "错误",
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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={{
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -154,6 +154,7 @@ const fieldToConvertToJson = [
|
|||
'params',
|
||||
'pattern_definitions',
|
||||
'processor',
|
||||
'value',
|
||||
];
|
||||
|
||||
export const convertProccesorsToJson = (obj: { [key: string]: any }): { [key: string]: any } => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue