diff --git a/x-pack/package.json b/x-pack/package.json index 2513a36f50014..d57eaa4cfa5c4 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -330,6 +330,7 @@ "react-syntax-highlighter": "^5.7.0", "react-tiny-virtual-list": "^2.2.0", "react-use": "^13.27.0", + "react-virtualized": "^9.21.2", "react-vis": "^1.8.1", "react-visibility-sensor": "^5.1.1", "recompose": "^0.26.0", diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx index 6acb6369e2e90..4318062491df8 100644 --- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx @@ -68,21 +68,6 @@ describe('', () => { expect(exists('versionField')).toBe(true); }); - test('should toggle the on-failure processors editor', async () => { - const { actions, component, exists } = testBed; - - // On-failure editor should be hidden by default - expect(exists('onFailureEditor')).toBe(false); - - await act(async () => { - actions.toggleOnFailureSwitch(); - await nextTick(); - component.update(); - }); - - expect(exists('onFailureEditor')).toBe(true); - }); - test('should show the request flyout', async () => { const { actions, component, find, exists } = testBed; diff --git a/x-pack/plugins/ingest_pipelines/common/types.ts b/x-pack/plugins/ingest_pipelines/common/types.ts index 8d77359a7c3c5..ad6d1cc8aa928 100644 --- a/x-pack/plugins/ingest_pipelines/common/types.ts +++ b/x-pack/plugins/ingest_pipelines/common/types.ts @@ -4,10 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -interface Processor { - [key: string]: { - [key: string]: unknown; - }; +export interface ESProcessorConfig { + on_failure?: Processor[]; + ignore_failure?: boolean; + if?: string; + tag?: string; + [key: string]: any; +} + +export interface Processor { + [typeName: string]: ESProcessorConfig; } export interface Pipeline { diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx index 73bc1cfaa2cf5..ec065a74abca0 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState } from 'react'; +import React, { useState, useCallback, useRef } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; @@ -16,6 +16,11 @@ import { PipelineTestFlyout } from './pipeline_test_flyout'; import { PipelineFormFields } from './pipeline_form_fields'; import { PipelineFormError } from './pipeline_form_error'; import { pipelineFormSchema } from './schema'; +import { + OnUpdateHandlerArg, + OnUpdateHandler, + SerializeResult, +} from '../pipeline_processors_editor'; export interface PipelineFormProps { onSave: (pipeline: Pipeline) => void; @@ -30,8 +35,8 @@ export const PipelineForm: React.FunctionComponent = ({ defaultValue = { name: '', description: '', - processors: '', - on_failure: '', + processors: [], + on_failure: [], version: '', }, onSave, @@ -44,10 +49,25 @@ export const PipelineForm: React.FunctionComponent = ({ const [isTestingPipeline, setIsTestingPipeline] = useState(false); + const processorStateRef = useRef(); + const handleSave: FormConfig['onSubmit'] = async (formData, isValid) => { - if (isValid) { - onSave(formData as Pipeline); + let override: SerializeResult | undefined; + + if (!isValid) { + return; + } + + if (processorStateRef.current) { + const processorsState = processorStateRef.current; + if (await processorsState.validate()) { + override = processorsState.getData(); + } else { + return; + } } + + onSave({ ...formData, ...(override || {}) } as Pipeline); }; const handleTestPipelineClick = () => { @@ -60,6 +80,10 @@ export const PipelineForm: React.FunctionComponent = ({ onSubmit: handleSave, }); + const onEditorFlyoutOpen = useCallback(() => { + setIsRequestVisible(false); + }, [setIsRequestVisible]); + const saveButtonLabel = isSaving ? ( = ({ /> ); + const onProcessorsChangeHandler = useCallback( + (arg) => (processorStateRef.current = arg), + [] + ); + return ( <>
= ({ {/* All form fields */} @@ -147,6 +179,9 @@ export const PipelineForm: React.FunctionComponent = ({ {/* ES request flyout */} {isRequestVisible ? ( + processorStateRef.current?.getData() || { processors: [], on_failure: [] } + } closeFlyout={() => setIsRequestVisible((prevIsRequestVisible) => !prevIsRequestVisible)} /> ) : null} @@ -154,6 +189,9 @@ export const PipelineForm: React.FunctionComponent = ({ {/* Test pipeline flyout */} {isTestingPipeline ? ( + processorStateRef.current?.getData() || { processors: [], on_failure: [] } + } closeFlyout={() => { setIsTestingPipeline((prevIsTestingPipeline) => !prevIsTestingPipeline); }} diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx index 9fb5ab55a34ce..a0e7c8fd8bcd7 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form_fields.tsx @@ -6,22 +6,22 @@ import React, { useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; -import { EuiButton, EuiSpacer, EuiSwitch, EuiLink } from '@elastic/eui'; +import { EuiSpacer, EuiSwitch } from '@elastic/eui'; -import { - getUseField, - getFormRow, - Field, - JsonEditorField, - useKibana, -} from '../../../shared_imports'; +import { Processor } from '../../../../common/types'; +import { FormDataProvider } from '../../../shared_imports'; +import { PipelineProcessorsEditor, OnUpdateHandler } from '../pipeline_processors_editor'; + +import { getUseField, getFormRow, Field, useKibana } from '../../../shared_imports'; interface Props { + initialProcessors: Processor[]; + initialOnFailureProcessors?: Processor[]; + onProcessorsUpdate: OnUpdateHandler; hasVersion: boolean; - hasOnFailure: boolean; isTestButtonDisabled: boolean; onTestPipelineClick: () => void; + onEditorFlyoutOpen: () => void; isEditing?: boolean; } @@ -29,16 +29,18 @@ const UseField = getUseField({ component: Field }); const FormRow = getFormRow({ titleTag: 'h3' }); export const PipelineFormFields: React.FunctionComponent = ({ + initialProcessors, + initialOnFailureProcessors, + onProcessorsUpdate, isEditing, hasVersion, - hasOnFailure, isTestButtonDisabled, onTestPipelineClick, + onEditorFlyoutOpen, }) => { const { services } = useKibana(); const [isVersionVisible, setIsVersionVisible] = useState(hasVersion); - const [isOnFailureEditorVisible, setIsOnFailureEditorVisible] = useState(hasOnFailure); return ( <> @@ -110,127 +112,32 @@ export const PipelineFormFields: React.FunctionComponent = ({ /> - {/* Processors field */} - - } - description={ - <> - - {i18n.translate('xpack.ingestPipelines.form.processorsDocumentionLink', { - defaultMessage: 'Learn more', - })} - - ), - }} - /> - - + {/* Pipeline Processors Editor */} + + {({ processors, on_failure: onFailure }) => { + const processorProp = + typeof processors === 'string' && processors + ? JSON.parse(processors) + : initialProcessors ?? []; - - - - - } - > - - + const onFailureProp = + typeof onFailure === 'string' && onFailure + ? JSON.parse(onFailure) + : initialOnFailureProcessors ?? []; - {/* On-failure field */} - - } - description={ - <> - - {i18n.translate('xpack.ingestPipelines.form.onFailureDocumentionLink', { - defaultMessage: 'Learn more', - })} - - ), - }} - /> - - - } - checked={isOnFailureEditorVisible} - onChange={(e) => setIsOnFailureEditorVisible(e.target.checked)} - data-test-subj="onFailureToggle" + return ( + - - } - > - {isOnFailureEditorVisible ? ( - - ) : ( - // requires children or a field - // For now, we return an empty
if the editor is not visible -
- )} - + ); + }} + ); }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/pipeline_request_flyout_provider.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/pipeline_request_flyout_provider.tsx index 6dcedca6085af..dd2439433fc41 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/pipeline_request_flyout_provider.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_request_flyout/pipeline_request_flyout_provider.tsx @@ -4,13 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, FunctionComponent } from 'react'; import { Pipeline } from '../../../../../common/types'; import { useFormContext } from '../../../../shared_imports'; + +import { ReadProcessorsFunction } from '../types'; + import { PipelineRequestFlyout } from './pipeline_request_flyout'; -export const PipelineRequestFlyoutProvider = ({ closeFlyout }: { closeFlyout: () => void }) => { +interface Props { + closeFlyout: () => void; + readProcessors: ReadProcessorsFunction; +} + +export const PipelineRequestFlyoutProvider: FunctionComponent = ({ + closeFlyout, + readProcessors, +}) => { const form = useFormContext(); const [formData, setFormData] = useState({} as Pipeline); @@ -25,5 +36,10 @@ export const PipelineRequestFlyoutProvider = ({ closeFlyout }: { closeFlyout: () return subscription.unsubscribe; }, [form]); - return ; + return ( + + ); }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/pipeline_test_flyout_provider.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/pipeline_test_flyout_provider.tsx index 351478394595a..7f91672d64df4 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/pipeline_test_flyout_provider.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_test_flyout/pipeline_test_flyout_provider.tsx @@ -8,11 +8,19 @@ import React, { useState, useEffect } from 'react'; import { Pipeline } from '../../../../../common/types'; import { useFormContext } from '../../../../shared_imports'; + +import { ReadProcessorsFunction } from '../types'; + import { PipelineTestFlyout, PipelineTestFlyoutProps } from './pipeline_test_flyout'; -type Props = Omit; +interface Props extends Omit { + readProcessors: ReadProcessorsFunction; +} -export const PipelineTestFlyoutProvider: React.FunctionComponent = ({ closeFlyout }) => { +export const PipelineTestFlyoutProvider: React.FunctionComponent = ({ + closeFlyout, + readProcessors, +}) => { const form = useFormContext(); const [formData, setFormData] = useState({} as Pipeline); const [isFormDataValid, setIsFormDataValid] = useState(false); @@ -31,7 +39,7 @@ export const PipelineTestFlyoutProvider: React.FunctionComponent = ({ clo return ( diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/types.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/types.ts new file mode 100644 index 0000000000000..bd74f09546ff4 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/types.ts @@ -0,0 +1,9 @@ +/* + * 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 { Pipeline } from '../../../../common/types'; + +export type ReadProcessorsFunction = () => Pick; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.helpers.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.helpers.ts new file mode 100644 index 0000000000000..acd61a9bbd01e --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.helpers.ts @@ -0,0 +1,36 @@ +/* + * 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 { registerTestBed, TestBed } from '../../../../../../../test_utils'; +import { PipelineProcessorsEditor, Props } from '../pipeline_processors_editor.container'; + +const testBedSetup = registerTestBed(PipelineProcessorsEditor, { + doMountAsync: false, +}); + +export interface SetupResult extends TestBed { + actions: { + toggleOnFailure: () => void; + }; +} + +export const setup = async (props: Props): Promise => { + const testBed = await testBedSetup(props); + const toggleOnFailure = () => { + const { find } = testBed; + find('pipelineEditorOnFailureToggle').simulate('click'); + }; + + return { + ...testBed, + actions: { toggleOnFailure }, + }; +}; + +type TestSubject = + | 'pipelineEditorDoneButton' + | 'pipelineEditorOnFailureToggle' + | 'pipelineEditorOnFailureTree'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.test.tsx new file mode 100644 index 0000000000000..758d6f5e620ce --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.test.tsx @@ -0,0 +1,67 @@ +/* + * 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 { setup } from './pipeline_processors_editor.helpers'; +import { Pipeline } from '../../../../../common/types'; + +const testProcessors: Pick = { + processors: [ + { + script: { + source: 'ctx._type = null', + }, + }, + { + gsub: { + field: '_index', + pattern: '(.monitoring-\\w+-)6(-.+)', + replacement: '$17$2', + }, + }, + ], +}; + +describe('Pipeline Editor', () => { + it('provides the same data out it got in if nothing changes', async () => { + const onUpdate = jest.fn(); + + await setup({ + value: { + ...testProcessors, + }, + onFlyoutOpen: jest.fn(), + onUpdate, + isTestButtonDisabled: false, + onTestPipelineClick: jest.fn(), + learnMoreAboutProcessorsUrl: 'test', + learnMoreAboutOnFailureProcessorsUrl: 'test', + }); + + const { + calls: [[arg]], + } = onUpdate.mock; + + expect(arg.getData()).toEqual(testProcessors); + }); + + it('toggles the on-failure processors', async () => { + const { actions, exists } = await setup({ + value: { + ...testProcessors, + }, + onFlyoutOpen: jest.fn(), + onUpdate: jest.fn(), + isTestButtonDisabled: false, + onTestPipelineClick: jest.fn(), + learnMoreAboutProcessorsUrl: 'test', + learnMoreAboutOnFailureProcessorsUrl: 'test', + }); + + expect(exists('pipelineEditorOnFailureTree')).toBe(false); + actions.toggleOnFailure(); + expect(exists('pipelineEditorOnFailureTree')).toBe(true); + }); +}); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/add_processor_button.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/add_processor_button.tsx new file mode 100644 index 0000000000000..5f9bf87ceca1e --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/add_processor_button.tsx @@ -0,0 +1,32 @@ +/* + * 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 { EuiButtonEmpty } from '@elastic/eui'; +import { usePipelineProcessorsContext } from '../context'; + +export interface Props { + onClick: () => void; +} + +export const AddProcessorButton: FunctionComponent = ({ onClick }) => { + const { + state: { editor }, + } = usePipelineProcessorsContext(); + return ( + + {i18n.translate('xpack.ingestPipelines.pipelineEditor.addProcessorButtonLabel', { + defaultMessage: 'Add a processor', + })} + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/index.ts new file mode 100644 index 0000000000000..cb5d5a10e9f42 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/index.ts @@ -0,0 +1,19 @@ +/* + * 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. + */ + +export { SettingsFormFlyout, OnSubmitHandler } from './settings_form_flyout'; + +export { ProcessorSettingsForm, ProcessorSettingsFromOnSubmitArg } from './processor_settings_form'; + +export { ProcessorsTree, ProcessorInfo, OnActionHandler } from './processors_tree'; + +export { PipelineProcessorsEditorItem } from './pipeline_processors_editor_item/pipeline_processors_editor_item'; + +export { ProcessorRemoveModal } from './processor_remove_modal'; + +export { ProcessorsTitleAndTestButton } from './processors_title_and_test_button'; + +export { OnFailureProcessorsTitle } from './on_failure_processors_title'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/on_failure_processors_title.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/on_failure_processors_title.tsx new file mode 100644 index 0000000000000..1c8edac7cfd64 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/on_failure_processors_title.tsx @@ -0,0 +1,52 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiLink, EuiText, EuiTitle } from '@elastic/eui'; +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { usePipelineProcessorsContext } from '../context'; + +export const OnFailureProcessorsTitle: FunctionComponent = () => { + const { links } = usePipelineProcessorsContext(); + return ( + + + +

+ +

+
+ + + {i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.onFailureProcessorsDocumentationLink', + { + defaultMessage: 'Learn more.', + } + )} + + ), + }} + /> + +
+
+ ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/context_menu.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/context_menu.tsx new file mode 100644 index 0000000000000..bc7d6fdcff357 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/context_menu.tsx @@ -0,0 +1,84 @@ +/* + * 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, useState } from 'react'; + +import { EuiContextMenuItem, EuiContextMenuPanel, EuiPopover, EuiButtonIcon } from '@elastic/eui'; + +import { editorItemMessages } from './messages'; + +interface Props { + disabled: boolean; + showAddOnFailure: boolean; + onDuplicate: () => void; + onDelete: () => void; + onAddOnFailure: () => void; +} + +export const ContextMenu: FunctionComponent = ({ + showAddOnFailure, + onDuplicate, + onAddOnFailure, + onDelete, + disabled, +}) => { + const [isOpen, setIsOpen] = useState(false); + + const contextMenuItems = [ + { + setIsOpen(false); + onDuplicate(); + }} + > + {editorItemMessages.duplicateButtonLabel} + , + showAddOnFailure ? ( + { + setIsOpen(false); + onAddOnFailure(); + }} + > + {editorItemMessages.addOnFailureButtonLabel} + + ) : undefined, + { + setIsOpen(false); + onDelete(); + }} + > + {editorItemMessages.deleteButtonLabel} + , + ].filter(Boolean) as JSX.Element[]; + + return ( + setIsOpen(false)} + button={ + setIsOpen((v) => !v)} + iconType="boxesHorizontal" + aria-label={editorItemMessages.moreButtonAriaLabel} + /> + } + > + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/index.ts new file mode 100644 index 0000000000000..02bafdb326024 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/index.ts @@ -0,0 +1,7 @@ +/* + * 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. + */ + +export { PipelineProcessorsEditorItem, Handlers } from './pipeline_processors_editor_item'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/inline_text_input.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/inline_text_input.tsx new file mode 100644 index 0000000000000..e0b67bc907ca9 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/inline_text_input.tsx @@ -0,0 +1,75 @@ +/* + * 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, useState, useEffect, useCallback } from 'react'; +import { EuiFieldText, EuiText, keyCodes } from '@elastic/eui'; + +export interface Props { + placeholder: string; + ariaLabel: string; + onChange: (value: string) => void; + text?: string; +} + +export const InlineTextInput: FunctionComponent = ({ + placeholder, + text, + ariaLabel, + onChange, +}) => { + const [isShowingTextInput, setIsShowingTextInput] = useState(false); + const [textValue, setTextValue] = useState(text ?? ''); + + const content = isShowingTextInput ? ( + el?.focus()} + onChange={(event) => setTextValue(event.target.value)} + /> + ) : ( + + {text || {placeholder}} + + ); + + const submitChange = useCallback(() => { + setIsShowingTextInput(false); + onChange(textValue); + }, [setIsShowingTextInput, onChange, textValue]); + + useEffect(() => { + const keyboardListener = (event: KeyboardEvent) => { + if (event.keyCode === keyCodes.ESCAPE || event.code === 'Escape') { + setIsShowingTextInput(false); + } + if (event.keyCode === keyCodes.ENTER || event.code === 'Enter') { + submitChange(); + } + }; + if (isShowingTextInput) { + window.addEventListener('keyup', keyboardListener); + } + return () => { + window.removeEventListener('keyup', keyboardListener); + }; + }, [isShowingTextInput, submitChange, setIsShowingTextInput]); + + return ( +
setIsShowingTextInput(true)} + onBlur={submitChange} + > + {content} +
+ ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/messages.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/messages.ts new file mode 100644 index 0000000000000..67dbf2708d665 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/messages.ts @@ -0,0 +1,58 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const editorItemMessages = { + moveButtonLabel: i18n.translate('xpack.ingestPipelines.pipelineEditor.item.moveButtonLabel', { + defaultMessage: 'Move this processor', + }), + editorButtonLabel: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.item.editButtonAriaLabel', + { + defaultMessage: 'Edit this processor', + } + ), + duplicateButtonLabel: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.item.moreMenu.duplicateButtonLabel', + { + defaultMessage: 'Duplicate this processor', + } + ), + addOnFailureButtonLabel: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.item.moreMenu.addOnFailureHandlerButtonLabel', + { + defaultMessage: 'Add on failure handler', + } + ), + cancelMoveButtonLabel: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.item.cancelMoveButtonAriaLabel', + { + defaultMessage: 'Cancel moving this processor', + } + ), + deleteButtonLabel: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.item.moreMenu.deleteButtonLabel', + { + defaultMessage: 'Delete', + } + ), + moreButtonAriaLabel: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.item.moreButtonAriaLabel', + { + defaultMessage: 'Show more actions for this processor', + } + ), + processorTypeLabel: ({ type }: { type: string }) => + i18n.translate('xpack.ingestPipelines.pipelineEditor.item.textInputAriaLabel', { + defaultMessage: 'Provide a description for this {type} processor', + values: { type }, + }), + descriptionPlaceholder: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.item.descriptionPlaceholder', + { defaultMessage: 'No description' } + ), +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.scss b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.scss new file mode 100644 index 0000000000000..a17e644853847 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.scss @@ -0,0 +1,17 @@ +.pipelineProcessorsEditor__item { + &__textContainer { + padding: 4px; + border-radius: 2px; + + transition: border-color .3s; + border: 2px solid #FFF; + + &:hover { + border: 2px solid $euiColorLightShade; + } + } + &__textInput { + height: 21px; + min-width: 100px; + } +} diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.tsx new file mode 100644 index 0000000000000..0e47b3ef7cf88 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.tsx @@ -0,0 +1,145 @@ +/* + * 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, memo } from 'react'; +import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@elastic/eui'; + +import { ProcessorInternal, ProcessorSelector } from '../../types'; + +import { usePipelineProcessorsContext } from '../../context'; + +import './pipeline_processors_editor_item.scss'; + +import { InlineTextInput } from './inline_text_input'; +import { ContextMenu } from './context_menu'; +import { editorItemMessages } from './messages'; + +export interface Handlers { + onMove: () => void; + onCancelMove: () => void; +} + +export interface Props { + processor: ProcessorInternal; + selected: boolean; + handlers: Handlers; + selector: ProcessorSelector; + description?: string; +} + +export const PipelineProcessorsEditorItem: FunctionComponent = memo( + ({ processor, description, handlers: { onCancelMove, onMove }, selector, selected }) => { + const { + state: { editor, processorsDispatch }, + } = usePipelineProcessorsContext(); + + const disabled = editor.mode.id !== 'idle'; + const isDarkBold = + editor.mode.id !== 'editingProcessor' || processor.id === editor.mode.arg.processor.id; + + return ( + + + + + + {processor.type} + + + + { + let nextOptions: Record; + if (!nextDescription) { + const { description: __, ...restOptions } = processor.options; + nextOptions = restOptions; + } else { + nextOptions = { + ...processor.options, + description: nextDescription, + }; + } + processorsDispatch({ + type: 'updateProcessor', + payload: { + processor: { + ...processor, + options: nextOptions, + }, + selector, + }, + }); + }} + ariaLabel={editorItemMessages.processorTypeLabel({ type: processor.type })} + text={description} + placeholder={editorItemMessages.descriptionPlaceholder} + /> + + + { + editor.setMode({ + id: 'editingProcessor', + arg: { processor, selector }, + }); + }} + /> + + + {selected ? ( + + ) : ( + + + + )} + + + + + { + editor.setMode({ id: 'creatingProcessor', arg: { selector } }); + }} + onDelete={() => { + editor.setMode({ id: 'removingProcessor', arg: { selector } }); + }} + onDuplicate={() => { + processorsDispatch({ + type: 'duplicateProcessor', + payload: { + source: selector, + }, + }); + }} + /> + + + ); + } +); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_remove_modal.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_remove_modal.tsx new file mode 100644 index 0000000000000..c38e470b36699 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_remove_modal.tsx @@ -0,0 +1,54 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n/react'; +import React from 'react'; +import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { ProcessorInternal, ProcessorSelector } from '../types'; + +interface Props { + processor: ProcessorInternal; + selector: ProcessorSelector; + onResult: (arg: { confirmed: boolean; selector: ProcessorSelector }) => void; +} + +export const ProcessorRemoveModal = ({ processor, onResult, selector }: Props) => { + return ( + + + } + onCancel={() => onResult({ confirmed: false, selector })} + onConfirm={() => onResult({ confirmed: true, selector })} + cancelButtonText={ + + } + confirmButtonText={ + + } + > +

+ +

+
+
+ ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/index.ts new file mode 100644 index 0000000000000..60a1aa0a96fb1 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/index.ts @@ -0,0 +1,10 @@ +/* + * 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. + */ + +export { + ProcessorSettingsForm, + ProcessorSettingsFromOnSubmitArg, +} from './processor_settings_form.container'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/map_processor_type_to_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/map_processor_type_to_form.tsx new file mode 100644 index 0000000000000..e8164a0057d39 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/map_processor_type_to_form.tsx @@ -0,0 +1,56 @@ +/* + * 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 { FunctionComponent } from 'react'; + +// import { SetProcessor } from './processors/set'; +// import { Gsub } from './processors/gsub'; + +const mapProcessorTypeToForm = { + append: undefined, // TODO: Implement + bytes: undefined, // TODO: Implement + circle: undefined, // TODO: Implement + convert: undefined, // TODO: Implement + csv: undefined, // TODO: Implement + date: undefined, // TODO: Implement + date_index_name: undefined, // TODO: Implement + dissect: undefined, // TODO: Implement + dot_expander: undefined, // TODO: Implement + drop: undefined, // TODO: Implement + enrich: undefined, // TODO: Implement + fail: undefined, // TODO: Implement + foreach: undefined, // TODO: Implement + geoip: undefined, // TODO: Implement + grok: undefined, // TODO: Implement + html_strip: undefined, // TODO: Implement + inference: undefined, // TODO: Implement + join: undefined, // TODO: Implement + json: undefined, // TODO: Implement + kv: undefined, // TODO: Implement + lowercase: undefined, // TODO: Implement + pipeline: undefined, // TODO: Implement + remove: undefined, // TODO: Implement + rename: undefined, // TODO: Implement + script: undefined, // TODO: Implement + set_security_user: undefined, // TODO: Implement + split: undefined, // TODO: Implement + sort: undefined, // TODO: Implement + trim: undefined, // TODO: Implement + uppercase: undefined, // TODO: Implement + urldecode: undefined, // TODO: Implement + user_agent: undefined, // TODO: Implement + + gsub: undefined, + set: undefined, +}; + +export const types = Object.keys(mapProcessorTypeToForm); + +export type ProcessorType = keyof typeof mapProcessorTypeToForm; + +export const getProcessorForm = (type: ProcessorType | string): FunctionComponent | undefined => { + return mapProcessorTypeToForm[type as ProcessorType]; +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processor_settings_form.container.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processor_settings_form.container.tsx new file mode 100644 index 0000000000000..29b52ef84600a --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processor_settings_form.container.tsx @@ -0,0 +1,56 @@ +/* + * 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, useCallback, useEffect } from 'react'; + +import { useForm, OnFormUpdateArg, FormData } from '../../../../../shared_imports'; +import { ProcessorInternal } from '../../types'; + +import { ProcessorSettingsForm as ViewComponent } from './processor_settings_form'; + +export type ProcessorSettingsFromOnSubmitArg = Omit; + +interface Props { + onFormUpdate: (form: OnFormUpdateArg) => void; + onSubmit: (processor: ProcessorSettingsFromOnSubmitArg) => void; + processor?: ProcessorInternal; +} + +export const ProcessorSettingsForm: FunctionComponent = ({ + processor, + onFormUpdate, + onSubmit, +}) => { + const handleSubmit = useCallback( + async (data: FormData, isValid: boolean) => { + if (isValid) { + const { type, customOptions, ...options } = data; + onSubmit({ + type, + options: customOptions ? customOptions : options, + }); + } + }, + [onSubmit] + ); + + const { form } = useForm({ + defaultValue: processor?.options, + onSubmit: handleSubmit, + }); + + useEffect(() => { + const subscription = form.subscribe(onFormUpdate); + return subscription.unsubscribe; + + // TODO: Address this issue + // For some reason adding `form` object to the dependencies array here is causing an + // infinite update loop. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [onFormUpdate]); + + return ; +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processor_settings_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processor_settings_form.tsx new file mode 100644 index 0000000000000..49bde2129aab6 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processor_settings_form.tsx @@ -0,0 +1,73 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import React, { FunctionComponent, memo } from 'react'; +import { EuiButton, EuiHorizontalRule } from '@elastic/eui'; + +import { Form, useForm, FormDataProvider } from '../../../../../shared_imports'; + +import { ProcessorInternal } from '../../types'; + +import { getProcessorForm } from './map_processor_type_to_form'; +import { CommonProcessorFields, ProcessorTypeField } from './processors/common_fields'; +import { Custom } from './processors/custom'; + +export interface Props { + processor?: ProcessorInternal; + form: ReturnType['form']; +} + +export const ProcessorSettingsForm: FunctionComponent = memo( + ({ processor, form }) => { + return ( + + + + + + + {(arg: any) => { + const { type } = arg; + let formContent: React.ReactNode | undefined; + + if (type?.length) { + const ProcessorFormFields = getProcessorForm(type as any); + + if (ProcessorFormFields) { + formContent = ( + <> + + + + ); + } else { + formContent = ; + } + + return ( + <> + {formContent} + + {i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.settingsForm.submitButtonLabel', + { defaultMessage: 'Submit' } + )} + + + ); + } + + // If the user has not yet defined a type, we do not show any settings fields + return null; + }} + + + ); + }, + (previous, current) => { + return previous.processor === current.processor; + } +); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processors/common_fields/common_processor_fields.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processors/common_fields/common_processor_fields.tsx new file mode 100644 index 0000000000000..4802653f9e680 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processors/common_fields/common_processor_fields.tsx @@ -0,0 +1,55 @@ +/* + * 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 { + FieldConfig, + UseField, + FIELD_TYPES, + Field, + ToggleField, +} from '../../../../../../../shared_imports'; + +const ignoreFailureConfig: FieldConfig = { + defaultValue: false, + label: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.commonFields.ignoreFailureFieldLabel', + { + defaultMessage: 'Ignore failure', + } + ), + type: FIELD_TYPES.TOGGLE, +}; + +const ifConfig: FieldConfig = { + defaultValue: undefined, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.commonFields.ifFieldLabel', { + defaultMessage: 'Condition (optional)', + }), + type: FIELD_TYPES.TEXT, +}; + +const tagConfig: FieldConfig = { + defaultValue: undefined, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.commonFields.tagFieldLabel', { + defaultMessage: 'Tag (optional)', + }), + type: FIELD_TYPES.TEXT, +}; + +export const CommonProcessorFields: FunctionComponent = () => { + return ( + <> + + + + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processors/common_fields/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processors/common_fields/index.ts new file mode 100644 index 0000000000000..f3fa0e028faaa --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processors/common_fields/index.ts @@ -0,0 +1,9 @@ +/* + * 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. + */ + +export { ProcessorTypeField } from './processor_type_field'; + +export { CommonProcessorFields } from './common_processor_fields'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processors/common_fields/processor_type_field.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processors/common_fields/processor_type_field.tsx new file mode 100644 index 0000000000000..6c86fc16bcdd0 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processors/common_fields/processor_type_field.tsx @@ -0,0 +1,67 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import React, { FunctionComponent } from 'react'; +import { + FIELD_TYPES, + FieldConfig, + UseField, + fieldValidators, + ComboBoxField, +} from '../../../../../../../shared_imports'; +import { types } from '../../map_processor_type_to_form'; + +interface Props { + initialType?: string; +} + +const { emptyField } = fieldValidators; + +const typeConfig: FieldConfig = { + type: FIELD_TYPES.COMBO_BOX, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.typeField.typeFieldLabel', { + defaultMessage: 'Processor', + }), + deserializer: (value: string | undefined) => { + if (value) { + return [value]; + } + return []; + }, + serializer: (value: string[]) => { + return value[0]; + }, + validations: [ + { + validator: emptyField( + i18n.translate('xpack.ingestPipelines.pipelineEditor.typeField.fieldRequiredError', { + defaultMessage: 'A type is required.', + }) + ), + }, + ], +}; + +export const ProcessorTypeField: FunctionComponent = ({ initialType }) => { + return ( + ({ label: type, value: type })), + noSuggestions: false, + singleSelection: { + asPlainText: true, + }, + }, + }} + /> + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processors/custom.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processors/custom.tsx new file mode 100644 index 0000000000000..61fc31a7b472a --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processors/custom.tsx @@ -0,0 +1,90 @@ +/* + * 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 { + FieldConfig, + FIELD_TYPES, + fieldValidators, + UseField, + JsonEditorField, +} from '../../../../../../shared_imports'; + +const { emptyField, isJsonField } = fieldValidators; + +const customConfig: FieldConfig = { + type: FIELD_TYPES.TEXT, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.customForm.optionsFieldLabel', { + defaultMessage: 'Configuration options', + }), + serializer: (value: string) => { + try { + return JSON.parse(value); + } catch (error) { + // swallow error and return non-parsed value; + return value; + } + }, + deserializer: (value: any) => { + if (value === '') { + return '{\n\n}'; + } + return JSON.stringify(value, null, 2); + }, + validations: [ + { + validator: emptyField( + i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.customForm.configurationRequiredError', + { + defaultMessage: 'Configuration options are required.', + } + ) + ), + }, + { + validator: isJsonField( + i18n.translate('xpack.ingestPipelines.pipelineEditor.customForm.invalidJsonError', { + defaultMessage: 'The input is not valid.', + }) + ), + }, + ], +}; + +interface Props { + defaultOptions?: any; +} + +/** + * This is a catch-all component to support settings for custom processors + * or existing processors not yet supported by the UI. + * + * We store the settings in a field called "customOptions" + **/ +export const Custom: FunctionComponent = ({ defaultOptions }) => { + return ( + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processors/gsub.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processors/gsub.tsx new file mode 100644 index 0000000000000..77f85e61eff6b --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processors/gsub.tsx @@ -0,0 +1,98 @@ +/* + * 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 { + FieldConfig, + FIELD_TYPES, + fieldValidators, + ToggleField, + UseField, + Field, +} from '../../../../../../shared_imports'; + +const { emptyField } = fieldValidators; + +const fieldConfig: FieldConfig = { + type: FIELD_TYPES.TEXT, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.gsubForm.fieldFieldLabel', { + defaultMessage: 'Field', + }), + validations: [ + { + validator: emptyField( + i18n.translate('xpack.ingestPipelines.pipelineEditor.gsubForm.fieldRequiredError', { + defaultMessage: 'A field value is required.', + }) + ), + }, + ], +}; + +const patternConfig: FieldConfig = { + type: FIELD_TYPES.TEXT, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.gsubForm.patternFieldLabel', { + defaultMessage: 'Pattern', + }), + validations: [ + { + validator: emptyField( + i18n.translate('xpack.ingestPipelines.pipelineEditor.gsubForm.patternRequiredError', { + defaultMessage: 'A pattern value is required.', + }) + ), + }, + ], +}; + +const replacementConfig: FieldConfig = { + type: FIELD_TYPES.TEXT, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.gsubForm.replacementFieldLabel', { + defaultMessage: 'Replacement', + }), + validations: [ + { + validator: emptyField( + i18n.translate('xpack.ingestPipelines.pipelineEditor.gsubForm.replacementRequiredError', { + defaultMessage: 'A replacement value is required.', + }) + ), + }, + ], +}; + +const targetConfig: FieldConfig = { + type: FIELD_TYPES.TEXT, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.gsubForm.targetFieldLabel', { + defaultMessage: 'Target field (optional)', + }), +}; + +const ignoreMissingConfig: FieldConfig = { + defaultValue: false, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.gsubForm.ignoreMissingFieldLabel', { + defaultMessage: 'Ignore missing', + }), + type: FIELD_TYPES.TOGGLE, +}; + +export const Gsub: FunctionComponent = () => { + return ( + <> + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processors/set.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processors/set.tsx new file mode 100644 index 0000000000000..1ba6a14d0448d --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_settings_form/processors/set.tsx @@ -0,0 +1,74 @@ +/* + * 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 { + FieldConfig, + FIELD_TYPES, + fieldValidators, + ToggleField, + UseField, + Field, +} from '../../../../../../shared_imports'; + +const { emptyField } = fieldValidators; + +const fieldConfig: FieldConfig = { + type: FIELD_TYPES.TEXT, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.fieldFieldLabel', { + defaultMessage: 'Field', + }), + validations: [ + { + validator: emptyField( + i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.fieldRequiredError', { + defaultMessage: 'A field value is required.', + }) + ), + }, + ], +}; + +const valueConfig: FieldConfig = { + type: FIELD_TYPES.TEXT, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.valueFieldLabel', { + defaultMessage: 'Value', + }), + validations: [ + { + validator: emptyField( + i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.valueRequiredError', { + defaultMessage: 'A value to set is required.', + }) + ), + }, + ], +}; + +const overrideConfig: FieldConfig = { + defaultValue: false, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.overrideFieldLabel', { + defaultMessage: 'Override', + }), + type: FIELD_TYPES.TOGGLE, +}; + +/** + * Disambiguate name from the Set data structure + */ +export const SetProcessor: FunctionComponent = () => { + return ( + <> + + + + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_title_and_test_button.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_title_and_test_button.tsx new file mode 100644 index 0000000000000..bc646c9eefa55 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_title_and_test_button.tsx @@ -0,0 +1,73 @@ +/* + * 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 { EuiButton, EuiFlexGroup, EuiFlexItem, EuiLink, EuiText, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { usePipelineProcessorsContext } from '../context'; + +export interface Props { + onTestPipelineClick: () => void; + isTestButtonDisabled: boolean; +} + +export const ProcessorsTitleAndTestButton: FunctionComponent = ({ + onTestPipelineClick, + isTestButtonDisabled, +}) => { + const { links } = usePipelineProcessorsContext(); + return ( + + + +

+ {i18n.translate('xpack.ingestPipelines.pipelineEditor.processorsTreeTitle', { + defaultMessage: 'Processors', + })} +

+
+ + + {i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.processorsDocumentationLink', + { + defaultMessage: 'Learn more.', + } + )} + + ), + }} + /> + +
+ + + + + +
+ ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/components/drop_zone_button.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/components/drop_zone_button.tsx new file mode 100644 index 0000000000000..a47886292cf32 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/components/drop_zone_button.tsx @@ -0,0 +1,42 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import React, { FunctionComponent } from 'react'; +import classNames from 'classnames'; +import { EuiButtonIcon, EuiFlexItem } from '@elastic/eui'; + +export interface Props { + isDisabled: boolean; + onClick: (event: React.MouseEvent) => void; +} + +const MOVE_HERE_LABEL = i18n.translate('xpack.ingestPipelines.pipelineEditor.moveTargetLabel', { + defaultMessage: 'Move here', +}); + +export const DropZoneButton: FunctionComponent = ({ onClick, isDisabled }) => { + const containerClasses = classNames({ + 'pipelineProcessorsEditor__tree__dropZoneContainer--active': !isDisabled, + }); + const buttonClasses = classNames({ + 'pipelineProcessorsEditor__tree__dropZoneButton--active': !isDisabled, + }); + + return ( + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/components/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/components/index.ts new file mode 100644 index 0000000000000..e9548624d2cef --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/components/index.ts @@ -0,0 +1,11 @@ +/* + * 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. + */ + +export { DropZoneButton } from './drop_zone_button'; + +export { PrivateTree } from './private_tree'; + +export { TreeNode } from './tree_node'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/components/private_tree.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/components/private_tree.tsx new file mode 100644 index 0000000000000..bdc6b2eb44e2d --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/components/private_tree.tsx @@ -0,0 +1,210 @@ +/* + * 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, MutableRefObject, useEffect } from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { AutoSizer, List, WindowScroller } from 'react-virtualized'; + +import { DropSpecialLocations } from '../../../constants'; +import { ProcessorInternal, ProcessorSelector } from '../../../types'; +import { isChildPath } from '../../../processors_reducer'; + +import { DropZoneButton } from '.'; +import { TreeNode } from '.'; +import { calculateItemHeight } from '../utils'; +import { OnActionHandler, ProcessorInfo } from '../processors_tree'; + +export interface PrivateProps { + processors: ProcessorInternal[]; + selector: ProcessorSelector; + onAction: OnActionHandler; + level: number; + movingProcessor?: ProcessorInfo; + // Only passed into the top level list + windowScrollerRef?: MutableRefObject; + listRef?: MutableRefObject; +} + +const isDropZoneAboveDisabled = (processor: ProcessorInfo, selectedProcessor: ProcessorInfo) => { + return Boolean( + // Is the selected node first in a list? + (!selectedProcessor.aboveId && selectedProcessor.id === processor.id) || + isChildPath(selectedProcessor.selector, processor.selector) + ); +}; + +const isDropZoneBelowDisabled = (processor: ProcessorInfo, selectedProcessor: ProcessorInfo) => { + return ( + processor.id === selectedProcessor.id || + processor.belowId === selectedProcessor.id || + isChildPath(selectedProcessor.selector, processor.selector) + ); +}; + +/** + * Recursively rendering tree component for ingest pipeline processors. + * + * Note: this tree should start at level 1. It is the only level at + * which we render the optimised virtual component. This gives a + * massive performance boost to this component which can get very tall. + * + * The first level list also contains the outside click listener which + * enables users to click outside of the tree and cancel moving a + * processor. + */ +export const PrivateTree: FunctionComponent = ({ + processors, + selector, + movingProcessor, + onAction, + level, + windowScrollerRef, + listRef, +}) => { + const renderRow = ({ + idx, + info, + processor, + }: { + idx: number; + info: ProcessorInfo; + processor: ProcessorInternal; + }) => { + return ( + <> + {idx === 0 ? ( + { + event.preventDefault(); + onAction({ + type: 'move', + payload: { + destination: selector.concat(DropSpecialLocations.top), + source: movingProcessor!.selector, + }, + }); + }} + isDisabled={Boolean( + !movingProcessor || isDropZoneAboveDisabled(info, movingProcessor!) + )} + /> + ) : undefined} + + + + { + event.preventDefault(); + onAction({ + type: 'move', + payload: { + destination: selector.concat(String(idx + 1)), + source: movingProcessor!.selector, + }, + }); + }} + /> + + ); + }; + + useEffect(() => { + if (windowScrollerRef && windowScrollerRef.current) { + windowScrollerRef.current.updatePosition(); + } + if (listRef && listRef.current) { + listRef.current.recomputeRowHeights(); + } + }, [processors, listRef, windowScrollerRef, movingProcessor]); + + // A list optimized to handle very many items. + const renderVirtualList = () => { + return ( + + {({ height, registerChild, isScrolling, onChildScroll, scrollTop }: any) => { + return ( + + + {({ width }) => { + return ( +
+ { + const processor = processors[index]; + return calculateItemHeight({ + processor, + isFirstInArray: index === 0, + }); + }} + rowRenderer={({ index: idx, style }) => { + const processor = processors[idx]; + const above = processors[idx - 1]; + const below = processors[idx + 1]; + const info: ProcessorInfo = { + id: processor.id, + selector: selector.concat(String(idx)), + aboveId: above?.id, + belowId: below?.id, + }; + + return ( +
+ {renderRow({ processor, info, idx })} +
+ ); + }} + processors={processors} + /> +
+ ); + }} +
+
+ ); + }} +
+ ); + }; + + if (level === 1) { + // Only render the optimised list for the top level list because that is the list + // that will almost certainly be the tallest + return renderVirtualList(); + } + + return ( + + {processors.map((processor, idx) => { + const above = processors[idx - 1]; + const below = processors[idx + 1]; + const info: ProcessorInfo = { + id: processor.id, + selector: selector.concat(String(idx)), + aboveId: above?.id, + belowId: below?.id, + }; + + return
{renderRow({ processor, idx, info })}
; + })} +
+ ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/components/tree_node.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/components/tree_node.tsx new file mode 100644 index 0000000000000..ebe4ca4962b4c --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/components/tree_node.tsx @@ -0,0 +1,115 @@ +/* + * 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, useMemo } from 'react'; +import classNames from 'classnames'; +import { i18n } from '@kbn/i18n'; +import { EuiPanel, EuiText } from '@elastic/eui'; + +import { ProcessorInternal } from '../../../types'; + +import { ProcessorInfo, OnActionHandler } from '../processors_tree'; + +import { PipelineProcessorsEditorItem, Handlers } from '../../pipeline_processors_editor_item'; +import { AddProcessorButton } from '../../add_processor_button'; + +import { PrivateTree } from './private_tree'; + +export interface Props { + processor: ProcessorInternal; + processorInfo: ProcessorInfo; + onAction: OnActionHandler; + level: number; + movingProcessor?: ProcessorInfo; +} + +const INDENTATION_PX = 34; + +export const TreeNode: FunctionComponent = ({ + processor, + processorInfo, + onAction, + movingProcessor, + level, +}) => { + const stringSelector = processorInfo.selector.join('.'); + const handlers = useMemo((): Handlers => { + return { + onMove: () => { + onAction({ type: 'selectToMove', payload: { info: processorInfo } }); + }, + onCancelMove: () => { + onAction({ type: 'cancelMove' }); + }, + }; + }, [onAction, stringSelector, processor]); // eslint-disable-line react-hooks/exhaustive-deps + + const selected = movingProcessor?.id === processor.id; + + const panelClasses = classNames({ + 'pipelineProcessorsEditor__tree__item--selected': selected, + }); + + const renderOnFailureHandlersTree = () => { + if (!processor.onFailure?.length) { + return; + } + + const onFailureHandlerLabelClasses = classNames({ + 'pipelineProcessorsEditor__tree__onFailureHandlerLabel--withDropZone': + movingProcessor != null && + movingProcessor.id !== processor.onFailure[0].id && + movingProcessor.id !== processor.id, + }); + + return ( +
+
+ + {i18n.translate('xpack.ingestPipelines.pipelineEditor.onFailureProcessorsLabel', { + defaultMessage: 'Failure handlers', + })} + +
+ + + onAction({ + type: 'addProcessor', + payload: { target: processorInfo.selector.concat('onFailure') }, + }) + } + /> +
+ ); + }; + + return ( + + + {renderOnFailureHandlersTree()} + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/index.ts new file mode 100644 index 0000000000000..5a09794fd4bee --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/index.ts @@ -0,0 +1,7 @@ +/* + * 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. + */ + +export { ProcessorsTree, OnActionHandler, ProcessorInfo } from './processors_tree'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/processors_tree.scss b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/processors_tree.scss new file mode 100644 index 0000000000000..ad9058cea5e18 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/processors_tree.scss @@ -0,0 +1,74 @@ +@import '@elastic/eui/src/global_styling/variables/size'; + +.pipelineProcessorsEditor__tree { + + &__container { + background-color: $euiColorLightestShade; + padding: $euiSizeS; + } + + &__dropZoneContainer { + margin: 2px; + visibility: hidden; + border: 2px dashed $euiColorLightShade; + height: 12px; + border-radius: 2px; + + transition: border .5s; + + &--active { + &:hover { + border: 2px dashed $euiColorPrimary; + } + visibility: visible; + } + } + + &__dropZoneButton { + height: 8px; + opacity: 0; + text-decoration: none !important; + + &--active { + &:hover { + transform: none !important; + } + } + + &:disabled { + cursor: default !important; + & > * { + cursor: default !important; + } + } + } + + &__onFailureHandlerLabelContainer { + position: relative; + height: 14px; + } + &__onFailureHandlerLabel { + position: absolute; + bottom: -16px; + &--withDropZone { + bottom: -4px; + } + } + + + &__onFailureHandlerContainer { + margin-top: $euiSizeS; + margin-bottom: $euiSizeS; + & > * { + overflow: visible; + } + } + + &__item { + transition: border-color 1s; + min-height: 50px; + &--selected { + border: 1px solid $euiColorPrimary; + } + } +} diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/processors_tree.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/processors_tree.tsx new file mode 100644 index 0000000000000..d0661913515b2 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/processors_tree.tsx @@ -0,0 +1,110 @@ +/* + * 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, memo, useRef, useEffect } from 'react'; +import { EuiFlexGroup, EuiFlexItem, keyCodes } from '@elastic/eui'; +import { List, WindowScroller } from 'react-virtualized'; + +import { ProcessorInternal, ProcessorSelector } from '../../types'; + +import './processors_tree.scss'; +import { AddProcessorButton } from '../add_processor_button'; +import { PrivateTree } from './components'; + +export interface ProcessorInfo { + id: string; + selector: ProcessorSelector; + aboveId?: string; + belowId?: string; +} + +export type Action = + | { type: 'move'; payload: { source: ProcessorSelector; destination: ProcessorSelector } } + | { type: 'selectToMove'; payload: { info: ProcessorInfo } } + | { type: 'cancelMove' } + | { type: 'addProcessor'; payload: { target: ProcessorSelector } }; + +export type OnActionHandler = (action: Action) => void; + +export interface Props { + processors: ProcessorInternal[]; + baseSelector: ProcessorSelector; + onAction: OnActionHandler; + movingProcessor?: ProcessorInfo; + 'data-test-subj'?: string; +} + +/** + * This component is the public interface to our optimised tree rendering private components and + * also contains top-level state concerns for an instance of the component + */ +export const ProcessorsTree: FunctionComponent = memo((props) => { + const { processors, baseSelector, onAction, movingProcessor } = props; + // These refs are created here so they can be shared with all + // recursively rendered trees. Their values should come from react-virtualized + // List component and WindowScroller component. + const windowScrollerRef = useRef(null); + const listRef = useRef(null); + + useEffect(() => { + const cancelMoveKbListener = (event: KeyboardEvent) => { + // x-browser support per https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode + if (event.keyCode === keyCodes.ESCAPE || event.code === 'Escape') { + onAction({ type: 'cancelMove' }); + } + }; + const cancelMoveClickListener = (ev: any) => { + onAction({ type: 'cancelMove' }); + }; + // Give the browser a chance to flush any click events including the click + // event that triggered any state transition into selecting a processor to move + setTimeout(() => { + if (movingProcessor) { + window.addEventListener('keyup', cancelMoveKbListener); + window.addEventListener('click', cancelMoveClickListener); + } else { + window.removeEventListener('keyup', cancelMoveKbListener); + window.removeEventListener('click', cancelMoveClickListener); + } + }); + return () => { + window.removeEventListener('keyup', cancelMoveKbListener); + window.removeEventListener('click', cancelMoveClickListener); + }; + }, [movingProcessor, onAction]); + + return ( + + + + + + + + { + onAction({ type: 'addProcessor', payload: { target: baseSelector } }); + }} + /> + + + + + ); +}); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/utils.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/utils.ts new file mode 100644 index 0000000000000..457e335602b9b --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/utils.ts @@ -0,0 +1,41 @@ +/* + * 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 { ProcessorInternal } from '../../types'; + +// These values are tied to the style and heights following components: +// Do not change these numbers without testing the component for visual +// regressions! +// - ./components/tree_node.tsx +// - ./components/drop_zone_button.tsx +// - ./components/pipeline_processors_editor_item.tsx +const itemHeightsPx = { + WITHOUT_NESTED_ITEMS: 67, + WITH_NESTED_ITEMS: 137, + TOP_PADDING: 16, +}; + +export const calculateItemHeight = ({ + processor, + isFirstInArray, +}: { + processor: ProcessorInternal; + isFirstInArray: boolean; +}): number => { + const padding = isFirstInArray ? itemHeightsPx.TOP_PADDING : 0; + + if (!processor.onFailure?.length) { + return padding + itemHeightsPx.WITHOUT_NESTED_ITEMS; + } + + return ( + padding + + itemHeightsPx.WITH_NESTED_ITEMS + + processor.onFailure.reduce((acc, p, idx) => { + return acc + calculateItemHeight({ processor: p, isFirstInArray: idx === 0 }); + }, 0) + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/settings_form_flyout.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/settings_form_flyout.tsx new file mode 100644 index 0000000000000..94d5f0eda6454 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/settings_form_flyout.tsx @@ -0,0 +1,67 @@ +/* + * 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 { EuiFlyout, EuiFlyoutBody, EuiFlyoutHeader, EuiTitle } from '@elastic/eui'; + +import React, { FunctionComponent, memo, useEffect } from 'react'; + +import { FormattedMessage } from '@kbn/i18n/react'; + +import { OnFormUpdateArg } from '../../../../shared_imports'; + +import { ProcessorInternal } from '../types'; + +import { ProcessorSettingsForm, ProcessorSettingsFromOnSubmitArg } from '.'; + +export type OnSubmitHandler = (processor: ProcessorSettingsFromOnSubmitArg) => void; + +export interface Props { + processor: ProcessorInternal | undefined; + onFormUpdate: (form: OnFormUpdateArg) => void; + onSubmit: OnSubmitHandler; + isOnFailureProcessor: boolean; + onOpen: () => void; + onClose: () => void; +} + +export const SettingsFormFlyout: FunctionComponent = memo( + ({ onClose, processor, onSubmit, onFormUpdate, onOpen, isOnFailureProcessor }) => { + useEffect( + () => { + onOpen(); + }, + [] /* eslint-disable-line react-hooks/exhaustive-deps */ + ); + const flyoutTitleContent = isOnFailureProcessor ? ( + + ) : ( + + ); + + return ( + + + +

{flyoutTitleContent}

+
+
+ + + +
+ ); + } +); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/constants.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/constants.ts new file mode 100644 index 0000000000000..46e3d1c803fd5 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/constants.ts @@ -0,0 +1,10 @@ +/* + * 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. + */ + +export enum DropSpecialLocations { + top = 'TOP', + bottom = 'BOTTOM', +} diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context.tsx new file mode 100644 index 0000000000000..150a52f1a5fe0 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context.tsx @@ -0,0 +1,55 @@ +/* + * 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, { createContext, Dispatch, FunctionComponent, useContext, useState } from 'react'; +import { EditorMode } from './types'; +import { ProcessorsDispatch } from './processors_reducer'; + +interface Links { + learnMoreAboutProcessorsUrl: string; + learnMoreAboutOnFailureProcessorsUrl: string; +} + +const PipelineProcessorsContext = createContext<{ + links: Links; + state: { + processorsDispatch: ProcessorsDispatch; + editor: { + mode: EditorMode; + setMode: Dispatch; + }; + }; +}>({} as any); + +interface Props { + links: Links; + processorsDispatch: ProcessorsDispatch; +} + +export const PipelineProcessorsContextProvider: FunctionComponent = ({ + links, + children, + processorsDispatch, +}) => { + const [mode, setMode] = useState({ id: 'idle' }); + return ( + + {children} + + ); +}; + +export const usePipelineProcessorsContext = () => { + const ctx = useContext(PipelineProcessorsContext); + if (!ctx) { + throw new Error( + 'usePipelineProcessorsContext can only be used inside of PipelineProcessorsContextProvider' + ); + } + return ctx; +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/deserialize.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/deserialize.ts new file mode 100644 index 0000000000000..fa1d041bdaba3 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/deserialize.ts @@ -0,0 +1,56 @@ +/* + * 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 uuid from 'uuid'; +import { Processor } from '../../../../common/types'; +import { ProcessorInternal } from './types'; + +export interface DeserializeArgs { + processors: Processor[]; + onFailure?: Processor[]; +} + +export interface DeserializeResult { + processors: ProcessorInternal[]; + onFailure?: ProcessorInternal[]; +} + +const getProcessorType = (processor: Processor): string => { + /** + * See the definition of {@link ProcessorInternal} for why this works to extract the + * processor type. + */ + return Object.keys(processor)[0]!; +}; + +const convertToPipelineInternalProcessor = (processor: Processor): ProcessorInternal => { + const type = getProcessorType(processor); + const { on_failure: originalOnFailure, ...options } = processor[type]; + const onFailure = originalOnFailure?.length + ? convertProcessors(originalOnFailure) + : (originalOnFailure as ProcessorInternal[] | undefined); + return { + id: uuid.v4(), + type, + onFailure, + options, + }; +}; + +const convertProcessors = (processors: Processor[]) => { + const convertedProcessors = []; + + for (const processor of processors) { + convertedProcessors.push(convertToPipelineInternalProcessor(processor)); + } + return convertedProcessors; +}; + +export const deserialize = ({ processors, onFailure }: DeserializeArgs): DeserializeResult => { + return { + processors: convertProcessors(processors), + onFailure: onFailure ? convertProcessors(onFailure) : undefined, + }; +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/index.ts new file mode 100644 index 0000000000000..58d6e492b85e5 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/index.ts @@ -0,0 +1,11 @@ +/* + * 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. + */ + +export { PipelineProcessorsEditor, OnUpdateHandler } from './pipeline_processors_editor.container'; + +export { OnUpdateHandlerArg } from './types'; + +export { SerializeResult } from './serialize'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.container.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.container.tsx new file mode 100644 index 0000000000000..057f8638700a4 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.container.tsx @@ -0,0 +1,76 @@ +/* + * 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, useMemo } from 'react'; + +import { Processor } from '../../../../common/types'; + +import { deserialize } from './deserialize'; + +import { useProcessorsState } from './processors_reducer'; + +import { PipelineProcessorsContextProvider } from './context'; + +import { OnUpdateHandlerArg } from './types'; + +import { PipelineProcessorsEditor as PipelineProcessorsEditorUI } from './pipeline_processors_editor'; + +export interface Props { + value: { + processors: Processor[]; + onFailure?: Processor[]; + }; + onUpdate: (arg: OnUpdateHandlerArg) => void; + isTestButtonDisabled: boolean; + onTestPipelineClick: () => void; + learnMoreAboutProcessorsUrl: string; + learnMoreAboutOnFailureProcessorsUrl: string; + /** + * Give users a way to react to this component opening a flyout + */ + onFlyoutOpen: () => void; +} + +export type OnUpdateHandler = (arg: OnUpdateHandlerArg) => void; + +export const PipelineProcessorsEditor: FunctionComponent = ({ + value: { processors: originalProcessors, onFailure: originalOnFailureProcessors }, + onFlyoutOpen, + onUpdate, + isTestButtonDisabled, + learnMoreAboutOnFailureProcessorsUrl, + learnMoreAboutProcessorsUrl, + onTestPipelineClick, +}) => { + const deserializedResult = useMemo( + () => + deserialize({ + processors: originalProcessors, + onFailure: originalOnFailureProcessors, + }), + // TODO: Re-add the dependency on the props and make the state set-able + // when new props come in so that this component will be controllable + [] // eslint-disable-line react-hooks/exhaustive-deps + ); + const [processorsState, processorsDispatch] = useProcessorsState(deserializedResult); + const { processors, onFailure } = processorsState; + + return ( + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.scss b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.scss new file mode 100644 index 0000000000000..ee7421d7dbfa8 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.scss @@ -0,0 +1,3 @@ +.pipelineProcessorsEditor { + margin-bottom: $euiSize; +} diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.tsx new file mode 100644 index 0000000000000..24b9598a74d47 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.tsx @@ -0,0 +1,239 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n/react'; +import React, { FunctionComponent, useCallback, memo, useState, useEffect } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiSwitch } from '@elastic/eui'; + +import './pipeline_processors_editor.scss'; + +import { + ProcessorsTitleAndTestButton, + OnFailureProcessorsTitle, + ProcessorsTree, + SettingsFormFlyout, + ProcessorRemoveModal, + OnActionHandler, + OnSubmitHandler, +} from './components'; + +import { + ProcessorInternal, + ProcessorSelector, + OnUpdateHandlerArg, + FormValidityState, + OnFormUpdateArg, +} from './types'; + +import { serialize } from './serialize'; +import { getValue } from './utils'; +import { usePipelineProcessorsContext } from './context'; + +export interface Props { + processors: ProcessorInternal[]; + onFailureProcessors: ProcessorInternal[]; + onUpdate: (arg: OnUpdateHandlerArg) => void; + isTestButtonDisabled: boolean; + onTestPipelineClick: () => void; + onFlyoutOpen: () => void; +} + +const PROCESSOR_STATE_SCOPE: ProcessorSelector = ['processors']; +const ON_FAILURE_STATE_SCOPE: ProcessorSelector = ['onFailure']; + +export const PipelineProcessorsEditor: FunctionComponent = memo( + function PipelineProcessorsEditor({ + processors, + onFailureProcessors, + onTestPipelineClick, + isTestButtonDisabled, + onUpdate, + onFlyoutOpen, + }) { + const { + state: { editor, processorsDispatch }, + } = usePipelineProcessorsContext(); + + const { mode: editorMode, setMode: setEditorMode } = editor; + + const [formState, setFormState] = useState({ + validate: () => Promise.resolve(true), + }); + + const onFormUpdate = useCallback<(arg: OnFormUpdateArg) => void>( + ({ isValid, validate }) => { + setFormState({ + validate: async () => { + if (isValid === undefined) { + return validate(); + } + return isValid; + }, + }); + }, + [setFormState] + ); + + const [showGlobalOnFailure, setShowGlobalOnFailure] = useState( + Boolean(onFailureProcessors.length) + ); + + useEffect(() => { + onUpdate({ + validate: async () => { + const formValid = await formState.validate(); + return formValid && editorMode.id === 'idle'; + }, + getData: () => + serialize({ + onFailure: showGlobalOnFailure ? onFailureProcessors : undefined, + processors, + }), + }); + }, [processors, onFailureProcessors, onUpdate, formState, editorMode, showGlobalOnFailure]); + + const onSubmit = useCallback( + (processorTypeAndOptions) => { + switch (editorMode.id) { + case 'creatingProcessor': + processorsDispatch({ + type: 'addProcessor', + payload: { + processor: { ...processorTypeAndOptions }, + targetSelector: editorMode.arg.selector, + }, + }); + break; + case 'editingProcessor': + processorsDispatch({ + type: 'updateProcessor', + payload: { + processor: { + ...editorMode.arg.processor, + ...processorTypeAndOptions, + }, + selector: editorMode.arg.selector, + }, + }); + break; + default: + } + setEditorMode({ id: 'idle' }); + }, + [processorsDispatch, editorMode, setEditorMode] + ); + + const onCloseSettingsForm = useCallback(() => { + setEditorMode({ id: 'idle' }); + setFormState({ validate: () => Promise.resolve(true) }); + }, [setFormState, setEditorMode]); + + const onTreeAction = useCallback( + (action) => { + switch (action.type) { + case 'addProcessor': + setEditorMode({ id: 'creatingProcessor', arg: { selector: action.payload.target } }); + break; + case 'move': + setEditorMode({ id: 'idle' }); + processorsDispatch({ + type: 'moveProcessor', + payload: action.payload, + }); + break; + case 'selectToMove': + setEditorMode({ id: 'movingProcessor', arg: action.payload.info }); + break; + case 'cancelMove': + setEditorMode({ id: 'idle' }); + break; + } + }, + [processorsDispatch, setEditorMode] + ); + + const movingProcessor = editorMode.id === 'movingProcessor' ? editorMode.arg : undefined; + + return ( +
+ + + + + + + + + + + + + + + + } + checked={showGlobalOnFailure} + onChange={(e) => setShowGlobalOnFailure(e.target.checked)} + data-test-subj="pipelineEditorOnFailureToggle" + /> + + {showGlobalOnFailure ? ( + + + + ) : undefined} + + {editorMode.id === 'editingProcessor' || editorMode.id === 'creatingProcessor' ? ( + 1} + processor={editorMode.id === 'editingProcessor' ? editorMode.arg.processor : undefined} + onOpen={onFlyoutOpen} + onFormUpdate={onFormUpdate} + onSubmit={onSubmit} + onClose={onCloseSettingsForm} + /> + ) : undefined} + {editorMode.id === 'removingProcessor' && ( + { + if (confirmed) { + processorsDispatch({ + type: 'removeProcessor', + payload: { selector }, + }); + } + setEditorMode({ id: 'idle' }); + }} + /> + )} +
+ ); + } +); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/index.ts new file mode 100644 index 0000000000000..b43d94e19bf9f --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/index.ts @@ -0,0 +1,15 @@ +/* + * 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. + */ + +export { + State, + reducer, + useProcessorsState, + ProcessorsDispatch, + Action, +} from './processors_reducer'; + +export { isChildPath } from './utils'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/processors_reducer.test.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/processors_reducer.test.ts new file mode 100644 index 0000000000000..43072d65bac4e --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/processors_reducer.test.ts @@ -0,0 +1,376 @@ +/* + * 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 { reducer, State } from './processors_reducer'; +import { DropSpecialLocations } from '../constants'; +import { PARENT_CHILD_NEST_ERROR } from './utils'; + +const initialState: State = { + processors: [], + onFailure: [], + isRoot: true, +}; + +describe('Processors reducer', () => { + it('reorders processors', () => { + const processor1 = { id: expect.any(String), type: 'test1', options: {} }; + const processor2 = { id: expect.any(String), type: 'test2', options: {} }; + const processor3 = { id: expect.any(String), type: 'test3', options: {} }; + + const s1 = reducer(initialState, { + type: 'addProcessor', + payload: { processor: processor1, targetSelector: ['processors'] }, + }); + const s2 = reducer(s1, { + type: 'addProcessor', + payload: { processor: processor2, targetSelector: ['processors'] }, + }); + const s3 = reducer(s2, { + type: 'addProcessor', + payload: { processor: processor3, targetSelector: ['processors'] }, + }); + + expect(s3.processors).toEqual([processor1, processor2, processor3]); + + // Move the second processor to the first + const s4 = reducer(s3, { + type: 'moveProcessor', + payload: { + source: ['processors', '1'], + destination: ['processors', '0'], + }, + }); + + expect(s4.processors).toEqual([processor2, processor1, processor3]); + }); + + it('moves and orders processors out of lists', () => { + const processor1 = { id: expect.any(String), type: 'test1', options: {} }; + const processor2 = { id: expect.any(String), type: 'test2', options: {} }; + const processor3 = { id: expect.any(String), type: 'test3', options: {} }; + const processor4 = { id: expect.any(String), type: 'test4', options: {} }; + + const s1 = reducer(initialState, { + type: 'addProcessor', + payload: { processor: processor1, targetSelector: ['processors'] }, + }); + const s2 = reducer(s1, { + type: 'addProcessor', + payload: { processor: processor2, targetSelector: ['processors'] }, + }); + + const s3 = reducer(s2, { + type: 'addProcessor', + payload: { processor: processor3, targetSelector: ['processors', '1'] }, + }); + + const s4 = reducer(s3, { + type: 'addProcessor', + payload: { + processor: processor4, + targetSelector: ['processors', '1', 'onFailure', '0'], + }, + }); + + expect(s4.processors).toEqual([ + processor1, + { ...processor2, onFailure: [{ ...processor3, onFailure: [processor4] }] }, + ]); + + // Move the first on failure processor of the second processors on failure processor + // to the second position of the root level. + const s5 = reducer(s4, { + type: 'moveProcessor', + payload: { + source: ['processors', '1', 'onFailure', '0'], + destination: ['processors', '1'], + }, + }); + + expect(s5.processors).toEqual([ + processor1, + { ...processor3, onFailure: [processor4] }, + { ...processor2, onFailure: undefined }, + ]); + }); + + it('moves and orders processors into lists', () => { + const processor1 = { id: expect.any(String), type: 'test1', options: {} }; + const processor2 = { id: expect.any(String), type: 'test2', options: {} }; + const processor3 = { id: expect.any(String), type: 'test3', options: {} }; + const processor4 = { id: expect.any(String), type: 'test4', options: {} }; + + const s1 = reducer(initialState, { + type: 'addProcessor', + payload: { processor: processor1, targetSelector: ['processors'] }, + }); + const s2 = reducer(s1, { + type: 'addProcessor', + payload: { processor: processor2, targetSelector: ['processors'] }, + }); + + const s3 = reducer(s2, { + type: 'addProcessor', + payload: { processor: processor3, targetSelector: ['processors', '1'] }, + }); + + const s4 = reducer(s3, { + type: 'addProcessor', + payload: { + processor: processor4, + targetSelector: ['processors', '1', 'onFailure', '0'], + }, + }); + + expect(s4.processors).toEqual([ + processor1, + { ...processor2, onFailure: [{ ...processor3, onFailure: [processor4] }] }, + ]); + + // Move the first processor to the deepest most on-failure processor's failure processor + const s5 = reducer(s4, { + type: 'moveProcessor', + payload: { + source: ['processors', '0'], + destination: ['processors', '1', 'onFailure', '0', 'onFailure', '0', 'onFailure', '0'], + }, + }); + + expect(s5.processors).toEqual([ + { + ...processor2, + onFailure: [{ ...processor3, onFailure: [{ ...processor4, onFailure: [processor1] }] }], + }, + ]); + }); + + it('handles sending processor to bottom correctly', () => { + const processor1 = { id: expect.any(String), type: 'test1', options: {} }; + const processor2 = { id: expect.any(String), type: 'test2', options: {} }; + const processor3 = { id: expect.any(String), type: 'test3', options: {} }; + + const s1 = reducer(initialState, { + type: 'addProcessor', + payload: { processor: processor1, targetSelector: ['processors'] }, + }); + + const s2 = reducer(s1, { + type: 'addProcessor', + payload: { processor: processor2, targetSelector: ['processors'] }, + }); + + const s3 = reducer(s2, { + type: 'addProcessor', + payload: { processor: processor3, targetSelector: ['processors'] }, + }); + + // Move the parent into a child list + const s4 = reducer(s3, { + type: 'moveProcessor', + payload: { + source: ['processors', '0'], + destination: ['processors', DropSpecialLocations.bottom], + }, + }); + + // Assert nothing changed + expect(s4.processors).toEqual([processor2, processor3, processor1]); + }); + + it('will not set the root "onFailure" to "undefined" if it is empty', () => { + const processor1 = { id: expect.any(String), type: 'test1', options: {} }; + const processor2 = { id: expect.any(String), type: 'test2', options: {} }; + + const s1 = reducer(initialState, { + type: 'addProcessor', + payload: { processor: processor1, targetSelector: ['processors'] }, + }); + + const s2 = reducer(s1, { + type: 'addProcessor', + payload: { processor: processor2, targetSelector: ['onFailure'] }, + }); + + // Move the parent into a child list + const s3 = reducer(s2, { + type: 'moveProcessor', + payload: { + source: ['onFailure', '0'], + destination: ['processors', '1'], + }, + }); + + expect(s3).toEqual({ + processors: [processor1, processor2], + onFailure: [], + isRoot: true, + }); + }); + + it('places copies and places the copied processor below the original', () => { + const processor1 = { id: expect.any(String), type: 'test1', options: {} }; + const processor2 = { id: expect.any(String), type: 'test2', options: {} }; + const processor3 = { id: expect.any(String), type: 'test3', options: {} }; + const processor4 = { + id: expect.any(String), + type: 'test4', + options: { field: 'field_name', value: 'field_value' }, + }; + + const s1 = reducer(initialState, { + type: 'addProcessor', + payload: { processor: processor1, targetSelector: ['processors'] }, + }); + const s2 = reducer(s1, { + type: 'addProcessor', + payload: { processor: processor2, targetSelector: ['processors'] }, + }); + + const s3 = reducer(s2, { + type: 'addProcessor', + payload: { processor: processor3, targetSelector: ['processors', '1'] }, + }); + + const s4 = reducer(s3, { + type: 'addProcessor', + payload: { + processor: processor4, + targetSelector: ['processors', '1', 'onFailure', '0'], + }, + }); + + const s5 = reducer(s4, { + type: 'duplicateProcessor', + payload: { source: ['processors', '1', 'onFailure', '0', 'onFailure', '0'] }, + }); + + const s6 = reducer(s5, { + type: 'duplicateProcessor', + payload: { source: ['processors', '1', 'onFailure', '0', 'onFailure', '0'] }, + }); + + expect(s6.processors).toEqual([ + processor1, + { + ...processor2, + onFailure: [ + { + ...processor3, + onFailure: [processor4, processor4, processor4], + }, + ], + }, + ]); + }); + + describe('Error conditions', () => { + let originalErrorLogger: any; + beforeEach(() => { + // eslint-disable-next-line no-console + originalErrorLogger = console.error; + // eslint-disable-next-line no-console + console.error = jest.fn(); + }); + + afterEach(() => { + // eslint-disable-next-line no-console + console.error = originalErrorLogger; + }); + + it('prevents moving a parent into child list', () => { + const processor1 = { id: expect.any(String), type: 'test1', options: {} }; + const processor2 = { id: expect.any(String), type: 'test2', options: {} }; + const processor3 = { id: expect.any(String), type: 'test3', options: {} }; + const processor4 = { id: expect.any(String), type: 'test4', options: {} }; + + const s1 = reducer(initialState, { + type: 'addProcessor', + payload: { processor: processor1, targetSelector: ['processors'] }, + }); + + const s2 = reducer(s1, { + type: 'addProcessor', + payload: { processor: processor2, targetSelector: ['processors'] }, + }); + + const s3 = reducer(s2, { + type: 'addProcessor', + payload: { processor: processor3, targetSelector: ['processors', '1'] }, + }); + + const s4 = reducer(s3, { + type: 'addProcessor', + payload: { + processor: processor4, + targetSelector: ['processors', '1', 'onFailure', '0'], + }, + }); + + expect(s4.processors).toEqual([ + processor1, + { ...processor2, onFailure: [{ ...processor3, onFailure: [processor4] }] }, + ]); + + // Move the parent into a child list + const s5 = reducer(s4, { + type: 'moveProcessor', + payload: { + source: ['processors', '1'], + destination: ['processors', '1', 'onFailure', '0', 'onFailure', '0', 'onFailure', '0'], + }, + }); + + // eslint-disable-next-line no-console + expect(console.error).toHaveBeenCalledWith(new Error(PARENT_CHILD_NEST_ERROR)); + + // Assert nothing changed + expect(s5.processors).toEqual(s4.processors); + }); + + it('does not remove top level processor and onFailure arrays if they are emptied', () => { + const processor1 = { id: expect.any(String), type: 'test1', options: {} }; + const s1 = reducer(initialState, { + type: 'addProcessor', + payload: { processor: processor1, targetSelector: ['processors'] }, + }); + const s2 = reducer(s1, { + type: 'removeProcessor', + payload: { selector: ['processors', '0'] }, + }); + expect(s2.processors).not.toBe(undefined); + }); + + it('throws for bad move processor', () => { + const processor1 = { id: expect.any(String), type: 'test1', options: {} }; + const processor2 = { id: expect.any(String), type: 'test2', options: {} }; + + const s1 = reducer(initialState, { + type: 'addProcessor', + payload: { processor: processor1, targetSelector: ['processors'] }, + }); + + const s2 = reducer(s1, { + type: 'addProcessor', + payload: { processor: processor2, targetSelector: ['onFailure'] }, + }); + + const s3 = reducer(s2, { + type: 'moveProcessor', + payload: { + source: ['onFailure'], + destination: ['processors'], + }, + }); + + // eslint-disable-next-line no-console + expect(console.error).toHaveBeenCalledWith( + new Error('Expected number but received "processors"') + ); + + expect(s3.processors).toEqual(s2.processors); + }); + }); +}); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/processors_reducer.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/processors_reducer.ts new file mode 100644 index 0000000000000..4e069aab8bdd1 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/processors_reducer.ts @@ -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 uuid from 'uuid'; +import { Reducer, useReducer, Dispatch } from 'react'; +import { DeserializeResult } from '../deserialize'; +import { getValue, setValue } from '../utils'; +import { ProcessorInternal, ProcessorSelector } from '../types'; + +import { unsafeProcessorMove, duplicateProcessor } from './utils'; + +export type State = Omit & { + onFailure: ProcessorInternal[]; + isRoot: true; +}; + +export type Action = + | { + type: 'addProcessor'; + payload: { processor: Omit; targetSelector: ProcessorSelector }; + } + | { + type: 'updateProcessor'; + payload: { processor: ProcessorInternal; selector: ProcessorSelector }; + } + | { + type: 'removeProcessor'; + payload: { selector: ProcessorSelector }; + } + | { + type: 'moveProcessor'; + payload: { source: ProcessorSelector; destination: ProcessorSelector }; + } + | { + type: 'duplicateProcessor'; + payload: { + source: ProcessorSelector; + }; + }; + +export type ProcessorsDispatch = Dispatch; + +export const reducer: Reducer = (state, action) => { + if (action.type === 'moveProcessor') { + const { destination, source } = action.payload; + try { + return unsafeProcessorMove(state, source, destination); + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + return { ...state }; + } + } + + if (action.type === 'removeProcessor') { + const { selector } = action.payload; + const processorsSelector = selector.slice(0, -1); + const parentProcessorSelector = processorsSelector.slice(0, -1); + const idx = parseInt(selector[selector.length - 1], 10); + const processors = getValue(processorsSelector, state); + processors.splice(idx, 1); + const parentProcessor = getValue(parentProcessorSelector, state); + if (!processors.length && selector.length && !(parentProcessor as State).isRoot) { + return setValue(processorsSelector, state, undefined); + } + return setValue(processorsSelector, state, [...processors]); + } + + if (action.type === 'addProcessor') { + const { processor, targetSelector } = action.payload; + if (!targetSelector.length) { + throw new Error('Expected target selector to contain a path, but received an empty array.'); + } + const targetProcessor = getValue( + targetSelector, + state + ); + if (!targetProcessor) { + throw new Error( + `Could not find processor or processors array at ${targetSelector.join('.')}` + ); + } + if (Array.isArray(targetProcessor)) { + return setValue( + targetSelector, + state, + targetProcessor.concat({ ...processor, id: uuid.v4() }) + ); + } else { + const processorWithId = { ...processor, id: uuid.v4() }; + targetProcessor.onFailure = targetProcessor.onFailure + ? targetProcessor.onFailure.concat(processorWithId) + : [processorWithId]; + return setValue(targetSelector, state, targetProcessor); + } + } + + if (action.type === 'updateProcessor') { + const { processor, selector } = action.payload; + const processorsSelector = selector.slice(0, -1); + const idx = parseInt(selector[selector.length - 1], 10); + + if (isNaN(idx)) { + throw new Error(`Expected numeric value, received ${idx}`); + } + + const processors = getValue(processorsSelector, state); + processors[idx] = processor; + return setValue(processorsSelector, state, [...processors]); + } + + if (action.type === 'duplicateProcessor') { + const sourceSelector = action.payload.source; + const sourceProcessor = getValue(sourceSelector, state); + const sourceIdx = parseInt(sourceSelector[sourceSelector.length - 1], 10); + const sourceProcessorsArraySelector = sourceSelector.slice(0, -1); + const sourceProcessorsArray = [ + ...getValue(sourceProcessorsArraySelector, state), + ]; + const copy = duplicateProcessor(sourceProcessor); + sourceProcessorsArray.splice(sourceIdx + 1, 0, copy); + return setValue(sourceProcessorsArraySelector, state, sourceProcessorsArray); + } + + return state; +}; + +export const useProcessorsState = (initialState: DeserializeResult) => { + const state = { + ...initialState, + onFailure: initialState.onFailure ?? [], + }; + return useReducer(reducer, { ...state, isRoot: true }); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/utils.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/utils.ts new file mode 100644 index 0000000000000..7cb7d076623aa --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer/utils.ts @@ -0,0 +1,100 @@ +/* + * 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 uuid from 'uuid'; +import { State } from './processors_reducer'; +import { ProcessorInternal, ProcessorSelector } from '../types'; +import { DropSpecialLocations } from '../constants'; +import { checkIfSamePath, getValue } from '../utils'; + +export const PARENT_CHILD_NEST_ERROR = 'PARENT_CHILD_NEST_ERROR'; + +export const duplicateProcessor = (sourceProcessor: ProcessorInternal): ProcessorInternal => { + const onFailure = sourceProcessor.onFailure + ? sourceProcessor.onFailure.map((p) => duplicateProcessor(p)) + : undefined; + return { + ...sourceProcessor, + onFailure, + id: uuid.v4(), + options: { + ...sourceProcessor.options, + }, + }; +}; + +export const isChildPath = (a: ProcessorSelector, b: ProcessorSelector) => { + return a.every((pathSegment, idx) => pathSegment === b[idx]); +}; + +/** + * Unsafe! + * + * This function takes a data structure and mutates it in place. + * + * It is convenient for updating the processors (see {@link ProcessorInternal}) + * structure in this way because the structure is recursive. We are moving processors between + * different arrays, removing in one, and adding to another. The end result should be consistent + * with these actions. + * + * @remark + * This function assumes parents cannot be moved into themselves. + */ +export const unsafeProcessorMove = ( + state: State, + source: ProcessorSelector, + destination: ProcessorSelector +): State => { + const pathToSourceArray = source.slice(0, -1); + const pathToDestArray = destination.slice(0, -1); + if (isChildPath(source, destination)) { + throw new Error(PARENT_CHILD_NEST_ERROR); + } + const isXArrayMove = !checkIfSamePath(pathToSourceArray, pathToDestArray); + + // Start by setting up references to objects of interest using our selectors + // At this point, our selectors are consistent with the data passed in. + const sourceProcessors = getValue(pathToSourceArray, state); + const destinationProcessors = getValue(pathToDestArray, state); + const sourceIndex = parseInt(source[source.length - 1], 10); + const sourceProcessor = getValue(pathToSourceArray.slice(0, -1), state); + const processor = sourceProcessors[sourceIndex]; + + const lastDestItem = destination[destination.length - 1]; + let destIndex: number; + if (lastDestItem === DropSpecialLocations.top) { + destIndex = 0; + } else if (lastDestItem === DropSpecialLocations.bottom) { + destIndex = Infinity; + } else if (/^-?[0-9]+$/.test(lastDestItem)) { + destIndex = parseInt(lastDestItem, 10); + } else { + throw new Error(`Expected number but received "${lastDestItem}"`); + } + + if (isXArrayMove) { + // First perform the add operation. + if (destinationProcessors) { + destinationProcessors.splice(destIndex, 0, processor); + } else { + const targetProcessor = getValue(pathToDestArray.slice(0, -1), state); + targetProcessor.onFailure = [processor]; + } + // !! Beyond this point, selectors are no longer usable because we have mutated the data structure! + // Second, we perform the deletion operation + sourceProcessors.splice(sourceIndex, 1); + + // If onFailure is empty, delete the array. + if (!sourceProcessors.length && !((sourceProcessor as unknown) as State).isRoot) { + delete sourceProcessor.onFailure; + } + } else { + destinationProcessors.splice(destIndex, 0, processor); + const targetIdx = sourceIndex > destIndex ? sourceIndex + 1 : sourceIndex; + sourceProcessors.splice(targetIdx, 1); + } + + return { ...state, processors: [...state.processors], onFailure: [...state.onFailure] }; +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/serialize.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/serialize.ts new file mode 100644 index 0000000000000..153c9e252ccc0 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/serialize.ts @@ -0,0 +1,49 @@ +/* + * 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 { Processor } from '../../../../common/types'; + +import { DeserializeResult } from './deserialize'; +import { ProcessorInternal } from './types'; + +type SerializeArgs = DeserializeResult; + +export interface SerializeResult { + processors: Processor[]; + on_failure?: Processor[]; +} + +const convertProcessorInternalToProcessor = (processor: ProcessorInternal): Processor => { + const { options, onFailure, type } = processor; + const outProcessor = { + [type]: { + ...options, + }, + }; + + if (onFailure?.length) { + outProcessor[type].on_failure = convertProcessors(onFailure); + } else if (onFailure) { + outProcessor[type].on_failure = []; + } + + return outProcessor; +}; + +const convertProcessors = (processors: ProcessorInternal[]) => { + const convertedProcessors = []; + + for (const processor of processors) { + convertedProcessors.push(convertProcessorInternalToProcessor(processor)); + } + return convertedProcessors; +}; + +export const serialize = ({ processors, onFailure }: SerializeArgs): SerializeResult => { + return { + processors: convertProcessors(processors), + on_failure: onFailure?.length ? convertProcessors(onFailure) : undefined, + }; +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/types.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/types.ts new file mode 100644 index 0000000000000..aa39fca29fa8b --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/types.ts @@ -0,0 +1,51 @@ +/* + * 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 { OnFormUpdateArg } from '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; +import { SerializeResult } from './serialize'; +import { ProcessorInfo } from './components/processors_tree'; + +/** + * An array of keys that map to a value in an object + * structure. + * + * For instance: + * ['a', 'b', '0', 'c'] given { a: { b: [ { c: [] } ] } } => [] + * + * Additionally, an empty selector `[]`, is a special indicator + * for the root level. + */ +export type ProcessorSelector = string[]; + +/** @private */ +export interface ProcessorInternal { + id: string; + type: string; + options: { [key: string]: any }; + onFailure?: ProcessorInternal[]; +} + +export { OnFormUpdateArg }; + +export interface FormValidityState { + validate: OnFormUpdateArg['validate']; +} + +export interface OnUpdateHandlerArg extends FormValidityState { + getData: () => SerializeResult; +} + +/** + * The editor can be in different modes. This enables us to hold + * a reference to data dispatch to the reducer (like the {@link ProcessorSelector} + * which will be used to update the in-memory processors data structure. + */ +export type EditorMode = + | { id: 'creatingProcessor'; arg: { selector: ProcessorSelector } } + | { id: 'movingProcessor'; arg: ProcessorInfo } + | { id: 'editingProcessor'; arg: { processor: ProcessorInternal; selector: ProcessorSelector } } + | { id: 'removingProcessor'; arg: { selector: ProcessorSelector } } + | { id: 'idle' }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/utils.test.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/utils.test.ts new file mode 100644 index 0000000000000..0b7620f517161 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/utils.test.ts @@ -0,0 +1,36 @@ +/* + * 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 { getValue, setValue } from './utils'; + +describe('get and set values', () => { + const testObject = Object.freeze([{ onFailure: [{ onFailure: 1 }] }]); + describe('#getValue', () => { + it('gets a deeply nested value', () => { + expect(getValue(['0', 'onFailure', '0', 'onFailure'], testObject)).toBe(1); + }); + + it('empty array for path returns "root" value', () => { + const result = getValue([], testObject); + expect(result).toEqual(testObject); + // Getting does not create a copy + expect(result).toBe(testObject); + }); + }); + + describe('#setValue', () => { + it('sets a deeply nested value', () => { + const result = setValue(['0', 'onFailure', '0', 'onFailure'], testObject, 2); + expect(result).toEqual([{ onFailure: [{ onFailure: 2 }] }]); + expect(result).not.toBe(testObject); + }); + + it('returns value if no path was provided', () => { + setValue([], testObject, 2); + expect(testObject).toEqual([{ onFailure: [{ onFailure: 1 }] }]); + }); + }); +}); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/utils.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/utils.ts new file mode 100644 index 0000000000000..49d24e8dc35c3 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/utils.ts @@ -0,0 +1,101 @@ +/* + * 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 { ProcessorSelector } from './types'; + +type Path = string[]; + +/** + * The below get and set functions are built with an API to make setting + * and getting and setting values more simple. + * + * @remark + * NEVER use these with objects that contain keys created by user input. + */ + +/** + * Given a path, get the value at the path + * + * @remark + * If path is an empty array, return the source. + */ +export const getValue = (path: Path, source: any) => { + let current = source; + for (const key of path) { + current = (current as any)[key]; + } + return (current as unknown) as Result; +}; + +const ARRAY_TYPE = Object.prototype.toString.call([]); +const OBJECT_TYPE = Object.prototype.toString.call({}); + +const dumbCopy = (value: R): R => { + const objectType = Object.prototype.toString.call(value); + if (objectType === ARRAY_TYPE) { + return ([...(value as any)] as unknown) as R; + } else if (objectType === OBJECT_TYPE) { + return { ...(value as any) } as R; + } + + throw new Error(`Expected (${ARRAY_TYPE}|${OBJECT_TYPE}) but received ${objectType}`); +}; + +const WHITELISTED_KEYS_REGEX = /^([0-9]+|onFailure|processors)$/; +/** + * Given a path, value and an object (array or object) set + * the value at the path and copy objects values on the + * path only. This is a partial copy mechanism that is best + * effort for providing state updates to the UI, could break down + * if other updates are made to non-copied parts of state in external + * references - but this should not happen. + * + * @remark + * If path is empty, just shallow copy source. + */ +export const setValue = ( + path: Path, + source: Target, + value: Value +): Target => { + if (!path.length) { + return dumbCopy(source); + } + + let current: any; + let result: Target; + + for (let idx = 0; idx < path.length; ++idx) { + const key = path[idx]; + if (!WHITELISTED_KEYS_REGEX.test(key)) { + // eslint-disable-next-line no-console + console.error( + `Received non-whitelisted key "${key}". Aborting set value operation; returning original.` + ); + return dumbCopy(source); + } + const atRoot = !current; + + if (atRoot) { + result = dumbCopy(source); + current = result; + } + + if (idx + 1 === path.length) { + current[key] = value; + } else { + current[key] = dumbCopy(current[key]); + current = current[key]; + } + } + + return result!; +}; + +export const checkIfSamePath = (pathA: ProcessorSelector, pathB: ProcessorSelector) => { + if (pathA.length !== pathB.length) return false; + return pathA.join('.') === pathB.join('.'); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/shared_imports.ts b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts index ab56ae427120b..9ddb953c71978 100644 --- a/x-pack/plugins/ingest_pipelines/public/shared_imports.ts +++ b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts @@ -29,7 +29,13 @@ export { Form, getUseField, ValidationFuncArg, + FormData, + UseField, + FormHook, useFormContext, + FormDataProvider, + OnFormUpdateArg, + FieldConfig, } from '../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; export { @@ -41,6 +47,9 @@ export { getFormRow, Field, JsonEditorField, + FormRow, + ToggleField, + ComboBoxField, } from '../../../../src/plugins/es_ui_shared/static/forms/components'; export { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 8888120d59f88..72f6b7a33709d 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -8521,28 +8521,18 @@ "xpack.ingestPipelines.form.nameDescription": "このパイプラインの固有の識別子です。", "xpack.ingestPipelines.form.nameFieldLabel": "名前", "xpack.ingestPipelines.form.nameTitle": "名前", - "xpack.ingestPipelines.form.onFailureDescription": "プロセッサーが失敗した後に実行する代替プロセッサー。{learnMoreLink}", - "xpack.ingestPipelines.form.onFailureDocumentionLink": "詳細", - "xpack.ingestPipelines.form.onFailureFieldAriaLabel": "障害プロセッサーJSONエディター", "xpack.ingestPipelines.form.onFailureFieldHelpText": "JSONフォーマットを使用:{code}", "xpack.ingestPipelines.form.onFailureFieldLabel": "障害プロセッサー(任意)", "xpack.ingestPipelines.form.onFailureProcessorsJsonError": "入力が無効です。", - "xpack.ingestPipelines.form.onFailureTitle": "障害プロセッサー", - "xpack.ingestPipelines.form.onFailureToggleDescription": "障害プロセッサーを追加", "xpack.ingestPipelines.form.pipelineNameRequiredError": "名前が必要です。", - "xpack.ingestPipelines.form.processorsDocumentionLink": "詳細", - "xpack.ingestPipelines.form.processorsFieldAriaLabel": "プロセッサーJSONエディター", - "xpack.ingestPipelines.form.processorsFieldDescription": "インデックスの前にドキュメントを変換するために使用するプロセッサー。{learnMoreLink}", "xpack.ingestPipelines.form.processorsFieldHelpText": "JSONフォーマットを使用:{code}", "xpack.ingestPipelines.form.processorsFieldLabel": "プロセッサー", - "xpack.ingestPipelines.form.processorsFieldTitle": "プロセッサー", "xpack.ingestPipelines.form.processorsJsonError": "入力が無効です。", "xpack.ingestPipelines.form.processorsRequiredError": "プロセッサーが必要です。", "xpack.ingestPipelines.form.saveButtonLabel": "パイプラインを保存", "xpack.ingestPipelines.form.savePipelineError": "パイプラインを作成できません", "xpack.ingestPipelines.form.savingButtonLabel": "保存中…", "xpack.ingestPipelines.form.showRequestButtonLabel": "リクエストを表示", - "xpack.ingestPipelines.form.testPipelineButtonLabel": "パイプラインをテスト", "xpack.ingestPipelines.form.versionFieldLabel": "バージョン(任意)", "xpack.ingestPipelines.form.versionToggleDescription": "バージョン番号を追加", "xpack.ingestPipelines.licenseCheckErrorMessage": "ライセンス確認失敗", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 3109bab4b9426..9845f41e0f771 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -8525,28 +8525,18 @@ "xpack.ingestPipelines.form.nameDescription": "此管道的唯一标识符。", "xpack.ingestPipelines.form.nameFieldLabel": "名称", "xpack.ingestPipelines.form.nameTitle": "名称", - "xpack.ingestPipelines.form.onFailureDescription": "处理器失败后要执行的备用处理器。{learnMoreLink}", - "xpack.ingestPipelines.form.onFailureDocumentionLink": "了解详情", - "xpack.ingestPipelines.form.onFailureFieldAriaLabel": "失败处理器 JSON 编辑器", "xpack.ingestPipelines.form.onFailureFieldHelpText": "使用 JSON 格式:{code}", "xpack.ingestPipelines.form.onFailureFieldLabel": "失败处理器(可选)", "xpack.ingestPipelines.form.onFailureProcessorsJsonError": "输入无效。", - "xpack.ingestPipelines.form.onFailureTitle": "失败处理器", - "xpack.ingestPipelines.form.onFailureToggleDescription": "添加失败处理器", "xpack.ingestPipelines.form.pipelineNameRequiredError": "“名称”必填。", - "xpack.ingestPipelines.form.processorsDocumentionLink": "了解详情", - "xpack.ingestPipelines.form.processorsFieldAriaLabel": "处理器 JSON 编辑器", - "xpack.ingestPipelines.form.processorsFieldDescription": "用于在索引之前转换文档的处理器。{learnMoreLink}", "xpack.ingestPipelines.form.processorsFieldHelpText": "使用 JSON 格式:{code}", "xpack.ingestPipelines.form.processorsFieldLabel": "处理器", - "xpack.ingestPipelines.form.processorsFieldTitle": "处理器", "xpack.ingestPipelines.form.processorsJsonError": "输入无效。", "xpack.ingestPipelines.form.processorsRequiredError": "需要指定处理器。", "xpack.ingestPipelines.form.saveButtonLabel": "保存管道", "xpack.ingestPipelines.form.savePipelineError": "无法创建管道", "xpack.ingestPipelines.form.savingButtonLabel": "正在保存......", "xpack.ingestPipelines.form.showRequestButtonLabel": "显示请求", - "xpack.ingestPipelines.form.testPipelineButtonLabel": "测试管道", "xpack.ingestPipelines.form.versionFieldLabel": "版本(可选)", "xpack.ingestPipelines.form.versionToggleDescription": "添加版本号", "xpack.ingestPipelines.licenseCheckErrorMessage": "许可证检查失败", diff --git a/x-pack/test/functional/apps/ingest_pipelines/ingest_pipelines.ts b/x-pack/test/functional/apps/ingest_pipelines/ingest_pipelines.ts index e926bcd6ef997..353902f4265a0 100644 --- a/x-pack/test/functional/apps/ingest_pipelines/ingest_pipelines.ts +++ b/x-pack/test/functional/apps/ingest_pipelines/ingest_pipelines.ts @@ -11,22 +11,6 @@ const PIPELINE = { name: 'test_pipeline', description: 'My pipeline description.', version: 1, - processors: JSON.stringify([ - { - set: { - field: 'foo', - value: 'new', - }, - }, - ]), - onFailureProcessors: JSON.stringify([ - { - set: { - field: '_index', - value: 'failed-{{ _index }}', - }, - }, - ]), }; export default ({ getPageObjects, getService }: FtrProviderContext) => {