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 116c360bc3980..703ffd1589a56 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, useCallback } from 'react'; +import React, { useState, useCallback, useRef } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; @@ -45,7 +45,7 @@ export const PipelineForm: React.FunctionComponent = ({ const [isTestingPipeline, setIsTestingPipeline] = useState(false); - const [processorsEditorState, setProcessorsEditorState] = useState(); + const processorStateRef = useRef(); const handleSave: FormConfig['onSubmit'] = async (formData, isValid) => { let override: any = {}; @@ -54,9 +54,10 @@ export const PipelineForm: React.FunctionComponent = ({ return; } - if (processorsEditorState) { - if (processorsEditorState.isValid === undefined && (await processorsEditorState.validate())) { - override = processorsEditorState.getData(); + if (processorStateRef.current) { + const processorsState = processorStateRef.current; + if (await processorsState.validate()) { + override = processorsState.getData(); } else { return; } @@ -93,8 +94,8 @@ export const PipelineForm: React.FunctionComponent = ({ ); const onProcessorsChangeHandler = useCallback( - arg => setProcessorsEditorState(arg), - [setProcessorsEditorState] + arg => (processorStateRef.current = arg), + [] ); return ( diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/drag_and_drop_tree/drag_and_drop_tree.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/drag_and_drop_tree/drag_and_drop_tree.tsx index c51304b275bc4..88bea2667eabf 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/drag_and_drop_tree/drag_and_drop_tree.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/drag_and_drop_tree/drag_and_drop_tree.tsx @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FunctionComponent } from 'react'; +import React, { FunctionComponent, useState, memo } from 'react'; import { EuiDragDropContext, EuiDroppable } from '@elastic/eui'; import { ProcessorInternal, DraggableLocation, ProcessorSelector } from '../../types'; -import { mapDestinationIndexToTreeLocation } from './utils'; +import { resolveDestinationLocation } from './utils'; import { TreeNode, TreeNodeComponentArgs } from './tree_node'; @@ -21,20 +21,31 @@ interface OnDragEndArgs { export interface Props { processors: ProcessorInternal[]; onDragEnd: (args: OnDragEndArgs) => void; - nodeComponent: (arg: TreeNodeComponentArgs) => React.ReactNode; + renderItem: (arg: TreeNodeComponentArgs) => React.ReactNode; } /** This value comes from the {@link ProcessorInternal} type */ const ON_FAILURE = 'onFailure'; -export const DragAndDropTree: FunctionComponent = ({ +/** + * Takes in array of {@link ProcessorInternal} and renders a drag and drop tree. + * + * @remark + * Because of issues with nesting EuiDroppable (based on react-beautiful-dnd) we render + * a flat structure with one droppable. This component is responsible for maintaining the + * {@link ProcessorSelector}s back to the nested structure so that it can emit instructions + * the reducer will understand. + */ +export const DragAndDropTreeUI: FunctionComponent = ({ processors, onDragEnd, - nodeComponent, + renderItem, }) => { let flatTreeIndex = 0; const items: Array<[ProcessorSelector, React.ReactElement]> = []; + const [currentDragSelector, setCurrentDragSelector] = useState(); + const addRenderedItems = ( _processors: ProcessorInternal[], _selector: ProcessorSelector, @@ -51,11 +62,11 @@ export const DragAndDropTree: FunctionComponent = ({ level={level} processor={processor} selector={nodeSelector} - component={nodeComponent} + component={renderItem} />, ]); - if (processor.onFailure?.length) { + if (processor.onFailure?.length && nodeSelector.join('.') !== currentDragSelector) { addRenderedItems(processor.onFailure, nodeSelector.concat(ON_FAILURE), level + 1); } }); @@ -65,7 +76,13 @@ export const DragAndDropTree: FunctionComponent = ({ return ( { + onBeforeCapture={({ draggableId: serializedSelector }) => { + setCurrentDragSelector(serializedSelector); + }} + onDragEnd={arg => { + setCurrentDragSelector(undefined); + + const { source, destination, combine } = arg; if (source && combine) { const [sourceSelector] = items[source.index]; const destinationSelector = combine.draggableId.split('.'); @@ -89,7 +106,7 @@ export const DragAndDropTree: FunctionComponent = ({ index: parseInt(sourceSelector[sourceSelector.length - 1], 10), selector: sourceSelector.slice(0, -1), }, - destination: mapDestinationIndexToTreeLocation( + destination: resolveDestinationLocation( items.map(([selector]) => selector), !sourceSelector.slice(0, -1).length, destination.index @@ -104,3 +121,7 @@ export const DragAndDropTree: FunctionComponent = ({ ); }; + +export const DragAndDropTree = memo(DragAndDropTreeUI, (prev, current) => { + return prev.processors === current.processors && prev.onDragEnd === current.onDragEnd; +}); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/drag_and_drop_tree/tree_node.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/drag_and_drop_tree/tree_node.tsx index bffcd0472fd60..71ee68ceee3dd 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/drag_and_drop_tree/tree_node.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/drag_and_drop_tree/tree_node.tsx @@ -32,23 +32,21 @@ export const TreeNode: FunctionComponent = ({ }) => { const id = selector.join('.'); return ( - <> - - {provided => ( - - - - -
- -
-
- {component({ processor, selector })} -
+ + {provided => ( + + + + +
+ +
+
+ {component({ processor, selector })}
-
- )} -
- +
+
+ )} +
); }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/drag_and_drop_tree/utils.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/drag_and_drop_tree/utils.ts index 91614527d1f3f..476d00de6cce2 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/drag_and_drop_tree/utils.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/drag_and_drop_tree/utils.ts @@ -6,7 +6,7 @@ import { DraggableLocation, ProcessorSelector } from '../../types'; -export const mapDestinationIndexToTreeLocation = ( +export const resolveDestinationLocation = ( items: ProcessorSelector[], isRootLevelSource: boolean, destinationIndex: number diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/data_in.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/data_in.ts index 9bd3850a34357..241f68f382467 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/data_in.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/data_in.ts @@ -23,10 +23,10 @@ const getProcessorType = (processor: Processor): string => { const convertToPipelineInternalProcessor = (processor: Processor): ProcessorInternal => { const type = getProcessorType(processor); - const options = processor[type]; - const onFailure = options.on_failure?.length - ? convertProcessors(options.on_failure) - : (options.on_failure as ProcessorInternal[] | undefined); + const { on_failure: originalOnFailure, ...options } = processor[type]; + const onFailure = originalOnFailure?.length + ? convertProcessors(originalOnFailure) + : (originalOnFailure as ProcessorInternal[] | undefined); return { type, onFailure, 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 index 38485648e288c..49fb3dc204403 100644 --- 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 @@ -9,16 +9,20 @@ import { EuiButton } from '@elastic/eui'; import { Processor } from '../../../../common/types'; +import { OnFormUpdateArg } from '../../../shared_imports'; + import { SettingsFormFlyout, DragAndDropTree, PipelineProcessorEditorItem } from './components'; import { deserialize } from './data_in'; import { serialize, SerializeResult } from './data_out'; -import { useProcessorsState } from './reducer'; +import { useProcessorsState } from './processors_reducer'; import { ProcessorInternal, ProcessorSelector } from './types'; -export interface OnUpdateHandlerArg { +interface FormState { + validate: OnFormUpdateArg['validate']; +} + +export interface OnUpdateHandlerArg extends FormState { getData: () => SerializeResult; - validate: () => Promise; - isValid?: boolean; } export type OnUpdateHandler = (arg: OnUpdateHandlerArg) => void; @@ -56,21 +60,27 @@ export const PipelineProcessorsEditor: FunctionComponent = ({ const [state, dispatch] = useProcessorsState(dataInResult); const { processors } = state; - useEffect(() => { - onUpdate({ - isValid: state.isValid, - validate: state.validate, - getData: () => serialize(state), - }); - }, [state, onUpdate]); + const [formState, setFormState] = useState({ + validate: () => Promise.resolve(true), + }); const onFormUpdate = useCallback( arg => { - dispatch({ type: 'processorForm.update', payload: arg }); + setFormState({ validate: arg.validate }); }, - [dispatch] + [setFormState] ); + useEffect(() => { + onUpdate({ + validate: async () => { + const formValid = await formState.validate(); + return formValid && mode.id === 'idle'; + }, + getData: () => serialize(state), + }); + }, [state, onUpdate, formState, mode]); + const onSubmit = useCallback( processorTypeAndOptions => { switch (mode.id) { @@ -108,6 +118,16 @@ export const PipelineProcessorsEditor: FunctionComponent = ({ [dispatch, mode] ); + const onDragEnd = useCallback( + args => { + dispatch({ + type: 'moveProcessor', + payload: args, + }); + }, + [dispatch] + ); + const dismissFlyout = () => { setMode({ id: 'idle' }); }; @@ -115,14 +135,9 @@ export const PipelineProcessorsEditor: FunctionComponent = ({ return ( <> { - dispatch({ - type: 'moveProcessor', - payload: args, - }); - }} + onDragEnd={onDragEnd} processors={processors} - nodeComponent={({ processor, selector }) => ( + renderItem={({ processor, selector }) => ( { switch (type) { @@ -156,7 +171,7 @@ export const PipelineProcessorsEditor: FunctionComponent = ({ processor={mode.id === 'editingProcessor' ? mode.arg.processor : undefined} onClose={() => { dismissFlyout(); - dispatch({ type: 'processorForm.close' }); + setFormState({ validate: () => Promise.resolve(true) }); }} /> ) : ( diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/reducer.test.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer.test.ts similarity index 98% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/reducer.test.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer.test.ts index 9f21e08f3a3f6..85ea6c1ccd588 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/reducer.test.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer.test.ts @@ -4,11 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { reducer, State } from './reducer'; +import { reducer, State } from './processors_reducer'; const initialState: State = { processors: [], - validate: () => Promise.resolve(true), }; describe('Processors reducer', () => { diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/reducer.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer.ts similarity index 83% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/reducer.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer.ts index b4f5fc5629d26..4921b6ac5a28b 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/reducer.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/processors_reducer.ts @@ -6,18 +6,11 @@ import { Reducer, useReducer } from 'react'; import { euiDragDropReorder } from '@elastic/eui'; -import { OnFormUpdateArg } from '../../../shared_imports'; - import { DeserializeResult } from './data_in'; import { getValue, setValue, unsafeProcessorMove, PARENT_CHILD_NEST_ERROR } from './utils'; import { ProcessorInternal, DraggableLocation, ProcessorSelector } from './types'; -type StateArg = DeserializeResult; - -export interface State extends StateArg { - isValid?: boolean; - validate: () => Promise; -} +export type State = DeserializeResult; type Action = | { @@ -42,10 +35,7 @@ type Action = | { type: 'moveProcessor'; payload: { source: DraggableLocation; destination: DraggableLocation }; - } - // Does not quite belong here, but using in reducer for convenience - | { type: 'processorForm.update'; payload: OnFormUpdateArg } - | { type: 'processorForm.close' }; + }; const addSelectorRoot = (selector: ProcessorSelector): ProcessorSelector => { return ['processors'].concat(selector); @@ -126,16 +116,8 @@ export const reducer: Reducer = (state, action) => { return setValue(processorsSelector, state, processors); } - if (action.type === 'processorForm.update') { - return { ...state, ...action.payload }; - } - - if (action.type === 'processorForm.close') { - return { ...state, isValid: undefined, validate: () => Promise.resolve(true) }; - } - return state; }; -export const useProcessorsState = (initialState: StateArg) => - useReducer(reducer, { ...initialState, validate: () => Promise.resolve(true) }); +export const useProcessorsState = (initialState: State) => + useReducer(reducer, { ...initialState });