mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Ingest pipelines] Add support for URI parts processor (#86163)
This commit is contained in:
parent
c733233e73
commit
2b98dc693c
7 changed files with 362 additions and 0 deletions
|
@ -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) => (
|
||||
<input
|
||||
data-test-subj={props['data-test-subj'] || 'mockCodeEditor'}
|
||||
data-currentvalue={props.value}
|
||||
onChange={(e: 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) => (
|
||||
<input
|
||||
data-test-subj={props['data-test-subj']}
|
||||
data-currentvalue={props.selectedOptions}
|
||||
onChange={async (syntheticEvent: any) => {
|
||||
props.onChange([syntheticEvent['0']]);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('react-virtualized', () => {
|
||||
const original = jest.requireActual('react-virtualized');
|
||||
|
||||
return {
|
||||
...original,
|
||||
AutoSizer: ({ children }: { children: any }) => (
|
||||
<div>{children({ height: 500, width: 500 })}</div>
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
const testBedSetup = registerTestBed<TestSubject>(
|
||||
(props: Props) => <ProcessorsEditorWithDeps {...props} />,
|
||||
{
|
||||
doMountAsync: false,
|
||||
}
|
||||
);
|
||||
|
||||
export interface SetupResult extends TestBed<TestSubject> {
|
||||
actions: ReturnType<typeof createActions>;
|
||||
}
|
||||
|
||||
const createActions = (testBed: TestBed<TestSubject>) => {
|
||||
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<SetupResult> => {
|
||||
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';
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -54,5 +54,6 @@ export const FieldNameField: FunctionComponent<Props> = ({ helpText, additionalV
|
|||
}}
|
||||
component={Field}
|
||||
path="fields.field"
|
||||
data-test-subj="fieldNameField"
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -40,6 +40,7 @@ export const TargetField: FunctionComponent<Props> = (props) => {
|
|||
}}
|
||||
component={Field}
|
||||
path={TARGET_FIELD_PATH}
|
||||
data-test-subj="targetField"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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: (
|
||||
<FormattedMessage
|
||||
id="xpack.ingestPipelines.pipelineEditor.commonFields.keepOriginalFieldHelpText"
|
||||
defaultMessage="Copy the unparsed URI to {field}."
|
||||
values={{
|
||||
field: <EuiCode>{'<target_field>.original'}</EuiCode>,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
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: (
|
||||
<FormattedMessage
|
||||
id="xpack.ingestPipelines.pipelineEditor.commonFields.removeIfSuccessfulFieldHelpText"
|
||||
defaultMessage="Remove the field after parsing the URI string."
|
||||
/>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export const UriParts: FunctionComponent = () => {
|
||||
return (
|
||||
<>
|
||||
<FieldNameField
|
||||
helpText={i18n.translate(
|
||||
'xpack.ingestPipelines.pipelineEditor.uriPartsForm.fieldNameHelpText',
|
||||
{ defaultMessage: 'Field containing URI string.' }
|
||||
)}
|
||||
/>
|
||||
|
||||
<TargetField />
|
||||
|
||||
<UseField
|
||||
config={fieldsConfig.keep_original}
|
||||
component={ToggleField}
|
||||
path="fields.keep_original"
|
||||
data-test-subj="keepOriginalField"
|
||||
/>
|
||||
|
||||
<UseField
|
||||
config={fieldsConfig.remove_if_successful}
|
||||
component={ToggleField}
|
||||
path="fields.remove_if_successful"
|
||||
data-test-subj="removeIfSuccessfulField"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue