From 21357e4733ba7e4cdf8280f0ffbc5e6a6bd9837f Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Thu, 17 Dec 2020 17:38:54 -0500 Subject: [PATCH] [Ingest pipelines] Add support for URI parts processor (#86163) (#86379) --- .../__jest__/processors/processor.helpers.tsx | 136 ++++++++++++++++++ .../__jest__/processors/uri_parts.test.tsx | 123 ++++++++++++++++ .../common_fields/field_name_field.tsx | 1 + .../processors/common_fields/target_field.tsx | 1 + .../processor_form/processors/index.ts | 1 + .../processor_form/processors/uri_parts.tsx | 88 ++++++++++++ .../shared/map_processor_type_to_form.tsx | 12 ++ 7 files changed, 362 insertions(+) create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/processors/processor.helpers.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/processors/uri_parts.test.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/uri_parts.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/processors/processor.helpers.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/processors/processor.helpers.tsx new file mode 100644 index 0000000000000..3585e7357b443 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/processors/processor.helpers.tsx @@ -0,0 +1,136 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { act } from 'react-dom/test-utils'; +import React from 'react'; +import axios from 'axios'; +import axiosXhrAdapter from 'axios/lib/adapters/xhr'; + +/* eslint-disable @kbn/eslint/no-restricted-paths */ +import { usageCollectionPluginMock } from 'src/plugins/usage_collection/public/mocks'; + +import { registerTestBed, TestBed } from '@kbn/test/jest'; +import { stubWebWorker } from '@kbn/test/jest'; +import { uiMetricService, apiService } from '../../../../services'; +import { Props } from '../../'; +import { initHttpRequests } from '../http_requests.helpers'; +import { ProcessorsEditorWithDeps } from '../processors_editor'; + +stubWebWorker(); + +jest.mock('../../../../../../../../../src/plugins/kibana_react/public', () => { + const original = jest.requireActual('../../../../../../../../../src/plugins/kibana_react/public'); + return { + ...original, + // Mocking CodeEditor, which uses React Monaco under the hood + CodeEditor: (props: any) => ( + { + props.onChange(e.jsonContent); + }} + /> + ), + }; +}); + +jest.mock('@elastic/eui', () => { + const original = jest.requireActual('@elastic/eui'); + return { + ...original, + // Mocking EuiComboBox, as it utilizes "react-virtualized" for rendering search suggestions, + // which does not produce a valid component wrapper + EuiComboBox: (props: any) => ( + { + props.onChange([syntheticEvent['0']]); + }} + /> + ), + }; +}); + +jest.mock('react-virtualized', () => { + const original = jest.requireActual('react-virtualized'); + + return { + ...original, + AutoSizer: ({ children }: { children: any }) => ( +
{children({ height: 500, width: 500 })}
+ ), + }; +}); + +const testBedSetup = registerTestBed( + (props: Props) => , + { + doMountAsync: false, + } +); + +export interface SetupResult extends TestBed { + actions: ReturnType; +} + +const createActions = (testBed: TestBed) => { + const { find, component } = testBed; + + return { + async saveNewProcessor() { + await act(async () => { + find('addProcessorForm.submitButton').simulate('click'); + }); + component.update(); + }, + + async addProcessorType({ type, label }: { type: string; label: string }) { + await act(async () => { + find('processorTypeSelector.input').simulate('change', [{ value: type, label }]); + }); + component.update(); + }, + + addProcessor() { + find('addProcessorButton').simulate('click'); + }, + }; +}; + +export const setup = async (props: Props): Promise => { + const testBed = await testBedSetup(props); + return { + ...testBed, + actions: createActions(testBed), + }; +}; + +const mockHttpClient = axios.create({ adapter: axiosXhrAdapter }); + +export const setupEnvironment = () => { + // Initialize mock services + uiMetricService.setup(usageCollectionPluginMock.createSetupContract()); + // @ts-ignore + apiService.setup(mockHttpClient, uiMetricService); + + const { server, httpRequestsMockHelpers } = initHttpRequests(); + + return { + server, + httpRequestsMockHelpers, + }; +}; + +type TestSubject = + | 'addProcessorForm.submitButton' + | 'addProcessorButton' + | 'addProcessorForm.submitButton' + | 'processorTypeSelector.input' + | 'fieldNameField.input' + | 'targetField.input' + | 'keepOriginalField.input' + | 'removeIfSuccessfulField.input'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/processors/uri_parts.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/processors/uri_parts.test.tsx new file mode 100644 index 0000000000000..95ec4e23ed9c3 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/processors/uri_parts.test.tsx @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { act } from 'react-dom/test-utils'; +import { setup, SetupResult } from './processor.helpers'; + +// Default parameter values automatically added to the URI parts processor when saved +const defaultUriPartsParameters = { + keep_original: undefined, + remove_if_successful: undefined, + ignore_failure: undefined, + description: undefined, +}; + +describe('Processor: URI parts', () => { + 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(); + }); + + test('prevents form submission if required fields are not provided', async () => { + const { + actions: { addProcessor, saveNewProcessor, addProcessorType }, + form, + } = testBed; + + // Open flyout to add new processor + addProcessor(); + // Click submit button without entering any fields + await saveNewProcessor(); + + // Expect form error as a processor type is required + expect(form.getErrorsMessages()).toEqual(['A type is required.']); + + // Add type (the other fields are not visible until a type is selected) + await addProcessorType({ type: 'uri_parts', label: 'URI parts' }); + + // 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 values', async () => { + const { + actions: { addProcessor, saveNewProcessor, addProcessorType }, + form, + } = testBed; + + // Open flyout to add new processor + addProcessor(); + // Add type (the other fields are not visible until a type is selected) + await addProcessorType({ type: 'uri_parts', label: 'URI parts' }); + // Add "field" value (required) + form.setInputValue('fieldNameField.input', 'field_1'); + // Save the field + await saveNewProcessor(); + + const [onUpdateResult] = onUpdate.mock.calls[onUpdate.mock.calls.length - 1]; + const { processors } = onUpdateResult.getData(); + expect(processors[0].uri_parts).toEqual({ + field: 'field_1', + ...defaultUriPartsParameters, + }); + }); + + test('allows optional parameters to be set', async () => { + const { + actions: { addProcessor, addProcessorType, saveNewProcessor }, + form, + } = testBed; + + // Open flyout to add new processor + addProcessor(); + // Add type (the other fields are not visible until a type is selected) + await addProcessorType({ type: 'uri_parts', label: 'URI parts' }); + // Add "field" value (required) + form.setInputValue('fieldNameField.input', 'field_1'); + + // Set optional parameteres + form.setInputValue('targetField.input', 'target_field'); + form.toggleEuiSwitch('keepOriginalField.input'); + form.toggleEuiSwitch('removeIfSuccessfulField.input'); + + // Save the field with new changes + await saveNewProcessor(); + + const [onUpdateResult] = onUpdate.mock.calls[onUpdate.mock.calls.length - 1]; + const { processors } = onUpdateResult.getData(); + expect(processors[0].uri_parts).toEqual({ + description: undefined, + field: 'field_1', + ignore_failure: undefined, + keep_original: false, + remove_if_successful: true, + target_field: 'target_field', + }); + }); +}); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/common_fields/field_name_field.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/common_fields/field_name_field.tsx index 7ef5ba6768c19..965def7f5e340 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/common_fields/field_name_field.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/common_fields/field_name_field.tsx @@ -54,5 +54,6 @@ export const FieldNameField: FunctionComponent = ({ helpText, additionalV }} component={Field} path="fields.field" + data-test-subj="fieldNameField" /> ); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/common_fields/target_field.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/common_fields/target_field.tsx index 69ce01777b616..44d727ee179eb 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/common_fields/target_field.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/common_fields/target_field.tsx @@ -40,6 +40,7 @@ export const TargetField: FunctionComponent = (props) => { }} component={Field} path={TARGET_FIELD_PATH} + data-test-subj="targetField" /> ); }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/index.ts index e211d682ab0f0..8732577bd833e 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/index.ts @@ -38,5 +38,6 @@ export { Trim } from './trim'; export { Uppercase } from './uppercase'; export { UrlDecode } from './url_decode'; export { UserAgent } from './user_agent'; +export { UriParts } from './uri_parts'; export { FormFieldsComponent } from './shared'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/uri_parts.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/uri_parts.tsx new file mode 100644 index 0000000000000..c380b0c18ee9d --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/uri_parts.tsx @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiCode } from '@elastic/eui'; + +import { FIELD_TYPES, UseField, ToggleField } from '../../../../../../shared_imports'; + +import { FieldsConfig, to, from } from './shared'; + +import { FieldNameField } from './common_fields/field_name_field'; +import { TargetField } from './common_fields/target_field'; + +export const fieldsConfig: FieldsConfig = { + keep_original: { + type: FIELD_TYPES.TOGGLE, + defaultValue: true, + deserializer: to.booleanOrUndef, + serializer: from.undefinedIfValue(true), + label: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.commonFields.keepOriginalFieldLabel', + { + defaultMessage: 'Keep original', + } + ), + helpText: ( + {'.original'}, + }} + /> + ), + }, + remove_if_successful: { + type: FIELD_TYPES.TOGGLE, + defaultValue: false, + deserializer: to.booleanOrUndef, + serializer: from.undefinedIfValue(false), + label: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.commonFields.removeIfSuccessfulFieldLabel', + { + defaultMessage: 'Remove if successful', + } + ), + helpText: ( + + ), + }, +}; + +export const UriParts: FunctionComponent = () => { + return ( + <> + + + + + + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/map_processor_type_to_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/map_processor_type_to_form.tsx index 4c98940b0138e..0678997552154 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/map_processor_type_to_form.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/map_processor_type_to_form.tsx @@ -45,6 +45,7 @@ import { UrlDecode, UserAgent, FormFieldsComponent, + UriParts, } from '../processor_form/processors'; interface FieldDescriptor { @@ -438,6 +439,17 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = { defaultMessage: "Extracts values from a browser's user agent string.", }), }, + uri_parts: { + FieldsComponent: UriParts, + docLinkPath: '/uri-parts-processor.html', + label: i18n.translate('xpack.ingestPipelines.processors.label.uriPartsLabel', { + defaultMessage: 'URI parts', + }), + description: i18n.translate('xpack.ingestPipelines.processors.uriPartsDescription', { + defaultMessage: + 'Parses a Uniform Resource Identifier (URI) string and extracts its components as an object.', + }), + }, }; export type ProcessorType = keyof typeof mapProcessorTypeToDescriptor;