diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx
index 9dd0d6cc72de..c00f09b2d2b0 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx
@@ -154,6 +154,10 @@ type TestSubject =
| 'separatorValueField.input'
| 'quoteValueField.input'
| 'emptyValueField.input'
+ | 'valueFieldInput'
+ | 'mediaTypeSelectorField'
+ | 'ignoreEmptyField.input'
+ | 'overrideField.input'
| 'fieldsValueField.input'
| 'saltValueField.input'
| 'methodsValueField'
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/set.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/set.test.tsx
new file mode 100644
index 000000000000..d7351c9dbf65
--- /dev/null
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/set.test.tsx
@@ -0,0 +1,146 @@
+/*
+ * 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 { 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', () => {
+ let onUpdate: jest.Mock;
+ let testBed: SetupResult;
+
+ beforeAll(() => {
+ jest.useFakeTimers();
+ });
+
+ afterAll(() => {
+ jest.useRealTimers();
+ });
+
+ beforeEach(async () => {
+ onUpdate = jest.fn();
+
+ await act(async () => {
+ testBed = await setup({
+ value: {
+ processors: [],
+ },
+ onFlyoutOpen: jest.fn(),
+ onUpdate,
+ });
+ });
+
+ testBed.component.update();
+
+ // Open flyout to add new processor
+ testBed.actions.addProcessor();
+ // Add type (the other fields are not visible until a type is selected)
+ await testBed.actions.addProcessorType(SET_TYPE);
+ });
+
+ test('prevents form submission if required fields are not provided', async () => {
+ const {
+ actions: { saveNewProcessor },
+ form,
+ } = testBed;
+
+ // Click submit button with only the type defined
+ await saveNewProcessor();
+
+ // Expect form error as "field" is required parameter
+ expect(form.getErrorsMessages()).toEqual(['A field value is required.']);
+ });
+
+ test('saves with default parameter value', async () => {
+ const {
+ actions: { saveNewProcessor },
+ form,
+ } = testBed;
+
+ // Add "field" value (required)
+ 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',
+ });
+ });
+
+ test('should allow to set mediaType when value is a template snippet', async () => {
+ const {
+ actions: { saveNewProcessor },
+ form,
+ exists,
+ } = testBed;
+
+ // Add "field" value (required)
+ form.setInputValue('fieldNameField.input', 'field_1');
+
+ // Shouldnt be able to set mediaType if value is not a template string
+ form.setInputValue('valueFieldInput', '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.setSelectValue('mediaTypeSelectorField', 'text/plain');
+
+ // Save the field with new changes
+ await saveNewProcessor();
+
+ const processors = getProcessorValue(onUpdate, SET_TYPE);
+ expect(processors[0][SET_TYPE]).toEqual({
+ ...defaultSetParameters,
+ field: 'field_1',
+ value: '{{{hello}}}',
+ media_type: 'text/plain',
+ });
+ });
+
+ test('allows optional parameters to be set', async () => {
+ const {
+ actions: { saveNewProcessor },
+ form,
+ } = testBed;
+
+ // Add "field" value (required)
+ form.setInputValue('fieldNameField.input', 'field_1');
+
+ // Set optional parameteres
+ form.setInputValue('valueFieldInput', '{{{hello}}}');
+ form.toggleEuiSwitch('overrideField.input');
+ form.toggleEuiSwitch('ignoreEmptyField.input');
+
+ // Save the field with new changes
+ await saveNewProcessor();
+
+ const processors = getProcessorValue(onUpdate, SET_TYPE);
+ expect(processors[0][SET_TYPE]).toEqual({
+ ...defaultSetParameters,
+ field: 'field_1',
+ value: '{{{hello}}}',
+ ignore_empty_value: true,
+ override: false,
+ });
+ });
+});
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/set.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/set.tsx
index 89ca373b9e65..fda34f8700b3 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/set.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/set.tsx
@@ -10,7 +10,15 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiCode } from '@elastic/eui';
-import { FIELD_TYPES, ToggleField, UseField, Field } from '../../../../../../shared_imports';
+import {
+ FIELD_TYPES,
+ useFormData,
+ SelectField,
+ ToggleField,
+ UseField,
+ Field,
+} from '../../../../../../shared_imports';
+import { hasTemplateSnippet } from '../../../utils';
import { FieldsConfig, to, from } from './shared';
@@ -35,6 +43,20 @@ const fieldsConfig: FieldsConfig = {
/>
),
},
+ mediaType: {
+ type: FIELD_TYPES.SELECT,
+ defaultValue: 'application/json',
+ serializer: from.undefinedIfValue('application/json'),
+ label: i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.mediaTypeFieldLabel', {
+ defaultMessage: 'Media Type',
+ }),
+ helpText: (
+
+ ),
+ },
/* Optional fields config */
override: {
type: FIELD_TYPES.TOGGLE,
@@ -83,6 +105,8 @@ const fieldsConfig: FieldsConfig = {
* Disambiguate name from the Set data structure
*/
export const SetProcessor: FunctionComponent = () => {
+ const [{ fields }] = useFormData({ watch: 'fields.value' });
+
return (
<>
{
path="fields.value"
/>
-
+ {hasTemplateSnippet(fields?.value) && (
+
+ )}
+
+
>
);
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/utils.test.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/utils.test.ts
index 6f21285398e1..6e367a83bf8d 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/utils.test.ts
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/utils.test.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { getValue, setValue } from './utils';
+import { getValue, setValue, hasTemplateSnippet } from './utils';
describe('get and set values', () => {
const testObject = Object.freeze([{ onFailure: [{ onFailure: 1 }] }]);
@@ -35,3 +35,19 @@ describe('get and set values', () => {
});
});
});
+
+describe('template snippets', () => {
+ it('knows when a string contains an invalid template snippet', () => {
+ expect(hasTemplateSnippet('')).toBe(false);
+ expect(hasTemplateSnippet('{}')).toBe(false);
+ expect(hasTemplateSnippet('{{{}}}')).toBe(false);
+ expect(hasTemplateSnippet('{{hello}}')).toBe(false);
+ });
+
+ it('knows when a string contains a valid template snippet', () => {
+ expect(hasTemplateSnippet('{{{hello}}}')).toBe(true);
+ expect(hasTemplateSnippet('hello{{{world}}}')).toBe(true);
+ expect(hasTemplateSnippet('{{{hello}}}world')).toBe(true);
+ expect(hasTemplateSnippet('{{{hello.world}}}')).toBe(true);
+ });
+});
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/utils.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/utils.ts
index e07b9eba9062..1259dbd5a9b9 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/utils.ts
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/utils.ts
@@ -102,3 +102,20 @@ export const checkIfSamePath = (pathA: ProcessorSelector, pathB: ProcessorSelect
if (pathA.length !== pathB.length) return false;
return pathA.join('.') === pathB.join('.');
};
+
+/*
+ * Given a string it checks if it contains a valid mustache template snippet.
+ *
+ * Note: This allows strings with spaces such as: {{{hello world}}}. I figured we
+ * should use .+ instead of \S (disallow all whitespaces) because the backend seems
+ * to allow spaces inside the template snippet anyway.
+ *
+ * See: https://www.elastic.co/guide/en/elasticsearch/reference/master/ingest.html#template-snippets
+ */
+export const hasTemplateSnippet = (str: string = '') => {
+ // Matches when:
+ // * contains a {{{
+ // * Followed by all strings of length >= 1
+ // * And followed by }}}
+ return /{{{.+}}}/.test(str);
+};