From 5159635d5b4b61ec07a5bef43d47bb932cef4163 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Fri, 3 Jul 2020 10:43:33 +0200 Subject: [PATCH] [Ingest Pipelines] Load from json (#70297) * WiP load from json modal ready, need to refactor more stuff * First iteration of load from JSON functionality - refactored the pipeline processsors editor components for portability - added CIT for load from json component * added comment * update deserialize with tests and make it more fault tolerant * use flyout footer * remove console.error and make the json editor a lot shorter * address PR feedback - Update form schema and form schema types - simplify the save handler - refactor processors_title to processors_header * remove unused translations Co-authored-by: Elastic Machine --- .../on_failure_processors_title.tsx | 2 +- .../pipeline_form.scss} | 0 .../pipeline_form/pipeline_form.tsx | 65 +++-- .../pipeline_form/pipeline_form_fields.tsx | 78 +++--- .../processors_header.tsx} | 13 +- .../components/pipeline_form/schema.ts | 44 ++++ .../components/pipeline_form/schema.tsx | 138 ---------- .../components/pipeline_form/types.ts | 2 + .../pipeline_processors_editor/README.md | 24 ++ .../pipeline_processors_editor.helpers.tsx | 24 +- .../pipeline_processors_editor.test.tsx | 14 +- .../components/index.ts | 8 +- .../components/load_from_json/button.tsx | 34 +++ .../components/load_from_json/index.ts | 8 + .../load_from_json/modal_provider.test.tsx | 127 ++++++++++ .../load_from_json/modal_provider.tsx | 138 ++++++++++ .../components/pipeline_processors_editor.tsx | 33 +++ .../context_menu.tsx | 10 +- .../{messages.ts => i18n_texts.ts} | 2 +- .../pipeline_processors_editor_item.tsx | 22 +- .../processor_settings_form.tsx | 44 ++-- .../pipeline_processors_editor/constants.ts | 5 + .../pipeline_processors_editor/context.tsx | 221 +++++++++++++++- .../deserialize.test.ts | 74 ++++++ .../pipeline_processors_editor/deserialize.ts | 8 +- .../global_on_failure_processors_editor.tsx | 13 + .../editors/index.ts | 8 + .../editors/processors_editor.tsx | 13 + .../pipeline_processors_editor/index.ts | 8 +- .../pipeline_processors_editor.container.tsx | 74 ------ .../pipeline_processors_editor.tsx | 239 ------------------ .../processors_reducer/index.ts | 2 +- .../processors_reducer/processors_reducer.ts | 14 + .../pipeline_processors_editor/types.ts | 2 + .../ingest_pipelines/public/shared_imports.ts | 2 + .../translations/translations/ja-JP.json | 6 - .../translations/translations/zh-CN.json | 6 - 37 files changed, 931 insertions(+), 594 deletions(-) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor/components => pipeline_form}/on_failure_processors_title.tsx (95%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor/pipeline_processors_editor.scss => pipeline_form/pipeline_form.scss} (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/{pipeline_processors_editor/components/processors_title_and_test_button.tsx => pipeline_form/processors_header.tsx} (84%) create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/schema.ts delete mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/schema.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/README.md create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/button.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/modal_provider.test.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/modal_provider.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor.tsx rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/{messages.ts => i18n_texts.ts} (98%) create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/deserialize.test.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/editors/global_on_failure_processors_editor.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/editors/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/editors/processors_editor.tsx delete mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.container.tsx delete mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.tsx 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_form/on_failure_processors_title.tsx similarity index 95% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/on_failure_processors_title.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/on_failure_processors_title.tsx index 251a2ffe95212..d223653442819 100644 --- 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_form/on_failure_processors_title.tsx @@ -8,7 +8,7 @@ import React, { FunctionComponent } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { usePipelineProcessorsContext } from '../context'; +import { usePipelineProcessorsContext } from '../pipeline_processors_editor/context'; export const OnFailureProcessorsTitle: FunctionComponent = () => { const { links } = usePipelineProcessorsContext(); 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_form/pipeline_form.scss similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.scss rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.scss 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 a68e667f4ab43..341e15132d353 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 @@ -9,19 +9,18 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { useForm, Form, FormConfig } from '../../../shared_imports'; -import { Pipeline } from '../../../../common/types'; +import { Pipeline, Processor } from '../../../../common/types'; -import { - OnUpdateHandlerArg, - OnUpdateHandler, - SerializeResult, -} from '../pipeline_processors_editor'; +import './pipeline_form.scss'; + +import { OnUpdateHandlerArg, OnUpdateHandler } from '../pipeline_processors_editor'; import { PipelineRequestFlyout } from './pipeline_request_flyout'; import { PipelineTestFlyout } from './pipeline_test_flyout'; import { PipelineFormFields } from './pipeline_form_fields'; import { PipelineFormError } from './pipeline_form_error'; import { pipelineFormSchema } from './schema'; +import { PipelineForm as IPipelineForm } from './types'; export interface PipelineFormProps { onSave: (pipeline: Pipeline) => void; @@ -32,14 +31,15 @@ export interface PipelineFormProps { isEditing?: boolean; } +const defaultFormValue: Pipeline = Object.freeze({ + name: '', + description: '', + processors: [], + on_failure: [], +}); + export const PipelineForm: React.FunctionComponent = ({ - defaultValue = { - name: '', - description: '', - processors: [], - on_failure: [], - version: '', - }, + defaultValue = defaultFormValue, onSave, isSaving, saveError, @@ -50,34 +50,42 @@ export const PipelineForm: React.FunctionComponent = ({ const [isTestingPipeline, setIsTestingPipeline] = useState(false); - const processorStateRef = useRef(); + const { + processors: initialProcessors, + on_failure: initialOnFailureProcessors, + ...defaultFormValues + } = defaultValue; + + const [processorsState, setProcessorsState] = useState<{ + processors: Processor[]; + onFailure?: Processor[]; + }>({ + processors: initialProcessors, + onFailure: initialOnFailureProcessors, + }); - const handleSave: FormConfig['onSubmit'] = async (formData, isValid) => { - let override: SerializeResult | undefined; + const processorStateRef = useRef(); + const handleSave: FormConfig['onSubmit'] = async (formData, isValid) => { if (!isValid) { return; } if (processorStateRef.current) { - const processorsState = processorStateRef.current; - if (await processorsState.validate()) { - override = processorsState.getData(); - } else { - return; + const state = processorStateRef.current; + if (await state.validate()) { + onSave({ ...formData, ...state.getData() }); } } - - onSave({ ...formData, ...(override || {}) } as Pipeline); }; const handleTestPipelineClick = () => { setIsTestingPipeline(true); }; - const { form } = useForm({ + const { form } = useForm({ schema: pipelineFormSchema, - defaultValue, + defaultValue: defaultFormValues, onSubmit: handleSave, }); @@ -121,9 +129,12 @@ export const PipelineForm: React.FunctionComponent = ({ {/* All form fields */} { + setProcessorsState({ processors, onFailure }); + }} onEditorFlyoutOpen={onEditorFlyoutOpen} - initialProcessors={defaultValue.processors} - initialOnFailureProcessors={defaultValue.on_failure} + processors={processorsState.processors} + onFailure={processorsState.onFailure} onProcessorsUpdate={onProcessorsChangeHandler} hasVersion={Boolean(defaultValue.version)} isTestButtonDisabled={isTestingPipeline || form.isValid === false} 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 52d1a77c1df6d..0e7a45e8d07b9 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,17 +6,27 @@ import React, { useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiSpacer, EuiSwitch } from '@elastic/eui'; +import { EuiSpacer, EuiSwitch, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; 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'; +import { + PipelineProcessorsContextProvider, + GlobalOnFailureProcessorsEditor, + ProcessorsEditor, + OnUpdateHandler, + OnDoneLoadJsonHandler, +} from '../pipeline_processors_editor'; + +import { ProcessorsHeader } from './processors_header'; +import { OnFailureProcessorsTitle } from './on_failure_processors_title'; + interface Props { - initialProcessors: Processor[]; - initialOnFailureProcessors?: Processor[]; + processors: Processor[]; + onFailure?: Processor[]; + onLoadJson: OnDoneLoadJsonHandler; onProcessorsUpdate: OnUpdateHandler; hasVersion: boolean; isTestButtonDisabled: boolean; @@ -29,8 +39,9 @@ const UseField = getUseField({ component: Field }); const FormRow = getFormRow({ titleTag: 'h3' }); export const PipelineFormFields: React.FunctionComponent = ({ - initialProcessors, - initialOnFailureProcessors, + processors, + onFailure, + onLoadJson, onProcessorsUpdate, isEditing, hasVersion, @@ -113,30 +124,37 @@ export const PipelineFormFields: React.FunctionComponent = ({ {/* 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 ?? []; - 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_form/processors_header.tsx similarity index 84% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_title_and_test_button.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/processors_header.tsx index 6d1e2610b5c2b..5e5cddbd36b92 100644 --- 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_form/processors_header.tsx @@ -9,22 +9,26 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiLink, EuiText, EuiTitle } from import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { usePipelineProcessorsContext } from '../context'; +import { usePipelineProcessorsContext } from '../pipeline_processors_editor/context'; + +import { LoadFromJsonButton, OnDoneLoadJsonHandler } from '../pipeline_processors_editor'; export interface Props { onTestPipelineClick: () => void; isTestButtonDisabled: boolean; + onLoadJson: OnDoneLoadJsonHandler; } -export const ProcessorsTitleAndTestButton: FunctionComponent = ({ +export const ProcessorsHeader: FunctionComponent = ({ onTestPipelineClick, isTestButtonDisabled, + onLoadJson, }) => { const { links } = usePipelineProcessorsContext(); return ( @@ -55,6 +59,9 @@ export const ProcessorsTitleAndTestButton: FunctionComponent = ({ /> + + + = { + name: { + type: FIELD_TYPES.TEXT, + label: i18n.translate('xpack.ingestPipelines.form.nameFieldLabel', { + defaultMessage: 'Name', + }), + validations: [ + { + validator: emptyField( + i18n.translate('xpack.ingestPipelines.form.pipelineNameRequiredError', { + defaultMessage: 'Name is required.', + }) + ), + }, + ], + }, + description: { + type: FIELD_TYPES.TEXTAREA, + label: i18n.translate('xpack.ingestPipelines.form.descriptionFieldLabel', { + defaultMessage: 'Description (optional)', + }), + }, + version: { + type: FIELD_TYPES.NUMBER, + label: i18n.translate('xpack.ingestPipelines.form.versionFieldLabel', { + defaultMessage: 'Version (optional)', + }), + formatters: [toInt], + }, +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/schema.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/schema.tsx deleted file mode 100644 index 5435f43a78acf..0000000000000 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/schema.tsx +++ /dev/null @@ -1,138 +0,0 @@ -/* - * 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 from 'react'; - -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiCode } from '@elastic/eui'; - -import { FormSchema, FIELD_TYPES, fieldValidators, fieldFormatters } from '../../../shared_imports'; -import { parseJson, stringifyJson } from '../../lib'; - -const { emptyField, isJsonField } = fieldValidators; -const { toInt } = fieldFormatters; - -export const pipelineFormSchema: FormSchema = { - name: { - type: FIELD_TYPES.TEXT, - label: i18n.translate('xpack.ingestPipelines.form.nameFieldLabel', { - defaultMessage: 'Name', - }), - validations: [ - { - validator: emptyField( - i18n.translate('xpack.ingestPipelines.form.pipelineNameRequiredError', { - defaultMessage: 'Name is required.', - }) - ), - }, - ], - }, - description: { - type: FIELD_TYPES.TEXTAREA, - label: i18n.translate('xpack.ingestPipelines.form.descriptionFieldLabel', { - defaultMessage: 'Description (optional)', - }), - }, - processors: { - label: i18n.translate('xpack.ingestPipelines.form.processorsFieldLabel', { - defaultMessage: 'Processors', - }), - helpText: ( - - {JSON.stringify([ - { - set: { - field: 'foo', - value: 'bar', - }, - }, - ])} - - ), - }} - /> - ), - serializer: parseJson, - deserializer: stringifyJson, - validations: [ - { - validator: emptyField( - i18n.translate('xpack.ingestPipelines.form.processorsRequiredError', { - defaultMessage: 'Processors are required.', - }) - ), - }, - { - validator: isJsonField( - i18n.translate('xpack.ingestPipelines.form.processorsJsonError', { - defaultMessage: 'The input is not valid.', - }) - ), - }, - ], - }, - on_failure: { - label: i18n.translate('xpack.ingestPipelines.form.onFailureFieldLabel', { - defaultMessage: 'Failure processors (optional)', - }), - helpText: ( - - {JSON.stringify([ - { - set: { - field: '_index', - value: 'failed-{{ _index }}', - }, - }, - ])} - - ), - }} - /> - ), - serializer: (value) => { - const result = parseJson(value); - // If an empty array was passed, strip out this value entirely. - if (!result.length) { - return undefined; - } - return result; - }, - deserializer: stringifyJson, - validations: [ - { - validator: (validationArg) => { - if (!validationArg.value) { - return; - } - return isJsonField( - i18n.translate('xpack.ingestPipelines.form.onFailureProcessorsJsonError', { - defaultMessage: 'The input is not valid.', - }) - )(validationArg); - }, - }, - ], - }, - version: { - type: FIELD_TYPES.NUMBER, - label: i18n.translate('xpack.ingestPipelines.form.versionFieldLabel', { - defaultMessage: 'Version (optional)', - }), - formatters: [toInt], - }, -}; 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 index bd74f09546ff4..aa52c14e61eae 100644 --- 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 @@ -7,3 +7,5 @@ import { Pipeline } from '../../../../common/types'; export type ReadProcessorsFunction = () => Pick; + +export type PipelineForm = Omit; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/README.md b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/README.md new file mode 100644 index 0000000000000..d29af67d3179c --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/README.md @@ -0,0 +1,24 @@ +# Pipeline Processors Editor + +This component provides a way to visually build and manage an ingest +pipeline. + +# API + +## Editor components + +The top-level API consists of 3 pieces that enable the maximum amount +of flexibility for consuming code to determine overall layout. + +- PipelineProcessorsEditorContext +- ProcessorsEditor +- GlobalOnFailureProcessorsEditor + +The editor components must be wrapped inside of the context component +as this is where the shared processors state is contained. + +## Load JSON button + +This component is totally standalone. It gives users a button that +presents a modal for loading a pipeline. It does some basic +validation on the JSON to ensure that it is correct. diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.helpers.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.helpers.tsx index 7ad9aed3c44a4..cc3817d92d5e3 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.helpers.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.helpers.tsx @@ -6,7 +6,12 @@ import { act } from 'react-dom/test-utils'; import React from 'react'; import { registerTestBed, TestBed } from '../../../../../../../test_utils'; -import { PipelineProcessorsEditor, Props } from '../pipeline_processors_editor.container'; +import { + PipelineProcessorsContextProvider, + Props, + ProcessorsEditor, + GlobalOnFailureProcessorsEditor, +} from '../'; jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); @@ -55,9 +60,16 @@ jest.mock('react-virtualized', () => { }; }); -const testBedSetup = registerTestBed(PipelineProcessorsEditor, { - doMountAsync: false, -}); +const testBedSetup = registerTestBed( + (props: Props) => ( + + + + ), + { + doMountAsync: false, + } +); export interface SetupResult extends TestBed { actions: ReturnType; @@ -146,10 +158,6 @@ const createActions = (testBed: TestBed) => { find(`${processorSelector}.moreMenu.duplicateButton`).simulate('click'); }); }, - - toggleOnFailure() { - find('pipelineEditorOnFailureToggle').simulate('click'); - }, }; }; 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 index 15121cc71c321..a4bbf840dff71 100644 --- 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 @@ -43,9 +43,9 @@ describe('Pipeline Editor', () => { }, onFlyoutOpen: jest.fn(), onUpdate, - isTestButtonDisabled: false, - onTestPipelineClick: jest.fn(), - esDocsBasePath: 'test', + links: { + esDocsBasePath: 'test', + }, }); }); @@ -57,13 +57,6 @@ describe('Pipeline Editor', () => { expect(arg.getData()).toEqual(testProcessors); }); - it('toggles the on-failure processors tree', () => { - const { actions, exists } = testBed; - expect(exists('pipelineEditorOnFailureTree')).toBe(false); - actions.toggleOnFailure(); - expect(exists('pipelineEditorOnFailureTree')).toBe(true); - }); - describe('processors', () => { it('adds a new processor', async () => { const { actions } = testBed; @@ -169,7 +162,6 @@ describe('Pipeline Editor', () => { it('moves to and from the global on-failure tree', async () => { const { actions } = testBed; - actions.toggleOnFailure(); await actions.addProcessor('onFailure', 'test', { if: '1 == 5' }); actions.moveProcessor('processors>0', 'dropButtonBelow-onFailure>0'); const [onUpdateResult1] = onUpdate.mock.calls[onUpdate.mock.calls.length - 1]; 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 2d512a6bfa2ed..de0621b187230 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 @@ -12,10 +12,10 @@ export { export { ProcessorsTree, ProcessorInfo, OnActionHandler } from './processors_tree'; -export { PipelineProcessorsEditorItem } from './pipeline_processors_editor_item/pipeline_processors_editor_item'; +export { PipelineProcessorsEditor } from './pipeline_processors_editor'; -export { ProcessorRemoveModal } from './processor_remove_modal'; +export { PipelineProcessorsEditorItem } from './pipeline_processors_editor_item'; -export { ProcessorsTitleAndTestButton } from './processors_title_and_test_button'; +export { ProcessorRemoveModal } from './processor_remove_modal'; -export { OnFailureProcessorsTitle } from './on_failure_processors_title'; +export { OnDoneLoadJsonHandler, LoadFromJsonButton } from './load_from_json'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/button.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/button.tsx new file mode 100644 index 0000000000000..482878d1bda58 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/button.tsx @@ -0,0 +1,34 @@ +/* + * 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 { EuiButton } from '@elastic/eui'; + +import { ModalProvider, OnDoneLoadJsonHandler } from './modal_provider'; + +interface Props { + onDone: OnDoneLoadJsonHandler; +} + +const i18nTexts = { + buttonLabel: i18n.translate('xpack.ingestPipelines.pipelineEditor.loadFromJson.buttonLabel', { + defaultMessage: 'Load JSON', + }), +}; + +export const LoadFromJsonButton: FunctionComponent = ({ onDone }) => { + return ( + + {(openModal) => { + return ( + + {i18nTexts.buttonLabel} + + ); + }} + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/index.ts new file mode 100644 index 0000000000000..c1c49f251d518 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { LoadFromJsonButton } from './button'; +export { OnDoneLoadJsonHandler } from './modal_provider'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/modal_provider.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/modal_provider.test.tsx new file mode 100644 index 0000000000000..2f4cdce1edd0b --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/modal_provider.test.tsx @@ -0,0 +1,127 @@ +/* + * 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 from 'react'; +import { ModalProvider, OnDoneLoadJsonHandler } from './modal_provider'; + +jest.mock('@elastic/eui', () => { + const original = jest.requireActual('@elastic/eui'); + + return { + ...original, + // Mocking EuiCodeEditor, which uses React Ace under the hood + EuiCodeEditor: (props: any) => ( + { + props.onChange(syntheticEvent.jsonString); + }} + /> + ), + }; +}); + +jest.mock('lodash', () => { + const original = jest.requireActual('lodash'); + + return { + ...original, + debounce: (fn: any) => fn, + }; +}); + +import { registerTestBed, TestBed } from '../../../../../../../../test_utils/testbed'; + +const setup = ({ onDone }: { onDone: OnDoneLoadJsonHandler }) => { + return registerTestBed( + () => ( + + {(openModal) => { + return ( + + ); + }} + + ), + { + memoryRouter: { + wrapComponent: false, + }, + } + )(); +}; + +describe('Load from JSON ModalProvider', () => { + let testBed: TestBed; + let onDone: jest.Mock; + + beforeEach(async () => { + onDone = jest.fn(); + testBed = await setup({ onDone }); + }); + + it('displays errors', () => { + const { find, exists } = testBed; + find('button').simulate('click'); + expect(exists('loadJsonConfirmationModal')); + const invalidPipeline = '{}'; + find('mockCodeEditor').simulate('change', { jsonString: invalidPipeline }); + find('confirmModalConfirmButton').simulate('click'); + const errorCallout = find('loadJsonConfirmationModal.errorCallOut'); + expect(errorCallout.text()).toContain('Please ensure the JSON is a valid pipeline object.'); + expect(onDone).toHaveBeenCalledTimes(0); + }); + + it('passes through a valid pipeline object', () => { + const { find, exists } = testBed; + find('button').simulate('click'); + expect(exists('loadJsonConfirmationModal')); + const validPipeline = JSON.stringify({ + processors: [{ set: { field: 'test', value: 123 } }, { badType1: null }, { badType2: 1 }], + on_failure: [ + { + gsub: { + field: '_index', + pattern: '(.monitoring-\\w+-)6(-.+)', + replacement: '$17$2', + }, + }, + ], + }); + find('mockCodeEditor').simulate('change', { jsonString: validPipeline }); + find('confirmModalConfirmButton').simulate('click'); + expect(!exists('loadJsonConfirmationModal')); + expect(onDone).toHaveBeenCalledTimes(1); + expect(onDone.mock.calls[0][0]).toMatchInlineSnapshot(` + Object { + "on_failure": Array [ + Object { + "gsub": Object { + "field": "_index", + "pattern": "(.monitoring-\\\\w+-)6(-.+)", + "replacement": "$17$2", + }, + }, + ], + "processors": Array [ + Object { + "set": Object { + "field": "test", + "value": 123, + }, + }, + Object { + "badType1": null, + }, + Object { + "badType2": 1, + }, + ], + } + `); + }); +}); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/modal_provider.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/modal_provider.tsx new file mode 100644 index 0000000000000..f183386d5927d --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/load_from_json/modal_provider.tsx @@ -0,0 +1,138 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n/react'; +import React, { FunctionComponent, useRef, useState } from 'react'; +import { EuiConfirmModal, EuiOverlayMask, EuiSpacer, EuiText, EuiCallOut } from '@elastic/eui'; + +import { JsonEditor, OnJsonEditorUpdateHandler } from '../../../../../shared_imports'; + +import { Processor } from '../../../../../../common/types'; + +import { deserialize } from '../../deserialize'; + +export type OnDoneLoadJsonHandler = (json: { + processors: Processor[]; + on_failure?: Processor[]; +}) => void; + +export interface Props { + onDone: OnDoneLoadJsonHandler; + children: (openModal: () => void) => React.ReactNode; +} + +const i18nTexts = { + modalTitle: i18n.translate('xpack.ingestPipelines.pipelineEditor.loadFromJson.modalTitle', { + defaultMessage: 'Load JSON', + }), + buttons: { + cancel: i18n.translate('xpack.ingestPipelines.pipelineEditor.loadFromJson.buttons.cancel', { + defaultMessage: 'Cancel', + }), + confirm: i18n.translate('xpack.ingestPipelines.pipelineEditor.loadFromJson.buttons.confirm', { + defaultMessage: 'Load and overwrite', + }), + }, + editor: { + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.loadFromJson.editor', { + defaultMessage: 'Pipeline object', + }), + }, + error: { + title: i18n.translate('xpack.ingestPipelines.pipelineEditor.loadFromJson.error.title', { + defaultMessage: 'Invalid pipeline', + }), + body: i18n.translate('xpack.ingestPipelines.pipelineEditor.loadFromJson.error.body', { + defaultMessage: 'Please ensure the JSON is a valid pipeline object.', + }), + }, +}; + +const defaultValue = {}; +const defaultValueRaw = JSON.stringify(defaultValue, null, 2); + +export const ModalProvider: FunctionComponent = ({ onDone, children }) => { + const [isModalVisible, setIsModalVisible] = useState(false); + const [isValidJson, setIsValidJson] = useState(true); + const [error, setError] = useState(); + const jsonContent = useRef['0']>({ + isValid: true, + validate: () => true, + data: { + format: () => defaultValue, + raw: defaultValueRaw, + }, + }); + const onJsonUpdate: OnJsonEditorUpdateHandler = (jsonUpdateData) => { + setIsValidJson(jsonUpdateData.validate()); + jsonContent.current = jsonUpdateData; + }; + return ( + <> + {children(() => setIsModalVisible(true))} + {isModalVisible ? ( + + { + setIsModalVisible(false); + }} + onConfirm={async () => { + try { + const json = jsonContent.current.data.format(); + const { processors, on_failure: onFailure } = json; + // This function will throw if it cannot parse the pipeline object + deserialize({ processors, onFailure }); + onDone(json as any); + setIsModalVisible(false); + } catch (e) { + setError(e); + } + }} + cancelButtonText={i18nTexts.buttons.cancel} + confirmButtonDisabled={!isValidJson} + confirmButtonText={i18nTexts.buttons.confirm} + maxWidth={600} + > +
+ + + + + + + {error && ( + <> + + {i18nTexts.error.body} + + + + )} + + +
+
+
+ ) : undefined} + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor.tsx new file mode 100644 index 0000000000000..c89ff1d3d99ac --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor.tsx @@ -0,0 +1,33 @@ +/* + * 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, useMemo } from 'react'; +import { ProcessorsTree } from '.'; +import { usePipelineProcessorsContext } from '../context'; + +import { ON_FAILURE_STATE_SCOPE, PROCESSOR_STATE_SCOPE } from '../processors_reducer'; + +export interface Props { + stateSlice: typeof ON_FAILURE_STATE_SCOPE | typeof PROCESSOR_STATE_SCOPE; +} + +export const PipelineProcessorsEditor: FunctionComponent = memo( + function PipelineProcessorsEditor({ stateSlice }) { + const { + onTreeAction, + state: { editor, processors }, + } = usePipelineProcessorsContext(); + const baseSelector = useMemo(() => [stateSlice], [stateSlice]); + return ( + + ); + } +); 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 index 5bbea4b89b053..5cee5311c62a9 100644 --- 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 @@ -9,7 +9,7 @@ import React, { FunctionComponent, useState } from 'react'; import { EuiContextMenuItem, EuiContextMenuPanel, EuiPopover, EuiButtonIcon } from '@elastic/eui'; -import { editorItemMessages } from './messages'; +import { i18nTexts } from './i18n_texts'; interface Props { disabled: boolean; @@ -39,7 +39,7 @@ export const ContextMenu: FunctionComponent = (props) => { onDuplicate(); }} > - {editorItemMessages.duplicateButtonLabel} + {i18nTexts.duplicateButtonLabel} , showAddOnFailure ? ( = (props) => { onAddOnFailure(); }} > - {editorItemMessages.addOnFailureButtonLabel} + {i18nTexts.addOnFailureButtonLabel} ) : undefined, = (props) => { onDelete(); }} > - {editorItemMessages.deleteButtonLabel} + {i18nTexts.deleteButtonLabel} , ].filter(Boolean) as JSX.Element[]; @@ -82,7 +82,7 @@ export const ContextMenu: FunctionComponent = (props) => { disabled={disabled} onClick={() => setIsOpen((v) => !v)} iconType="boxesHorizontal" - aria-label={editorItemMessages.moreButtonAriaLabel} + aria-label={i18nTexts.moreButtonAriaLabel} /> } > 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/i18n_texts.ts similarity index 98% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/messages.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/i18n_texts.ts index 913902d295503..ab080767b6029 100644 --- 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/i18n_texts.ts @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; -export const editorItemMessages = { +export const i18nTexts = { moveButtonLabel: i18n.translate('xpack.ingestPipelines.pipelineEditor.item.moveButtonLabel', { defaultMessage: 'Move this processor', }), 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 index 0fe804adaeb48..09c047d1d51b7 100644 --- 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 @@ -25,7 +25,7 @@ import './pipeline_processors_editor_item.scss'; import { InlineTextInput } from './inline_text_input'; import { ContextMenu } from './context_menu'; -import { editorItemMessages } from './messages'; +import { i18nTexts } from './i18n_texts'; import { ProcessorInfo } from '../processors_tree'; export interface Handlers { @@ -52,7 +52,7 @@ export const PipelineProcessorsEditorItem: FunctionComponent = memo( renderOnFailureHandlers, }) => { const { - state: { editor, processorsDispatch }, + state: { editor, processors }, } = usePipelineProcessorsContext(); const isDisabled = editor.mode.id !== 'idle'; @@ -115,7 +115,7 @@ export const PipelineProcessorsEditorItem: FunctionComponent = memo( description: nextDescription, }; } - processorsDispatch({ + processors.dispatch({ type: 'updateProcessor', payload: { processor: { @@ -126,17 +126,17 @@ export const PipelineProcessorsEditorItem: FunctionComponent = memo( }, }); }} - ariaLabel={editorItemMessages.processorTypeLabel({ type: processor.type })} + ariaLabel={i18nTexts.processorTypeLabel({ type: processor.type })} text={description} - placeholder={editorItemMessages.descriptionPlaceholder} + placeholder={i18nTexts.descriptionPlaceholder} />
{!isInMoveMode && ( - + { @@ -151,12 +151,12 @@ export const PipelineProcessorsEditorItem: FunctionComponent = memo( {!isInMoveMode && ( - + @@ -165,7 +165,7 @@ export const PipelineProcessorsEditorItem: FunctionComponent = memo( - {editorItemMessages.cancelMoveButtonLabel} + {i18nTexts.cancelMoveButtonLabel}
@@ -183,7 +183,7 @@ export const PipelineProcessorsEditorItem: FunctionComponent = memo( editor.setMode({ id: 'removingProcessor', arg: { selector } }); }} onDuplicate={() => { - processorsDispatch({ + processors.dispatch({ type: 'duplicateProcessor', payload: { source: selector, 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 index 9d284748a3d15..3eccda55fbb3a 100644 --- 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 @@ -9,11 +9,13 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React, { FunctionComponent, memo, useEffect } from 'react'; import { EuiButton, + EuiButtonEmpty, EuiHorizontalRule, EuiFlyout, EuiFlyoutHeader, - EuiTitle, EuiFlyoutBody, + EuiFlyoutFooter, + EuiTitle, EuiFlexGroup, EuiFlexItem, } from '@elastic/eui'; @@ -44,6 +46,11 @@ const addButtonLabel = i18n.translate( { defaultMessage: 'Add' } ); +const cancelButtonLabel = i18n.translate( + 'xpack.ingestPipelines.settingsFormOnFailureFlyout.cancelButtonLabel', + { defaultMessage: 'Cancel' } +); + export const ProcessorSettingsForm: FunctionComponent = memo( ({ processor, form, isOnFailure, onClose, onOpen }) => { const { @@ -71,7 +78,7 @@ export const ProcessorSettingsForm: FunctionComponent = memo( return (
- + @@ -109,30 +116,19 @@ export const ProcessorSettingsForm: FunctionComponent = memo( {(arg: any) => { const { type } = arg; - let formContent: React.ReactNode | undefined; if (type?.length) { const formDescriptor = getProcessorFormDescriptor(type as any); if (formDescriptor?.FieldsComponent) { - formContent = ( + return ( <> ); - } else { - formContent = ; } - - return ( - <> - {formContent} - - {processor ? updateButtonLabel : addButtonLabel} - - - ); + return ; } // If the user has not yet defined a type, we do not show any settings fields @@ -140,6 +136,24 @@ export const ProcessorSettingsForm: FunctionComponent = memo( }} + + + + {cancelButtonLabel} + + + { + form.submit(); + }} + > + {processor ? updateButtonLabel : addButtonLabel} + + + + ); 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 index 46e3d1c803fd5..87e6eb7f642a6 100644 --- 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 @@ -4,7 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ON_FAILURE_STATE_SCOPE, PROCESSOR_STATE_SCOPE } from './processors_reducer'; + export enum DropSpecialLocations { top = 'TOP', bottom = 'BOTTOM', } + +export const PROCESSORS_BASE_SELECTOR = [PROCESSOR_STATE_SCOPE]; +export const ON_FAILURE_BASE_SELECTOR = [ON_FAILURE_STATE_SCOPE]; 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 index fbc06f41208fe..ec864d31d1986 100644 --- 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 @@ -4,41 +4,242 @@ * 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'; +import React, { + createContext, + Dispatch, + FunctionComponent, + useCallback, + useContext, + useEffect, + useMemo, + useState, + useRef, +} from 'react'; + +import { Processor } from '../../../../common/types'; + +import { EditorMode, FormValidityState, OnFormUpdateArg, OnUpdateHandlerArg } from './types'; + +import { + ProcessorsDispatch, + useProcessorsState, + State as ProcessorsState, + isOnFailureSelector, +} from './processors_reducer'; + +import { deserialize } from './deserialize'; + +import { serialize } from './serialize'; + +import { OnSubmitHandler, ProcessorSettingsForm } from './components/processor_settings_form'; + +import { OnActionHandler } from './components/processors_tree'; + +import { ProcessorRemoveModal } from './components'; + +import { getValue } from './utils'; interface Links { esDocsBasePath: string; } -const PipelineProcessorsContext = createContext<{ +interface ContextValue { links: Links; + onTreeAction: OnActionHandler; state: { - processorsDispatch: ProcessorsDispatch; + processors: { + state: ProcessorsState; + dispatch: ProcessorsDispatch; + }; editor: { mode: EditorMode; setMode: Dispatch; }; }; -}>({} as any); +} -interface Props { +const PipelineProcessorsContext = createContext({} as any); + +export interface Props { links: Links; - processorsDispatch: ProcessorsDispatch; + value: { + processors: Processor[]; + onFailure?: Processor[]; + }; + /** + * Give users a way to react to this component opening a flyout + */ + onFlyoutOpen: () => void; + onUpdate: (arg: OnUpdateHandlerArg) => void; } export const PipelineProcessorsContextProvider: FunctionComponent = ({ links, + value: { processors: originalProcessors, onFailure: originalOnFailureProcessors }, + onUpdate, + onFlyoutOpen, children, - processorsDispatch, }) => { + const initRef = useRef(false); const [mode, setMode] = useState({ id: 'idle' }); + const deserializedResult = useMemo( + () => + deserialize({ + processors: originalProcessors, + onFailure: originalOnFailureProcessors, + }), + [originalProcessors, originalOnFailureProcessors] + ); + const [processorsState, processorsDispatch] = useProcessorsState(deserializedResult); + + useEffect(() => { + if (initRef.current) { + processorsDispatch({ + type: 'loadProcessors', + payload: { + newState: deserializedResult, + }, + }); + } else { + initRef.current = true; + } + }, [deserializedResult, processorsDispatch]); + + const { onFailure: onFailureProcessors, processors } = processorsState; + + 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] + ); + + useEffect(() => { + onUpdate({ + validate: async () => { + const formValid = await formState.validate(); + return formValid && mode.id === 'idle'; + }, + getData: () => + serialize({ + onFailure: onFailureProcessors, + processors, + }), + }); + }, [processors, onFailureProcessors, onUpdate, formState, mode]); + + const onSubmit = useCallback( + (processorTypeAndOptions) => { + switch (mode.id) { + case 'creatingProcessor': + processorsDispatch({ + type: 'addProcessor', + payload: { + processor: { ...processorTypeAndOptions }, + targetSelector: mode.arg.selector, + }, + }); + break; + case 'editingProcessor': + processorsDispatch({ + type: 'updateProcessor', + payload: { + processor: { + ...mode.arg.processor, + ...processorTypeAndOptions, + }, + selector: mode.arg.selector, + }, + }); + break; + default: + } + setMode({ id: 'idle' }); + }, + [processorsDispatch, mode, setMode] + ); + + const onCloseSettingsForm = useCallback(() => { + setMode({ id: 'idle' }); + setFormState({ validate: () => Promise.resolve(true) }); + }, [setFormState, setMode]); + + const onTreeAction = useCallback( + (action) => { + switch (action.type) { + case 'addProcessor': + setMode({ id: 'creatingProcessor', arg: { selector: action.payload.target } }); + break; + case 'move': + setMode({ id: 'idle' }); + processorsDispatch({ + type: 'moveProcessor', + payload: action.payload, + }); + break; + case 'selectToMove': + setMode({ id: 'movingProcessor', arg: action.payload.info }); + break; + case 'cancelMove': + setMode({ id: 'idle' }); + break; + } + }, + [processorsDispatch, setMode] + ); + return ( {children} + + {mode.id === 'editingProcessor' || mode.id === 'creatingProcessor' ? ( + + ) : undefined} + {mode.id === 'removingProcessor' && ( + { + if (confirmed) { + processorsDispatch({ + type: 'removeProcessor', + payload: { selector }, + }); + } + setMode({ id: 'idle' }); + }} + /> + )} ); }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/deserialize.test.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/deserialize.test.ts new file mode 100644 index 0000000000000..9b7c2069fcddd --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/deserialize.test.ts @@ -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 { deserialize } from './deserialize'; + +describe('deserialize', () => { + it('tolerates certain bad values correctly', () => { + expect( + deserialize({ + processors: [ + { set: { field: 'test', value: 123 } }, + { badType1: null } as any, + { badType2: 1 } as any, + ], + onFailure: [ + { + gsub: { + field: '_index', + pattern: '(.monitoring-\\w+-)6(-.+)', + replacement: '$17$2', + }, + }, + ], + }) + ).toEqual({ + processors: [ + { + id: expect.any(String), + type: 'set', + options: { + field: 'test', + value: 123, + }, + }, + { + id: expect.any(String), + onFailure: undefined, + type: 'badType1', + options: {}, + }, + { + id: expect.any(String), + onFailure: undefined, + type: 'badType2', + options: {}, + }, + ], + onFailure: [ + { + id: expect.any(String), + type: 'gsub', + onFailure: undefined, + options: { + field: '_index', + pattern: '(.monitoring-\\w+-)6(-.+)', + replacement: '$17$2', + }, + }, + ], + }); + }); + + it('throws for unacceptable values', () => { + expect(() => { + deserialize({ + processors: [{ reallyBad: undefined } as any, 1 as any], + onFailure: [], + }); + }).toThrow('Invalid processor type'); + }); +}); 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 index fa1d041bdaba3..1e9a97e189a5e 100644 --- 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 @@ -22,12 +22,16 @@ 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 type: unknown = Object.keys(processor)[0]; + if (typeof type !== 'string') { + throw new Error(`Invalid processor type. Received "${type}"`); + } + return type; }; const convertToPipelineInternalProcessor = (processor: Processor): ProcessorInternal => { const type = getProcessorType(processor); - const { on_failure: originalOnFailure, ...options } = processor[type]; + const { on_failure: originalOnFailure, ...options } = processor[type] ?? {}; const onFailure = originalOnFailure?.length ? convertProcessors(originalOnFailure) : (originalOnFailure as ProcessorInternal[] | undefined); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/editors/global_on_failure_processors_editor.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/editors/global_on_failure_processors_editor.tsx new file mode 100644 index 0000000000000..7c62383024cfe --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/editors/global_on_failure_processors_editor.tsx @@ -0,0 +1,13 @@ +/* + * 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 { PipelineProcessorsEditor } from '../components'; + +export const GlobalOnFailureProcessorsEditor: FunctionComponent = () => { + return ; +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/editors/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/editors/index.ts new file mode 100644 index 0000000000000..6c544b31df439 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/editors/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { GlobalOnFailureProcessorsEditor } from './global_on_failure_processors_editor'; +export { ProcessorsEditor } from './processors_editor'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/editors/processors_editor.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/editors/processors_editor.tsx new file mode 100644 index 0000000000000..108b22be43ca0 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/editors/processors_editor.tsx @@ -0,0 +1,13 @@ +/* + * 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 { PipelineProcessorsEditor } from '../components'; + +export const ProcessorsEditor: FunctionComponent = () => { + return ; +}; 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 index 58d6e492b85e5..89bc50fc0600a 100644 --- 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 @@ -4,8 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -export { PipelineProcessorsEditor, OnUpdateHandler } from './pipeline_processors_editor.container'; +export { PipelineProcessorsContextProvider, Props } from './context'; -export { OnUpdateHandlerArg } from './types'; +export { ProcessorsEditor, GlobalOnFailureProcessorsEditor } from './editors'; + +export { OnUpdateHandlerArg, OnUpdateHandler } from './types'; export { SerializeResult } from './serialize'; + +export { LoadFromJsonButton, OnDoneLoadJsonHandler } from './components'; 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 deleted file mode 100644 index 7257677c08fc2..0000000000000 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.container.tsx +++ /dev/null @@ -1,74 +0,0 @@ -/* - * 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; - esDocsBasePath: 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, - esDocsBasePath, - 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.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.tsx deleted file mode 100644 index 09e77c5107754..0000000000000 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/pipeline_processors_editor.tsx +++ /dev/null @@ -1,239 +0,0 @@ -/* - * 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, - ProcessorRemoveModal, - OnActionHandler, - OnSubmitHandler, - ProcessorSettingsForm, -} from './components'; - -import { ProcessorInternal, OnUpdateHandlerArg, FormValidityState, OnFormUpdateArg } from './types'; - -import { - ON_FAILURE_STATE_SCOPE, - PROCESSOR_STATE_SCOPE, - isOnFailureSelector, -} from './processors_reducer'; - -const PROCESSORS_BASE_SELECTOR = [PROCESSOR_STATE_SCOPE]; -const ON_FAILURE_BASE_SELECTOR = [ON_FAILURE_STATE_SCOPE]; - -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; -} - -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' ? ( - - ) : 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 index 7265f63f45a5d..0e06b8d55d379 100644 --- 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 @@ -12,6 +12,6 @@ export { Action, } from './processors_reducer'; -export { ON_FAILURE_STATE_SCOPE, PROCESSOR_STATE_SCOPE } from './constants'; +export * from './constants'; export { isChildPath, isOnFailureSelector } from './utils'; 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 index 4e069aab8bdd1..295e7ff141117 100644 --- 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 @@ -38,6 +38,12 @@ export type Action = payload: { source: ProcessorSelector; }; + } + | { + type: 'loadProcessors'; + payload: { + newState: DeserializeResult; + }; }; export type ProcessorsDispatch = Dispatch; @@ -124,6 +130,14 @@ export const reducer: Reducer = (state, action) => { return setValue(sourceProcessorsArraySelector, state, sourceProcessorsArray); } + if (action.type === 'loadProcessors') { + return { + ...action.payload.newState, + onFailure: action.payload.newState.onFailure ?? [], + isRoot: true, + }; + } + return state; }; 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 aa39fca29fa8b..aea8f0f0910f4 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 @@ -38,6 +38,8 @@ export interface OnUpdateHandlerArg extends FormValidityState { getData: () => SerializeResult; } +export type OnUpdateHandler = (arg: OnUpdateHandlerArg) => void; + /** * The editor can be in different modes. This enables us to hold * a reference to data dispatch to the reducer (like the {@link ProcessorSelector} diff --git a/x-pack/plugins/ingest_pipelines/public/shared_imports.ts b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts index 05e7d1e41c5fa..d2c4b73a48767 100644 --- a/x-pack/plugins/ingest_pipelines/public/shared_imports.ts +++ b/x-pack/plugins/ingest_pipelines/public/shared_imports.ts @@ -22,6 +22,8 @@ export { UseRequestConfig, WithPrivileges, Monaco, + JsonEditor, + OnJsonEditorUpdateHandler, } from '../../../../src/plugins/es_ui_shared/public/'; export { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 5c5d270d324ff..9a3f52a0ce47c 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -8469,13 +8469,7 @@ "xpack.ingestPipelines.form.nameFieldLabel": "名前", "xpack.ingestPipelines.form.nameTitle": "名前", "xpack.ingestPipelines.form.onFailureFieldHelpText": "JSONフォーマットを使用:{code}", - "xpack.ingestPipelines.form.onFailureFieldLabel": "障害プロセッサー(任意)", - "xpack.ingestPipelines.form.onFailureProcessorsJsonError": "入力が無効です。", "xpack.ingestPipelines.form.pipelineNameRequiredError": "名前が必要です。", - "xpack.ingestPipelines.form.processorsFieldHelpText": "JSONフォーマットを使用:{code}", - "xpack.ingestPipelines.form.processorsFieldLabel": "プロセッサー", - "xpack.ingestPipelines.form.processorsJsonError": "入力が無効です。", - "xpack.ingestPipelines.form.processorsRequiredError": "プロセッサーが必要です。", "xpack.ingestPipelines.form.saveButtonLabel": "パイプラインを保存", "xpack.ingestPipelines.form.savePipelineError": "パイプラインを作成できません", "xpack.ingestPipelines.form.savingButtonLabel": "保存中…", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index c71215d2bfb74..7aebb59e0f8a2 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -8473,13 +8473,7 @@ "xpack.ingestPipelines.form.nameFieldLabel": "名称", "xpack.ingestPipelines.form.nameTitle": "名称", "xpack.ingestPipelines.form.onFailureFieldHelpText": "使用 JSON 格式:{code}", - "xpack.ingestPipelines.form.onFailureFieldLabel": "失败处理器(可选)", - "xpack.ingestPipelines.form.onFailureProcessorsJsonError": "输入无效。", "xpack.ingestPipelines.form.pipelineNameRequiredError": "“名称”必填。", - "xpack.ingestPipelines.form.processorsFieldHelpText": "使用 JSON 格式:{code}", - "xpack.ingestPipelines.form.processorsFieldLabel": "处理器", - "xpack.ingestPipelines.form.processorsJsonError": "输入无效。", - "xpack.ingestPipelines.form.processorsRequiredError": "需要指定处理器。", "xpack.ingestPipelines.form.saveButtonLabel": "保存管道", "xpack.ingestPipelines.form.savePipelineError": "无法创建管道", "xpack.ingestPipelines.form.savingButtonLabel": "正在保存......",