diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/add_processor_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/add_processor_form.tsx index 5231a3d17811b..b663daedd9b9c 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/add_processor_form.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/add_processor_form.tsx @@ -118,6 +118,7 @@ export const AddProcessorForm: FunctionComponent = ({ { await handleSubmit(); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/edit_processor_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/edit_processor_form.tsx index e449ed75b6343..d9feaaffa5aec 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/edit_processor_form.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/edit_processor_form.tsx @@ -234,6 +234,7 @@ export const EditProcessorForm: FunctionComponent = ({ { if (activeTab === 'output') { diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/field_components/drag_and_drop_text_list.scss b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/field_components/drag_and_drop_text_list.scss new file mode 100644 index 0000000000000..2f563d86a6d4a --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/field_components/drag_and_drop_text_list.scss @@ -0,0 +1,28 @@ +.pipelineProcessorsEditor__form__dragAndDropList { + &__panel { + background-color: $euiColorLightestShade; + padding: $euiSizeM; + } + + &__grabIcon { + margin-right: $euiSizeS; + } + + &__removeButton { + margin-left: $euiSizeS; + } + + &__errorIcon { + margin-left: -$euiSizeXL; + } + + &__item { + background-color: $euiColorLightestShade; + padding-top: $euiSizeS; + padding-bottom: $euiSizeS; + } + + &__labelContainer { + margin-bottom: $euiSizeXS; + } +} diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/field_components/drag_and_drop_text_list.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/field_components/drag_and_drop_text_list.tsx new file mode 100644 index 0000000000000..63e1fdaa9a8f0 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/field_components/drag_and_drop_text_list.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 { i18n } from '@kbn/i18n'; +import React, { useState, useCallback, memo } from 'react'; +import uuid from 'uuid'; +import { + EuiButtonEmpty, + EuiButtonIcon, + EuiDragDropContext, + EuiDraggable, + EuiDroppable, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiFieldText, + EuiIconTip, + EuiFormRow, + EuiText, +} from '@elastic/eui'; + +import { + UseField, + ArrayItem, + ValidationFunc, + getFieldValidityAndErrorMessage, +} from '../../../../../../shared_imports'; + +import './drag_and_drop_text_list.scss'; + +interface Props { + label: string; + helpText: React.ReactNode; + error: string | null; + value: ArrayItem[]; + onMove: (sourceIdx: number, destinationIdx: number) => void; + onAdd: () => void; + onRemove: (id: number) => void; + addLabel: string; + /** + * Validation to be applied to every text item + */ + textValidation?: ValidationFunc; +} + +const i18nTexts = { + removeItemButtonAriaLabel: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.dragAndDropList.removeItemLabel', + { defaultMessage: 'Remove item' } + ), +}; + +function DragAndDropTextListComponent({ + label, + helpText, + error, + value, + onMove, + onAdd, + onRemove, + addLabel, + textValidation, +}: Props): JSX.Element { + const [droppableId] = useState(() => uuid.v4()); + const [firstItemId] = useState(() => uuid.v4()); + + const onDragEnd = useCallback( + ({ source, destination }) => { + if (source && destination) { + onMove(source.index, destination.index); + } + }, + [onMove] + ); + return ( + + <> + {/* Label and help text. Also wire up the htmlFor so the label points to the first text field. */} + + + + + + + + +

{helpText}

+
+
+
+ + {/* The processor panel */} +
+ + + {value.map((item, idx) => { + return ( + + {(provided) => { + return ( + + +
+ +
+
+ + + path={item.path} + config={{ + validations: textValidation + ? [{ validator: textValidation }] + : undefined, + }} + readDefaultValueOnForm={!item.isNew} + > + {(field) => { + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage( + field + ); + return ( + + + + + {typeof errorMessage === 'string' && ( + +
+ +
+
+ )} +
+ ); + }} + +
+ + {value.length > 1 ? ( + onRemove(item.id)} + /> + ) : ( + // Render a no-op placeholder button + + )} + +
+ ); + }} +
+ ); + })} +
+
+ + {addLabel} + +
+ +
+ ); +} + +export const DragAndDropTextList = memo( + DragAndDropTextListComponent +) as typeof DragAndDropTextListComponent; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/field_components/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/field_components/index.ts index 6ce9eefd26445..605568f90ce9f 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/field_components/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/field_components/index.ts @@ -4,5 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ +export { DragAndDropTextList } from './drag_and_drop_text_list'; export { XJsonEditor } from './xjson_editor'; export { TextEditor } from './text_editor'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/field_components/text_editor.scss b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/field_components/text_editor.scss new file mode 100644 index 0000000000000..f48e19fd0e635 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/field_components/text_editor.scss @@ -0,0 +1,5 @@ +.pipelineProcessorsEditor__form__textEditor { + &__panel { + box-shadow: none; + } +} diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/field_components/text_editor.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/field_components/text_editor.tsx index 1d0e36c0d526c..88b4a0aa2be06 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/field_components/text_editor.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/field_components/text_editor.tsx @@ -13,6 +13,8 @@ import { getFieldValidityAndErrorMessage, } from '../../../../../../shared_imports'; +import './text_editor.scss'; + interface Props { field: FieldHook; editorProps: { [key: string]: any }; @@ -30,7 +32,11 @@ export const TextEditor: FunctionComponent = ({ field, editorProps }) => error={errorMessage} fullWidth > - + diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processor_form.container.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processor_form.container.tsx index c3b1799ac2a28..25c9579e3c48e 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processor_form.container.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processor_form.container.tsx @@ -60,6 +60,7 @@ export const ProcessorFormContainer: FunctionComponent = ({ const { form } = useForm({ defaultValue: { fields: getProcessor().options }, }); + const { subscribe } = form; const handleSubmit = useCallback( async (shouldCloseFlyout: boolean = true) => { @@ -92,14 +93,9 @@ export const ProcessorFormContainer: FunctionComponent = ({ }, [onSubmit, processor]); useEffect(() => { - const subscription = form.subscribe(onFormUpdate); + const subscription = 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]); + }, [onFormUpdate, subscribe]); if (processor) { return ( diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/common_fields/processor_type_field.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/common_fields/processor_type_field.tsx index 3264923442886..5b3df63a11294 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/common_fields/processor_type_field.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/common_fields/processor_type_field.tsx @@ -14,6 +14,7 @@ import { FieldConfig, UseField, fieldValidators, + useKibana, } from '../../../../../../../shared_imports'; import { getProcessorDescriptor, mapProcessorTypeToDescriptor } from '../../../shared'; @@ -64,6 +65,10 @@ const typeConfig: FieldConfig = { }; export const ProcessorTypeField: FunctionComponent = ({ initialType }) => { + const { + services: { documentation }, + } = useKibana(); + const esDocUrl = documentation.getEsDocsBasePath(); return ( config={typeConfig} defaultValue={initialType} path="type"> {(typeField) => { @@ -107,7 +112,7 @@ export const ProcessorTypeField: FunctionComponent = ({ initialType }) => {}; + (this as any).terminate = () => {}; +}; + +describe('', () => { + const setup = (props?: { defaultValue: Record }) => { + function MyComponent() { + const { form } = useForm({ defaultValue: props?.defaultValue }); + const i18n = i18nServiceMock.createStartContract(); + return ( + + +
+ + +
+
+ ); + } + return mount(); + }; + + beforeAll(() => { + // disable all react-beautiful-dnd development warnings + (window as any)['__react-beautiful-dnd-disable-dev-warnings'] = true; + }); + + afterAll(() => { + // enable all react-beautiful-dnd development warnings + (window as any)['__react-beautiful-dnd-disable-dev-warnings'] = false; + }); + test('smoke', () => { + setup({ defaultValue: { type: 'grok', fields: { patterns: ['test'] } } }); + }); +}); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/grok.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/grok.tsx index c5c6adbe2a7a8..5df30be3407a2 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/grok.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/grok.tsx @@ -10,24 +10,46 @@ import { i18n } from '@kbn/i18n'; import { FIELD_TYPES, UseField, - ComboBoxField, + UseArray, ToggleField, fieldValidators, + ValidationFunc, + ArrayItem, } from '../../../../../../shared_imports'; -import { XJsonEditor } from '../field_components'; +import { XJsonEditor, DragAndDropTextList } from '../field_components'; import { FieldNameField } from './common_fields/field_name_field'; import { IgnoreMissingField } from './common_fields/ignore_missing_field'; import { FieldsConfig, to, from, EDITOR_PX_HEIGHT } from './shared'; -const { emptyField, isJsonField } = fieldValidators; +const { isJsonField, emptyField } = fieldValidators; + +const i18nTexts = { + addPatternLabel: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.grokForm.patternsAddPatternLabel', + { defaultMessage: 'Add pattern' } + ), +}; + +const valueRequiredMessage = i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.grokForm.patternsValueRequiredError', + { defaultMessage: 'A value is required.' } +); + +const patternsValidation: ValidationFunc = ({ value, formData }) => { + if (value.length === 0) { + return { + message: valueRequiredMessage, + }; + } +}; + +const patternValidation = emptyField(valueRequiredMessage); const fieldsConfig: FieldsConfig = { /* Required field configs */ patterns: { - type: FIELD_TYPES.COMBO_BOX, - deserializer: to.arrayOfStrings, label: i18n.translate('xpack.ingestPipelines.pipelineEditor.grokForm.patternsFieldLabel', { defaultMessage: 'Patterns', }), @@ -37,12 +59,7 @@ const fieldsConfig: FieldsConfig = { }), validations: [ { - validator: emptyField( - i18n.translate( - 'xpack.ingestPipelines.pipelineEditor.grokForm.patternsValueRequiredError', - { defaultMessage: 'A value is required.' } - ) - ), + validator: patternsValidation as ValidationFunc, }, ], }, @@ -103,7 +120,23 @@ export const Grok: FunctionComponent = () => { )} /> - + + {({ items, addItem, removeItem, moveItem, error }) => { + return ( + + ); + }} + ReactNode); } type MapProcessorTypeToDescriptor = Record; @@ -176,11 +175,7 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = { label: i18n.translate('xpack.ingestPipelines.processors.label.enrich', { defaultMessage: 'Enrich', }), - description: function Description() { - const { - services: { documentation }, - } = useKibana(); - const esDocUrl = documentation.getEsDocsBasePath(); + description: (esDocUrl) => { return ( { return ( _useKibana();