Skip to content

Commit

Permalink
[Ingest pipelines] Add copy_from to set processor (#104070) (#106953)
Browse files Browse the repository at this point in the history
* fix up validation conditions for fields

* add tests and fix prettier errors

* Small refactor and fix tests

* Fix copy surrounding error handling

* Clean up unnecessary boilerplate from tests

* Fix i18n error

* Keep optional select next to bound input

* Pass disabled prop as boolean

* fix test matchers

* No need to whitelist fields anymore

* Small CR changes

* Convert optional inputs to toggle state

* Fix testcase copy

* address CR suggestions

* address CR changes

* Fix i18n

* Fix labelAppend link alignment
# Conflicts:
#	x-pack/plugins/translations/translations/ja-JP.json
#	x-pack/plugins/translations/translations/zh-CN.json
  • Loading branch information
sabarasaba committed Jul 28, 2021
1 parent d796576 commit 220ee16
Show file tree
Hide file tree
Showing 8 changed files with 209 additions and 90 deletions.
Expand Up @@ -172,8 +172,6 @@ type TestSubject =
| 'valueFieldInput'
| 'mediaTypeSelectorField'
| 'networkDirectionField.input'
| 'sourceIpField.input'
| 'destinationIpField.input'
| 'toggleCustomField'
| 'ignoreEmptyField.input'
| 'overrideField.input'
Expand All @@ -189,4 +187,5 @@ type TestSubject =
| 'ianaField.input'
| 'transportField.input'
| 'seedField.input'
| 'copyFromInput'
| 'trimSwitch.input';
Expand Up @@ -8,19 +8,6 @@
import { act } from 'react-dom/test-utils';
import { setup, SetupResult, getProcessorValue } from './processor.helpers';

// Default parameter values automatically added to the set processor when saved
const defaultSetParameters = {
value: '',
if: undefined,
tag: undefined,
override: undefined,
media_type: undefined,
description: undefined,
ignore_missing: undefined,
ignore_failure: undefined,
ignore_empty_value: undefined,
};

const SET_TYPE = 'set';

describe('Processor: Set', () => {
Expand Down Expand Up @@ -66,7 +53,10 @@ describe('Processor: Set', () => {
await saveNewProcessor();

// Expect form error as "field" is required parameter
expect(form.getErrorsMessages()).toEqual(['A field value is required.']);
expect(form.getErrorsMessages()).toEqual([
'A field value is required.',
'A value is required.',
]);
});

test('saves with default parameter value', async () => {
Expand All @@ -75,15 +65,43 @@ describe('Processor: Set', () => {
form,
} = testBed;

// Add "field" value (required)
// Add required fields
form.setInputValue('valueFieldInput', 'value');
form.setInputValue('fieldNameField.input', 'field_1');
// Save the field
await saveNewProcessor();

const processors = getProcessorValue(onUpdate, SET_TYPE);
expect(processors[0][SET_TYPE]).toEqual({
...defaultSetParameters,
field: 'field_1',
value: 'value',
});
});

test('allows to save the the copy_from value', async () => {
const {
actions: { saveNewProcessor },
form,
find,
} = testBed;

// Add required fields
form.setInputValue('fieldNameField.input', 'field_1');

// Set value field
form.setInputValue('valueFieldInput', 'value');

// Toggle to copy_from field and set a random value
find('toggleCustomField').simulate('click');
form.setInputValue('copyFromInput', 'copy_from');

// Save the field with new changes
await saveNewProcessor();

const processors = getProcessorValue(onUpdate, SET_TYPE);
expect(processors[0][SET_TYPE]).toEqual({
field: 'field_1',
copy_from: 'copy_from',
});
});

Expand All @@ -110,7 +128,6 @@ describe('Processor: Set', () => {

const processors = getProcessorValue(onUpdate, SET_TYPE);
expect(processors[0][SET_TYPE]).toEqual({
...defaultSetParameters,
field: 'field_1',
value: '{{{hello}}}',
media_type: 'text/plain',
Expand All @@ -136,7 +153,6 @@ describe('Processor: Set', () => {

const processors = getProcessorValue(onUpdate, SET_TYPE);
expect(processors[0][SET_TYPE]).toEqual({
...defaultSetParameters,
field: 'field_1',
value: '{{{hello}}}',
ignore_empty_value: true,
Expand Down
Expand Up @@ -9,7 +9,7 @@ import React, { FunctionComponent, useState, useCallback, useMemo } from 'react'
import { i18n } from '@kbn/i18n';
import { isEmpty } from 'lodash';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiButtonEmpty, EuiCode } from '@elastic/eui';
import { EuiCode, EuiLink, EuiText } from '@elastic/eui';

import {
FIELD_TYPES,
Expand Down Expand Up @@ -131,11 +131,14 @@ const getInternalNetworkConfig: (
),
},
labelAppend: (
<EuiButtonEmpty size="xs" onClick={toggleCustom} data-test-subj="toggleCustomField">
{i18n.translate('xpack.ingestPipelines.pipelineEditor.internalNetworkCustomLabel', {
defaultMessage: 'Use custom field',
})}
</EuiButtonEmpty>
<EuiText size="xs">
<EuiLink onClick={toggleCustom} data-test-subj="toggleCustomField">
<FormattedMessage
id="xpack.ingestPipelines.pipelineEditor.internalNetworkCustomLabel"
defaultMessage="Use custom field"
/>
</EuiLink>
</EuiText>
),
key: 'preset',
},
Expand Down Expand Up @@ -171,11 +174,14 @@ const getInternalNetworkConfig: (
),
},
labelAppend: (
<EuiButtonEmpty size="xs" onClick={toggleCustom} data-test-subj="toggleCustomField">
{i18n.translate('xpack.ingestPipelines.pipelineEditor.internalNetworkPredefinedLabel', {
defaultMessage: 'Use preset field',
})}
</EuiButtonEmpty>
<EuiText size="xs">
<EuiLink onClick={toggleCustom} data-test-subj="toggleCustomField">
<FormattedMessage
id="xpack.ingestPipelines.pipelineEditor.internalNetworkPredefinedLabel"
defaultMessage="Use preset field"
/>
</EuiLink>
</EuiText>
),
key: 'custom',
},
Expand Down
Expand Up @@ -5,10 +5,11 @@
* 2.0.
*/

import React, { FunctionComponent } from 'react';
import React, { FunctionComponent, useState, useCallback, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { isEmpty } from 'lodash';
import { EuiCode, EuiLink, EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiCode } from '@elastic/eui';

import {
FIELD_TYPES,
Expand All @@ -17,32 +18,27 @@ import {
ToggleField,
UseField,
Field,
FieldHook,
FieldConfig,
useFormContext,
} from '../../../../../../shared_imports';
import { hasTemplateSnippet } from '../../../utils';

import { FieldsConfig, to, from } 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]>;
};

// Optional fields config
const fieldsConfig: FieldsConfig = {
/* Required fields config */
// This is a required field, but we exclude validation because we accept empty values as ''
value: {
type: FIELD_TYPES.TEXT,
deserializer: String,
label: i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.valueFieldLabel', {
defaultMessage: 'Value',
}),
helpText: (
<FormattedMessage
id="xpack.ingestPipelines.pipelineEditor.setForm.valueFieldHelpText"
defaultMessage="Value for the field. A blank value will set {emptyString}."
values={{
emptyString: <EuiCode>{'""'}</EuiCode>,
}}
/>
),
},
mediaType: {
type: FIELD_TYPES.SELECT,
defaultValue: 'application/json',
Expand All @@ -57,7 +53,6 @@ const fieldsConfig: FieldsConfig = {
/>
),
},
/* Optional fields config */
override: {
type: FIELD_TYPES.TOGGLE,
defaultValue: true,
Expand Down Expand Up @@ -101,11 +96,134 @@ 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',
},
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.',
}),
};
}
},
},
],
},
labelAppend: (
<EuiText size="xs">
<EuiLink onClick={toggleCustom} data-test-subj="toggleCustomField">
<FormattedMessage
id="xpack.ingestPipelines.pipelineEditor.useCopyFromLabel"
defaultMessage="Use copy from field"
/>
</EuiLink>
</EuiText>
),
key: 'value',
},
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.',
}),
};
}
},
},
],
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>
),
key: 'copy_from',
},
});

