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 be1fbcd76ae9d..67711c74a3d0c 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 @@ -89,6 +89,7 @@ export const PipelineForm: React.FunctionComponent = ({ if (processorsEditorState.isValid === undefined) { (async () => { const valid = await processorsEditorState.validate(); + const { processors } = processorsEditorState.getData(); if (valid) { onSave({ ...formData, processors } as Pipeline); } @@ -100,7 +101,7 @@ export const PipelineForm: React.FunctionComponent = ({ return; } - const { processors } = processorsEditorState!.getData(); + const { processors } = processorsEditorState.getData(); onSave({ ...formData, processors } as Pipeline); } else { onSave(formData as Pipeline); 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 6ab351c0111ef..8e03b9cc84f8a 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 @@ -7,29 +7,65 @@ import React, { FunctionComponent } from 'react'; import { EuiDragDropContext, EuiDroppable, EuiPanel } from '@elastic/eui'; -import { ProcessorInternal } from '../../types'; +import { ProcessorInternal, DraggableLocation } from '../../types'; + import { TreeNode } from './tree_node'; +interface OnDragEndArgs { + source: DraggableLocation; + destination: DraggableLocation; +} + export interface Props { - processors: ProcessorInternal; + processors: ProcessorInternal[]; + onDragEnd: (args: OnDragEndArgs) => void; } -export interface PrivateProps extends Props { +export interface PrivateProps extends Omit { pathSelector: string; } -const PrivateDragAndDropTree: FunctionComponent = ({ - processors, - pathSelector, -}) => { - return - - - {} - - +const ROOT_PATH_ID = 'ROOT_PATH_ID'; + +const PrivateDragAndDropTree: FunctionComponent = ({ processors, pathSelector }) => { + return ( + + + {processors.map((processor, idx) => { + return ( + + ); + })} + + + ); }; -export const DragAndDropTree: FunctionComponent = ({ processors }) => { - return = ({ processors, onDragEnd }) => { + return ( + { + if (source && destination) { + onDragEnd({ + source: { + index: source.index, + pathSelector: source.droppableId === ROOT_PATH_ID ? undefined : source.droppableId, + }, + destination: { + index: destination.index, + pathSelector: + destination.droppableId === ROOT_PATH_ID ? undefined : destination.droppableId, + }, + }); + } + }} + > + + + ); }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/drag_and_drop_tree/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/drag_and_drop_tree/index.ts index 41bc2aa258807..79961dfa587a4 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/drag_and_drop_tree/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/drag_and_drop_tree/index.ts @@ -3,3 +3,5 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + +export { DragAndDropTree } from './drag_and_drop_tree'; 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 b4f66f377c1b0..299a4411f7d4e 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 @@ -5,13 +5,9 @@ */ import React, { FunctionComponent } from 'react'; -import { ProcessorInternal } from '../../types'; import { - EuiButton, EuiButtonEmpty, - EuiDragDropContext, EuiDraggable, - EuiDroppable, EuiFlexGroup, EuiFlexItem, EuiIcon, @@ -19,85 +15,53 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -interface OnDragEndArgs { - pathSelector: string; - sourceIndex: number; - destinationIndex: number; -} +import { ProcessorInternal } from '../../types'; interface Props { pathSelector: string; - processors: ProcessorInternal[]; - onDragEnd: (args: OnDragEndArgs) => void; + processor: ProcessorInternal; + index: number; } -export const TreeNode: FunctionComponent = ({ processors, onDragEnd, pathSelector }) => { +export const TreeNode: FunctionComponent = ({ processor, pathSelector, index }) => { return ( - - { - if (source && destination) { - onDragEnd({ - pathSelector, - sourceIndex: source.index, - destinationIndex: destination.index, - }); - // dispatch({ - // type: 'reorderProcessors', - // payload: { sourceIdx: source.index, destIdx: destination.index }, - // }); - } - }} - > - - {processors.map((processor, idx) => { - const { type, id } = processor; - return ( - + {provided => ( + + + +
+ +
+
+ {processor.type} + + {}}> + {i18n.translate('xpack.ingestPipelines.pipelineEditor.editProcessorButtonLabel', { + defaultMessage: 'Edit', + })} + + {} + // dispatch({ type: 'removeProcessor', payload: { processor } }) + } > - {provided => ( - - - -
- -
-
- {type} - - setSelectedProcessor(processor)}> - {i18n.translate( - 'xpack.ingestPipelines.pipelineEditor.editProcessorButtonLabel', - { defaultMessage: 'Edit' } - )} - - - dispatch({ type: 'removeProcessor', payload: { processor } }) - } - > - {i18n.translate( - 'xpack.ingestPipelines.pipelineEditor.deleteProcessorButtonLabel', - { defaultMessage: 'Delete' } - )} - - -
-
- )} -
- ); - })} -
-
- {/* TODO: Translate */} - setIsAddingNewProcessor(true)}>Add a processor -
+ {i18n.translate('xpack.ingestPipelines.pipelineEditor.deleteProcessorButtonLabel', { + defaultMessage: 'Delete', + })} + + + + + )} + ); }; 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 index 15afa01cac543..367d3d5879ea4 100644 --- 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 @@ -5,4 +5,7 @@ */ export { SettingsFormFlyout } from './settings_form_flyout'; + export { ProcessorSettingsForm, ProcessorSettingsFromOnSubmitArg } from './processor_settings_form'; + +export { DragAndDropTree } from './drag_and_drop_tree'; 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 6863ffcea3cd1..3acf2a3ed2abf 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 @@ -4,23 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { i18n } from '@kbn/i18n'; import React, { FunctionComponent, useState, useMemo, useEffect, useCallback } from 'react'; -import { - EuiPanel, - EuiFlexGroup, - EuiFlexItem, - EuiDragDropContext, - EuiDroppable, - EuiDraggable, - EuiIcon, - EuiButton, - EuiButtonEmpty, -} from '@elastic/eui'; +import { EuiPanel, EuiButton } from '@elastic/eui'; import { Processor } from '../../../../common/types'; -import { SettingsFormFlyout } from './components'; +import { SettingsFormFlyout, DragAndDropTree } from './components'; import { deserialize } from './data_in'; import { serialize, SerializeResult } from './data_out'; import { useEditorState } from './reducer'; @@ -79,64 +68,15 @@ export const PipelineProcessorsEditor: FunctionComponent = ({ return ( <> - { - if (source && destination) { - dispatch({ - type: 'reorderProcessors', - payload: { sourceIdx: source.index, destIdx: destination.index }, - }); - } + { + dispatch({ + type: 'moveProcessor', + payload: args, + }); }} - > - - {processors.map((processor, idx) => { - const { type, id } = processor; - return ( - - {provided => ( - - - -
- -
-
- {type} - - setSelectedProcessor(processor)}> - {i18n.translate( - 'xpack.ingestPipelines.pipelineEditor.editProcessorButtonLabel', - { defaultMessage: 'Edit' } - )} - - - dispatch({ type: 'removeProcessor', payload: { processor } }) - } - > - {i18n.translate( - 'xpack.ingestPipelines.pipelineEditor.deleteProcessorButtonLabel', - { defaultMessage: 'Delete' } - )} - - -
-
- )} -
- ); - })} -
-
- {/* TODO: Translate */} + processors={processors} + /> setIsAddingNewProcessor(true)}>Add a processor
{selectedProcessor || isAddingNewProcessor ? ( 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/reducer.ts index 032f28596b559..5cb5555280548 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/reducer.ts @@ -9,7 +9,8 @@ import { euiDragDropReorder } from '@elastic/eui'; import { OnFormUpdateArg } from '../../../shared_imports'; import { createProcessorInternal, DeserializeResult } from './data_in'; -import { ProcessorInternal } from './types'; +import { getValue, setValue } from './utils'; +import { ProcessorInternal, DraggableLocation } from './types'; type StateArg = DeserializeResult; @@ -22,7 +23,10 @@ type Action = | { type: 'addProcessor'; payload: { processor: Omit } } | { type: 'updateProcessor'; payload: { processor: ProcessorInternal } } | { type: 'removeProcessor'; payload: { processor: ProcessorInternal } } - | { type: 'reorderProcessors'; payload: { sourceIdx: number; destIdx: number } } + | { + type: 'moveProcessor'; + payload: { source: DraggableLocation; destination: DraggableLocation }; + } | { type: 'processorForm.update'; payload: OnFormUpdateArg } | { type: 'processorForm.close' }; @@ -34,12 +38,29 @@ const findProcessorIdx = ( }; export const reducer: Reducer = (state, action) => { - if (action.type === 'reorderProcessors') { - const { destIdx, sourceIdx } = action.payload; - return { - ...state, - processors: euiDragDropReorder(state.processors, sourceIdx, destIdx), - }; + if (action.type === 'moveProcessor') { + const { destination, source } = action.payload; + if (destination.pathSelector === source.pathSelector) { + if (source.pathSelector === undefined) { + return { + ...state, + processors: euiDragDropReorder(state.processors, source.index, destination.index), + }; + } else { + const path = source.pathSelector.split('.'); + const processors = getValue(path, state.processors); + return { + ...state, + processors: setValue( + path, + state.processors, + euiDragDropReorder(processors, source.index, destination.index) + ), + }; + } + } else { + return state; + } } if (action.type === 'removeProcessor') { 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 index 0c72c4bd629e6..a86e6256be05b 100644 --- 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 @@ -7,8 +7,12 @@ import { ESCommonProcessorOptions } from '../../../../common/types'; export interface DraggableLocation { - pathSelector: string; index: number; + /** + * When path selector is undefined it means that we are at the tree + * root. + */ + pathSelector?: string; } export type ProcessorOptions = ESCommonProcessorOptions & 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..ec662d14f04ff --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/utils.test.ts @@ -0,0 +1,24 @@ +/* + * 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 immutably with objects and arrays', () => { + const test = Object.freeze([{ a: [{ a: 1 }] }]); + describe('#getValue', () => { + it('gets a deeply nested value', () => { + expect(getValue(['0', 'a', '0', 'a'], test)).toBe(1); + }); + }); + + describe('#setValue', () => { + it('sets a deeply nested value', () => { + const result = setValue(['0', 'a', '0', 'a'], test, 2); + expect(result).toEqual([{ a: [{ a: 2 }] }]); + expect(result).not.toBe(test); + }); + }); +}); 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..8950b631df731 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/utils.ts @@ -0,0 +1,66 @@ +/* + * 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. + */ + +type Path = string[]; + +/** + * The below getter and setter functions are the immutable counter-parts to + * lodash's `get` and `set` functions. They are built specifically to get and + * set values with arrays that contain objects that contain arrays. + * + * 'immer' + lodash was attempted (as this would in theory provide the same + * result) but resulted in an issue with updating the array in place. 'immer' + * can also result in a high performance spike and is NOT call-stack safe. + * + * @remark + * NEVER use these with objects that contain keys created by user input. + */ + +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 = '[object Array]'; +const OBJECT = '[object Object]'; + +const copy = (value: unknown): any => { + const result = Object.prototype.toString.call(value); + if (result === ARRAY) { + return [...(value as any[])]; + } + if (result === OBJECT) { + return { ...(value as object) }; + } + throw Error(`Unrecognized type ${result}`); +}; + +export const setValue = (path: Path, source: Target, value: Value) => { + let current: any; + let result: Value; + + for (let idx = 0; idx < path.length; ++idx) { + const key = path[idx]; + const atRoot = !current; + + if (atRoot) { + result = copy(source); + current = result; + } + + if (idx + 1 === path.length) { + current[key] = value; + } else { + current[key] = copy(current[key]); + current = current[key]; + } + } + + return result!; +};