/**
* Disambiguate name from the Set data structure
*/
export const SetProcessor: FunctionComponent = () => {
const [{ fields }] = useFormData({ watch: 'fields.value' });
const { getFieldDefaultValue } = useFormContext();
const [{ fields }] = useFormData({ watch: ['fields.value', 'fields.copy_from'] });

const isCopyFromDefined = getFieldDefaultValue('fields.copy_from') !== undefined;
const [isCopyFromEnabled, setIsCopyFrom] = useState<boolean>(isCopyFromDefined);

const toggleCustom = useCallback(() => {
setIsCopyFrom((prev) => !prev);
}, []);

const valueFieldProps = useMemo(
() =>
isCopyFromEnabled
? getValueConfig(toggleCustom).copy_from
: getValueConfig(toggleCustom).value,
[isCopyFromEnabled, toggleCustom]
);

return (
<>
Expand All @@ -115,16 +233,7 @@ export const SetProcessor: FunctionComponent = () => {
})}
/>

<UseField
config={fieldsConfig.value}
component={Field}
componentProps={{
euiFieldProps: {
'data-test-subj': 'valueFieldInput',
},
}}
path="fields.value"
/>
<UseField {...valueFieldProps} component={Field} />

{hasTemplateSnippet(fields?.value) && (
<UseField
Expand Down

0 comments on commit 220ee16

Please sign in to comment.