From c62d4c36c19e6751a02f30527f19287ed43ebe0c Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Thu, 20 Aug 2020 11:52:58 -0400 Subject: [PATCH] [Ingest pipelines] Test pipeline enhancements (#74964) --- .../helpers/pipeline_form.helpers.ts | 7 +- .../ingest_pipelines_create.test.tsx | 2 +- .../on_failure_processors_title.tsx | 74 +++--- .../pipeline_form/pipeline_form.scss | 8 + .../pipeline_form/pipeline_form_fields.tsx | 11 +- .../pipeline_form/processors_header.tsx | 33 +-- .../documents_dropdown.scss | 3 + .../documents_dropdown/documents_dropdown.tsx | 69 ++++++ .../components/documents_dropdown/index.ts | 7 + .../components/index.ts | 4 +- .../components/load_from_json/button.tsx | 8 +- .../manage_processor_form.tsx | 51 +++- .../processor_output.tsx | 217 ++++++++++++++++++ .../pipeline_processors_editor_item.scss | 5 + .../pipeline_processors_editor_item.tsx | 21 ++ ...pipeline_processors_editor_item_status.tsx | 85 +++++++ .../processors_tree/processors_tree.scss | 1 - .../{button.tsx => add_documents_button.tsx} | 29 +-- .../test_pipeline/flyout_provider.tsx | 172 -------------- .../components/test_pipeline/index.ts | 2 +- .../test_pipeline/test_output_button.tsx | 60 +++++ .../test_pipeline/test_pipeline_actions.tsx | 84 +++++++ .../test_pipeline/test_pipeline_flyout.tsx | 197 ++++++++++++++++ .../documents_schema.tsx} | 0 .../index.ts | 2 +- .../tab_documents.tsx | 56 +++-- .../tab_output.tsx | 45 ++-- .../test_pipeline_tabs.tsx} | 14 +- .../context/context.tsx | 6 +- .../context/index.ts | 7 +- .../context/processors_context.tsx | 22 +- .../context/test_config_context.tsx | 57 ----- .../context/test_pipeline_context.tsx | 189 +++++++++++++++ .../pipeline_processors_editor/deserialize.ts | 47 +++- .../pipeline_processors_editor/index.ts | 7 +- .../pipeline_processors_editor/serialize.ts | 44 +++- .../pipeline_processors_editor/types.ts | 32 +++ .../public/application/services/api.ts | 4 +- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 40 files changed, 1291 insertions(+), 393 deletions(-) create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/documents_dropdown/documents_dropdown.scss create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/documents_dropdown/documents_dropdown.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/documents_dropdown/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processor_output.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item_status.tsx rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/{button.tsx => add_documents_button.tsx} (51%) delete mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/flyout_provider.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_output_button.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_actions.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout.tsx rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/{flyout_tabs/schema.tsx => test_pipeline_flyout_tabs/documents_schema.tsx} (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/{flyout_tabs => test_pipeline_flyout_tabs}/index.ts (83%) rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/{flyout_tabs => test_pipeline_flyout_tabs}/tab_documents.tsx (68%) rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/{flyout_tabs => test_pipeline_flyout_tabs}/tab_output.tsx (68%) rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/{flyout_tabs/pipeline_test_tabs.tsx => test_pipeline_flyout_tabs/test_pipeline_tabs.tsx} (77%) delete mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context/test_config_context.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context/test_pipeline_context.tsx diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipeline_form.helpers.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipeline_form.helpers.ts index 85848b3d2f73cb..752ffef51b43b5 100644 --- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipeline_form.helpers.ts +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/helpers/pipeline_form.helpers.ts @@ -13,8 +13,8 @@ export const getFormActions = (testBed: TestBed) => { find('submitButton').simulate('click'); }; - const clickTestPipelineButton = () => { - find('testPipelineButton').simulate('click'); + const clickAddDocumentsButton = () => { + find('addDocumentsButton').simulate('click'); }; const clickShowRequestLink = () => { @@ -34,11 +34,12 @@ export const getFormActions = (testBed: TestBed) => { clickShowRequestLink, toggleVersionSwitch, toggleOnFailureSwitch, - clickTestPipelineButton, + clickAddDocumentsButton, }; }; export type PipelineFormTestSubjects = + | 'addDocumentsButton' | 'submitButton' | 'pageTitle' | 'savePipelineError' diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx index 813057813f1398..6074c64d2bdb08 100644 --- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx @@ -201,7 +201,7 @@ describe('', () => { const { actions, exists, find, waitFor } = testBed; await act(async () => { - actions.clickTestPipelineButton(); + actions.clickAddDocumentsButton(); await waitFor('testPipelineFlyout'); }); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/on_failure_processors_title.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/on_failure_processors_title.tsx index d2c001b0aaa138..0beb5657b54cb9 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/on_failure_processors_title.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/on_failure_processors_title.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiText, EuiTitle } from '@elastic/eui'; +import { EuiLink, EuiText, EuiTitle } from '@elastic/eui'; import React, { FunctionComponent } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -12,47 +12,41 @@ import { useKibana } from '../../../shared_imports'; export const OnFailureProcessorsTitle: FunctionComponent = () => { const { services } = useKibana(); + return ( - - - -

- -

-
- +
+ +

- {i18n.translate( - 'xpack.ingestPipelines.pipelineEditor.onFailureProcessorsDocumentationLink', - { - defaultMessage: 'Learn more.', - } - )} - - ), - }} + id="xpack.ingestPipelines.pipelineEditor.onFailureTreeTitle" + defaultMessage="Failure processors" /> - - - +

+
+ + + {i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.onFailureProcessorsDocumentationLink', + { + defaultMessage: 'Learn more.', + } + )} + + ), + }} + /> + +
); }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.scss b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.scss index 73eb54827e04fb..d5592b87dda51b 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.scss +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/pipeline_form.scss @@ -1,3 +1,11 @@ .pipelineProcessorsEditor { margin-bottom: $euiSizeXL; + + &__container { + background-color: $euiColorLightestShade; + } + + &__onFailureTitle { + padding-left: $euiSizeS; + } } 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 3a97e6408b144b..6033f34af6825d 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 @@ -129,16 +129,13 @@ export const PipelineFormFields: React.FunctionComponent = ({ - + - - + - - + - - +
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/processors_header.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/processors_header.tsx index 1f27d611e54d46..43477affa8d947 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/processors_header.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/processors_header.tsx @@ -14,7 +14,7 @@ import { useKibana } from '../../../shared_imports'; import { LoadFromJsonButton, OnDoneLoadJsonHandler, - TestPipelineButton, + TestPipelineActions, } from '../pipeline_processors_editor'; export interface Props { @@ -23,6 +23,7 @@ export interface Props { export const ProcessorsHeader: FunctionComponent = ({ onLoadJson }) => { const { services } = useKibana(); + return ( = ({ onLoadJson }) => { justifyContent="spaceBetween" responsive={false} > - - -

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

-
+ + + + +

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

+
+
+ + + +
+ = ({ onLoadJson }) => { {i18n.translate( 'xpack.ingestPipelines.pipelineEditor.processorsDocumentationLink', @@ -61,10 +71,7 @@ export const ProcessorsHeader: FunctionComponent = ({ onLoadJson }) => {
- - - - +
); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/documents_dropdown/documents_dropdown.scss b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/documents_dropdown/documents_dropdown.scss new file mode 100644 index 00000000000000..c5b14dc129b0e3 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/documents_dropdown/documents_dropdown.scss @@ -0,0 +1,3 @@ +.documentsDropdown__selectContainer { + max-width: 200px; +} diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/documents_dropdown/documents_dropdown.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/documents_dropdown/documents_dropdown.tsx new file mode 100644 index 00000000000000..e9aa5c1d56f734 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/documents_dropdown/documents_dropdown.tsx @@ -0,0 +1,69 @@ +/* + * 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 { EuiSelect, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; + +import { Document } from '../../types'; + +import './documents_dropdown.scss'; + +const i18nTexts = { + ariaLabel: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.testPipeline.documentsDropdownAriaLabel', + { + defaultMessage: 'Select documents', + } + ), + dropdownLabel: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.testPipeline.documentsdropdownLabel', + { + defaultMessage: 'Documents:', + } + ), + buttonLabel: i18n.translate('xpack.ingestPipelines.pipelineEditor.testPipeline.buttonLabel', { + defaultMessage: 'Add documents', + }), +}; + +const getDocumentOptions = (documents: Document[]) => + documents.map((doc, index) => ({ + value: index, + text: doc._id, + })); + +interface Props { + documents: Document[]; + selectedDocumentIndex: number; + updateSelectedDocument: (index: number) => void; +} + +export const DocumentsDropdown: FunctionComponent = ({ + documents, + selectedDocumentIndex, + updateSelectedDocument, +}) => { + return ( + + + + {i18nTexts.dropdownLabel} + + + + { + updateSelectedDocument(Number(e.target.value)); + }} + aria-label={i18nTexts.ariaLabel} + /> + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/documents_dropdown/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/documents_dropdown/index.ts new file mode 100644 index 00000000000000..a8b55788647fb0 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/documents_dropdown/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { DocumentsDropdown } from './documents_dropdown'; 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 3b0ae477c871f9..435d0ed66c4b00 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 @@ -20,6 +20,8 @@ export { ProcessorRemoveModal } from './processor_remove_modal'; export { OnDoneLoadJsonHandler, LoadFromJsonButton } from './load_from_json'; -export { TestPipelineButton } from './test_pipeline'; +export { TestPipelineActions } from './test_pipeline'; + +export { DocumentsDropdown } from './documents_dropdown'; export { PipelineProcessorsItemTooltip, Position } from './pipeline_processors_editor_item_tooltip'; 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 index 482878d1bda587..21d15fc86a0ce6 100644 --- 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 @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; import React, { FunctionComponent } from 'react'; -import { EuiButton } from '@elastic/eui'; +import { EuiButtonEmpty } from '@elastic/eui'; import { ModalProvider, OnDoneLoadJsonHandler } from './modal_provider'; @@ -15,7 +15,7 @@ interface Props { const i18nTexts = { buttonLabel: i18n.translate('xpack.ingestPipelines.pipelineEditor.loadFromJson.buttonLabel', { - defaultMessage: 'Load JSON', + defaultMessage: 'Import', }), }; @@ -24,9 +24,9 @@ export const LoadFromJsonButton: FunctionComponent = ({ onDone }) => { {(openModal) => { return ( - + {i18nTexts.buttonLabel} - + ); }} diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/manage_processor_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/manage_processor_form.tsx index ad6d191be802df..ee8ca71e584461 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/manage_processor_form.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/manage_processor_form.tsx @@ -24,10 +24,12 @@ import { import { Form, FormDataProvider, FormHook } from '../../../../../shared_imports'; import { ProcessorInternal } from '../../types'; +import { useTestPipelineContext } from '../../context'; import { getProcessorDescriptor } from '../shared'; import { ProcessorSettingsFields } from './processor_settings_fields'; import { DocumentationButton } from './documentation_button'; +import { ProcessorOutput } from './processor_output'; export interface Props { isOnFailure: boolean; @@ -53,7 +55,7 @@ const cancelButtonLabel = i18n.translate( { defaultMessage: 'Cancel' } ); -export type TabType = 'configuration'; +export type TabType = 'configuration' | 'output'; interface Tab { id: TabType; @@ -70,6 +72,12 @@ const tabs: Tab[] = [ } ), }, + { + id: 'output', + name: i18n.translate('xpack.ingestPipelines.settingsFormOnFailureFlyout.outputTabTitle', { + defaultMessage: 'Output', + }), + }, ]; const getFlyoutTitle = (isOnFailure: boolean, isExistingProcessor: boolean) => { @@ -102,6 +110,28 @@ const getFlyoutTitle = (isOnFailure: boolean, isExistingProcessor: boolean) => { export const ManageProcessorForm: FunctionComponent = memo( ({ processor, form, isOnFailure, onClose, onOpen, esDocsBasePath }) => { + const { testPipelineData, setCurrentTestPipelineData } = useTestPipelineContext(); + const { + testOutputPerProcessor, + config: { selectedDocumentIndex, documents }, + } = testPipelineData; + + const processorOutput = + processor && + testOutputPerProcessor && + testOutputPerProcessor[selectedDocumentIndex][processor.id]; + + const updateSelectedDocument = (index: number) => { + setCurrentTestPipelineData({ + type: 'updateActiveDocument', + payload: { + config: { + selectedDocumentIndex: index, + }, + }, + }); + }; + useEffect( () => { onOpen(); @@ -111,7 +141,20 @@ export const ManageProcessorForm: FunctionComponent = memo( const [activeTab, setActiveTab] = useState('configuration'); - const flyoutContent = ; + let flyoutContent: React.ReactNode; + + if (activeTab === 'output') { + flyoutContent = ( + + ); + } else { + flyoutContent = ; + } return (
@@ -156,6 +199,10 @@ export const ManageProcessorForm: FunctionComponent = memo( isSelected={tab.id === activeTab} key={tab.id} data-test-subj={`${tab.id}Tab`} + disabled={ + (tab.id === 'output' && Boolean(testOutputPerProcessor) === false) || + Boolean(processorOutput) === false + } > {tab.name} diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processor_output.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processor_output.tsx new file mode 100644 index 00000000000000..c081f69fd41fe5 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processor_output.tsx @@ -0,0 +1,217 @@ +/* + * 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 { + EuiAccordion, + EuiCallOut, + EuiCodeBlock, + EuiText, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, +} from '@elastic/eui'; + +import { ProcessorResult, Document } from '../../types'; +import { DocumentsDropdown } from '../documents_dropdown'; + +export interface Props { + processorOutput?: ProcessorResult; + documents: Document[]; + selectedDocumentIndex: number; + updateSelectedDocument: (index: number) => void; +} + +const i18nTexts = { + noOutputCalloutTitle: i18n.translate( + 'xpack.ingestPipelines.processorOutput.noOutputCalloutTitle', + { + defaultMessage: 'Unable to load the processor output.', + } + ), + tabDescription: i18n.translate('xpack.ingestPipelines.processorOutput.descriptionText', { + defaultMessage: + 'View how the processor affects the ingest document as it passes through the pipeline.', + }), + skippedCalloutTitle: i18n.translate('xpack.ingestPipelines.processorOutput.skippedCalloutTitle', { + defaultMessage: 'The processor was not run.', + }), + droppedCalloutTitle: i18n.translate('xpack.ingestPipelines.processorOutput.droppedCalloutTitle', { + defaultMessage: 'The document was dropped.', + }), + processorOutputLabel: i18n.translate( + 'xpack.ingestPipelines.processorOutput.processorOutputCodeBlockLabel', + { + defaultMessage: 'Processor output', + } + ), + processorErrorLabel: i18n.translate( + 'xpack.ingestPipelines.processorOutput.processorErrorCodeBlockLabel', + { + defaultMessage: 'Processor error', + } + ), + prevProcessorLabel: i18n.translate( + 'xpack.ingestPipelines.processorOutput.previousOutputCodeBlockLabel', + { + defaultMessage: 'View previous processor output', + } + ), + processorIgnoredErrorLabel: i18n.translate( + 'xpack.ingestPipelines.processorOutput.ignoredErrorCodeBlockLabel', + { + defaultMessage: 'View ignored error', + } + ), +}; + +export const ProcessorOutput: React.FunctionComponent = ({ + processorOutput, + documents, + selectedDocumentIndex, + updateSelectedDocument, +}) => { + // This code should not be reached, + // but if for some reason the output is undefined, we render a callout message + if (!processorOutput) { + return ; + } + + const { + prevProcessorResult, + doc: currentResult, + ignored_error: ignoredError, + error, + status, + } = processorOutput!; + + return ( + <> + +

{i18nTexts.tabDescription}

+
+ + {/* There is no output for "skipped" status, so we render an info callout */} + {status === 'skipped' && ( + <> + + + + )} + + {/* There is no output for "dropped status", so we render a warning callout */} + {status === 'dropped' && ( + <> + + + + )} + + {currentResult && ( + <> + + + + +

{i18nTexts.processorOutputLabel}

+
+ + + +
+ + + + + {JSON.stringify(currentResult, null, 2)} + + + )} + + {error && ( + <> + + + + +

{i18nTexts.processorErrorLabel}

+
+ + + +
+ + + + + {JSON.stringify(error, null, 2)} + + + )} + + {prevProcessorResult?.doc && ( + <> + + + +

{i18nTexts.prevProcessorLabel}

+ + } + > + <> + + + + {JSON.stringify(prevProcessorResult.doc, null, 2)} + + +
+ + )} + + {ignoredError && ( + <> + + + +

{i18nTexts.processorIgnoredErrorLabel}

+ + } + > + <> + + + + {JSON.stringify(ignoredError, null, 2)} + + +
+ + )} + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.scss b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.scss index 85a123b421975f..d9c3d84eec0820 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.scss +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/pipeline_processors_editor_item.scss @@ -60,4 +60,9 @@ z-index: $cancelButtonZIndex; } } + + &__statusContainer { + // Prevent content jump when spinner renders + min-width: 12px; + } } 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 a13321c38c1939..4a67e27d2ebe63 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 @@ -11,6 +11,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiLink, + EuiLoadingSpinner, EuiPanel, EuiText, EuiToolTip, @@ -21,6 +22,8 @@ import { selectorToDataTestSubject } from '../../utils'; import { ProcessorsDispatch } from '../../processors_reducer'; import { ProcessorInfo } from '../processors_tree'; +import { PipelineProcessorsItemStatus } from '../pipeline_processors_editor_item_status'; +import { useTestPipelineContext } from '../../context'; import { getProcessorDescriptor } from '../shared'; @@ -63,6 +66,17 @@ export const PipelineProcessorsEditorItem: FunctionComponent = memo( const isMovingOtherProcessor = editor.mode.id === 'movingProcessor' && !isMovingThisProcessor; const isDimmed = isEditingOtherProcessor || isMovingOtherProcessor; + const { testPipelineData } = useTestPipelineContext(); + const { + config: { selectedDocumentIndex }, + testOutputPerProcessor, + isExecutingPipeline, + } = testPipelineData; + + const processorOutput = + testOutputPerProcessor && testOutputPerProcessor[selectedDocumentIndex][processor.id]; + const processorStatus = processorOutput?.status ?? 'inactive'; + const panelClasses = classNames('pipelineProcessorsEditor__item', { // eslint-disable-next-line @typescript-eslint/naming-convention 'pipelineProcessorsEditor__item--selected': isMovingThisProcessor || isEditingThisProcessor, @@ -131,6 +145,13 @@ export const PipelineProcessorsEditorItem: FunctionComponent = memo( {renderMoveButton()} + + {isExecutingPipeline ? ( + + ) : ( + + )} + = { + success: { + icon: 'checkInCircleFilled', + iconColor: 'success', + label: i18n.translate('xpack.ingestPipelines.pipelineEditorItem.successStatusAriaLabel', { + defaultMessage: 'Success', + }), + }, + error: { + icon: 'crossInACircleFilled', + iconColor: 'danger', + label: i18n.translate('xpack.ingestPipelines.pipelineEditorItem.errorStatusAriaLabel', { + defaultMessage: 'Error', + }), + }, + error_ignored: { + icon: 'alert', + iconColor: 'warning', + label: i18n.translate('xpack.ingestPipelines.pipelineEditorItem.errorIgnoredStatusAriaLabel', { + defaultMessage: 'Error ignored', + }), + }, + dropped: { + icon: 'alert', + iconColor: 'warning', + label: i18n.translate('xpack.ingestPipelines.pipelineEditorItem.droppedStatusAriaLabel', { + defaultMessage: 'Dropped', + }), + }, + skipped: { + icon: 'dot', + iconColor: 'subdued', + label: i18n.translate('xpack.ingestPipelines.pipelineEditorItem.skippedStatusAriaLabel', { + defaultMessage: 'Skipped', + }), + }, + inactive: { + icon: 'dot', + iconColor: 'subdued', + label: i18n.translate('xpack.ingestPipelines.pipelineEditorItem.inactiveStatusAriaLabel', { + defaultMessage: 'Not run', + }), + }, +}; + +// This is a fallback in case ES returns a status we do not support +// This is not expected and likely means we need to modify the code to support a new status +const unknownStatus = { + icon: 'dot', + iconColor: 'subdued', + label: i18n.translate('xpack.ingestPipelines.pipelineEditorItem.unknownStatusAriaLabel', { + defaultMessage: 'Unknown', + }), +}; + +interface Props { + processorStatus: ProcessorStatus; +} + +export const PipelineProcessorsItemStatus: FunctionComponent = ({ processorStatus }) => { + const { icon, iconColor, label } = processorStatusToIconMap[processorStatus] || unknownStatus; + + return ( + {label}

}> + +
+ ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/processors_tree.scss b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/processors_tree.scss index 061c9adb5d4437..a54cc994ab730a 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/processors_tree.scss +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/processors_tree.scss @@ -3,7 +3,6 @@ .pipelineProcessorsEditor__tree { &__container { - background-color: $euiColorLightestShade; padding: $euiSizeS; } diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/button.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/add_documents_button.tsx similarity index 51% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/button.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/add_documents_button.tsx index 0e8e23ba80ea87..e3ef9a9ee5390e 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/button.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/add_documents_button.tsx @@ -5,26 +5,27 @@ */ import { i18n } from '@kbn/i18n'; import React, { FunctionComponent } from 'react'; -import { EuiButton } from '@elastic/eui'; - -import { FlyoutProvider } from './flyout_provider'; +import { EuiButtonEmpty } from '@elastic/eui'; const i18nTexts = { buttonLabel: i18n.translate('xpack.ingestPipelines.pipelineEditor.testPipeline.buttonLabel', { - defaultMessage: 'Test pipeline', + defaultMessage: 'Add documents', }), }; -export const TestPipelineButton: FunctionComponent = () => { +interface Props { + openTestPipelineFlyout: () => void; +} + +export const AddDocumentsButton: FunctionComponent = ({ openTestPipelineFlyout }) => { return ( - - {(openFlyout) => { - return ( - - {i18nTexts.buttonLabel} - - ); - }} - + + {i18nTexts.buttonLabel} + ); }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/flyout_provider.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/flyout_provider.tsx deleted file mode 100644 index 53aeb9fdc08ba0..00000000000000 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/flyout_provider.tsx +++ /dev/null @@ -1,172 +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, { useState, useEffect, useCallback } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; - -import { - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutHeader, - EuiSpacer, - EuiTitle, - EuiCallOut, -} from '@elastic/eui'; - -import { useKibana } from '../../../../../shared_imports'; - -import { usePipelineProcessorsContext, useTestConfigContext } from '../../context'; -import { serialize } from '../../serialize'; - -import { Tabs, Tab, OutputTab, DocumentsTab } from './flyout_tabs'; - -export interface Props { - children: (openFlyout: () => void) => React.ReactNode; -} - -export const FlyoutProvider: React.FunctionComponent = ({ children }) => { - const { services } = useKibana(); - const { - state: { processors }, - } = usePipelineProcessorsContext(); - - const serializedProcessors = serialize(processors.state); - - const { testConfig } = useTestConfigContext(); - const { documents: cachedDocuments, verbose: cachedVerbose } = testConfig; - - const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); - - const initialSelectedTab = cachedDocuments ? 'output' : 'documents'; - const [selectedTab, setSelectedTab] = useState(initialSelectedTab); - - const [shouldExecuteImmediately, setShouldExecuteImmediately] = useState(false); - const [isExecuting, setIsExecuting] = useState(false); - const [executeError, setExecuteError] = useState(null); - const [executeOutput, setExecuteOutput] = useState(undefined); - - const handleExecute = useCallback( - async (documents: object[], verbose?: boolean) => { - setIsExecuting(true); - setExecuteError(null); - - const { error, data: output } = await services.api.simulatePipeline({ - documents, - verbose, - pipeline: { ...serializedProcessors }, - }); - - setIsExecuting(false); - - if (error) { - setExecuteError(error); - return; - } - - setExecuteOutput(output); - - services.notifications.toasts.addSuccess( - i18n.translate('xpack.ingestPipelines.testPipelineFlyout.successNotificationText', { - defaultMessage: 'Pipeline executed', - }), - { - toastLifeTimeMs: 1000, - } - ); - - setSelectedTab('output'); - }, - [services.api, services.notifications.toasts, serializedProcessors] - ); - - useEffect(() => { - if (isFlyoutVisible === false && cachedDocuments) { - setShouldExecuteImmediately(true); - } - }, [isFlyoutVisible, cachedDocuments]); - - useEffect(() => { - // If the user has already tested the pipeline once, - // use the cached test config and automatically execute the pipeline - if (isFlyoutVisible && shouldExecuteImmediately && cachedDocuments) { - setShouldExecuteImmediately(false); - handleExecute(cachedDocuments!, cachedVerbose); - } - }, [handleExecute, cachedDocuments, cachedVerbose, isFlyoutVisible, shouldExecuteImmediately]); - - let tabContent; - - if (selectedTab === 'output') { - tabContent = ( - - ); - } else { - // default to "Documents" tab - tabContent = ; - } - - return ( - <> - {children(() => setIsFlyoutVisible(true))} - - {isFlyoutVisible && ( - setIsFlyoutVisible(false)} - data-test-subj="testPipelineFlyout" - > - - -

- -

-
-
- - - !executeOutput && tabId === 'output'} - /> - - - - {/* Execute error */} - {executeError ? ( - <> - - } - color="danger" - iconType="alert" - > -

{executeError.message}

-
- - - ) : null} - - {/* Documents or output tab content */} - {tabContent} -
-
- )} - - ); -}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/index.ts index 8e5037c15bac41..4050971d0930ae 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { TestPipelineButton } from './button'; +export { TestPipelineActions } from './test_pipeline_actions'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_output_button.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_output_button.tsx new file mode 100644 index 00000000000000..361e32c77d59bc --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_output_button.tsx @@ -0,0 +1,60 @@ +/* + * 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, EuiToolTip } from '@elastic/eui'; + +const i18nTexts = { + buttonLabel: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.testPipeline.outputButtonLabel', + { + defaultMessage: 'View output', + } + ), + disabledButtonTooltipLabel: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.testPipeline.outputButtonTooltipLabel', + { + defaultMessage: 'Add documents to view the output', + } + ), +}; + +interface Props { + isDisabled: boolean; + openTestPipelineFlyout: () => void; +} + +export const TestOutputButton: FunctionComponent = ({ + isDisabled, + openTestPipelineFlyout, +}) => { + if (isDisabled) { + return ( + {i18nTexts.disabledButtonTooltipLabel}

}> + + {i18nTexts.buttonLabel} + +
+ ); + } + + return ( + + {i18nTexts.buttonLabel} + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_actions.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_actions.tsx new file mode 100644 index 00000000000000..eb9d9352e4b906 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_actions.tsx @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { FunctionComponent, useState } from 'react'; + +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { useTestPipelineContext, usePipelineProcessorsContext } from '../../context'; + +import { DocumentsDropdown } from '../documents_dropdown'; +import { TestPipelineFlyoutTab } from './test_pipeline_flyout_tabs'; +import { AddDocumentsButton } from './add_documents_button'; +import { TestOutputButton } from './test_output_button'; +import { TestPipelineFlyout } from './test_pipeline_flyout'; + +export const TestPipelineActions: FunctionComponent = () => { + const { testPipelineData, setCurrentTestPipelineData } = useTestPipelineContext(); + + const { + state: { processors }, + } = usePipelineProcessorsContext(); + + const { + testOutputPerProcessor, + config: { documents, selectedDocumentIndex }, + } = testPipelineData; + + const [openTestPipelineFlyout, setOpenTestPipelineFlyout] = useState(false); + const [activeFlyoutTab, setActiveFlyoutTab] = useState('documents'); + + const updateSelectedDocument = (index: number) => { + setCurrentTestPipelineData({ + type: 'updateActiveDocument', + payload: { + config: { + selectedDocumentIndex: index, + }, + }, + }); + }; + + return ( + <> + + + {documents ? ( + + ) : ( + { + setOpenTestPipelineFlyout(true); + setActiveFlyoutTab('documents'); + }} + /> + )} + + + { + setOpenTestPipelineFlyout(true); + setActiveFlyoutTab('output'); + }} + /> + + + {openTestPipelineFlyout && ( + setOpenTestPipelineFlyout(false)} + /> + )} + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout.tsx new file mode 100644 index 00000000000000..e8bb1aa1d357f8 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout.tsx @@ -0,0 +1,197 @@ +/* + * 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, { useState, useCallback, useEffect } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +import { + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiSpacer, + EuiTitle, + EuiCallOut, +} from '@elastic/eui'; + +import { useKibana } from '../../../../../shared_imports'; +import { useTestPipelineContext } from '../../context'; +import { serialize } from '../../serialize'; +import { DeserializeResult } from '../../deserialize'; +import { Document } from '../../types'; + +import { Tabs, TestPipelineFlyoutTab, OutputTab, DocumentsTab } from './test_pipeline_flyout_tabs'; + +export interface Props { + activeTab: TestPipelineFlyoutTab; + onClose: () => void; + processors: DeserializeResult; +} + +export interface HandleTestPipelineArgs { + documents: Document[]; + verbose?: boolean; +} + +export const TestPipelineFlyout: React.FunctionComponent = ({ + onClose, + activeTab, + processors, +}) => { + const { services } = useKibana(); + + const { + testPipelineData, + setCurrentTestPipelineData, + updateTestOutputPerProcessor, + } = useTestPipelineContext(); + + const { + config: { documents: cachedDocuments, verbose: cachedVerbose }, + } = testPipelineData; + + const [selectedTab, setSelectedTab] = useState(activeTab); + + const [shouldTestImmediately, setShouldTestImmediately] = useState(false); + const [isRunningTest, setIsRunningTest] = useState(false); + const [testingError, setTestingError] = useState(null); + const [testOutput, setTestOutput] = useState(undefined); + + const handleTestPipeline = useCallback( + async ({ documents, verbose }: HandleTestPipelineArgs) => { + const serializedProcessors = serialize({ pipeline: processors }); + + setIsRunningTest(true); + setTestingError(null); + + const { error, data: currentTestOutput } = await services.api.simulatePipeline({ + documents, + verbose, + pipeline: { ...serializedProcessors }, + }); + + setIsRunningTest(false); + + if (error) { + setTestingError(error); + return; + } + + setCurrentTestPipelineData({ + type: 'updateConfig', + payload: { + config: { + documents, + verbose, + }, + }, + }); + + setTestOutput(currentTestOutput); + + services.notifications.toasts.addSuccess( + i18n.translate('xpack.ingestPipelines.testPipelineFlyout.successNotificationText', { + defaultMessage: 'Pipeline executed', + }), + { + toastLifeTimeMs: 1000, + } + ); + + setSelectedTab('output'); + }, + [services.api, processors, setCurrentTestPipelineData, services.notifications.toasts] + ); + + useEffect(() => { + if (cachedDocuments) { + setShouldTestImmediately(true); + } + // We only want to know on initial mount if there are cached documents + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + // If the user has already tested the pipeline once, + // use the cached test config and automatically execute the pipeline + if (shouldTestImmediately) { + setShouldTestImmediately(false); + handleTestPipeline({ documents: cachedDocuments!, verbose: cachedVerbose }); + } + }, [handleTestPipeline, cachedDocuments, cachedVerbose, shouldTestImmediately]); + + let tabContent; + + if (selectedTab === 'output') { + tabContent = ( + + ); + } else { + // default to "Documents" tab + tabContent = ( + + ); + } + + return ( + + + +

+ +

+
+
+ + + !testOutput && tabId === 'output'} + /> + + + + {/* Testing error callout */} + {testingError ? ( + <> + + } + color="danger" + iconType="alert" + > +

{testingError.message}

+
+ + + ) : null} + + {/* Documents or output tab content */} + {tabContent} +
+
+ ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/flyout_tabs/schema.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/documents_schema.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/flyout_tabs/schema.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/documents_schema.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/flyout_tabs/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/index.ts similarity index 83% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/flyout_tabs/index.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/index.ts index ea8fe2cd923507..1f306f96b4bd64 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/flyout_tabs/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export { Tabs, Tab } from './pipeline_test_tabs'; +export { Tabs, TestPipelineFlyoutTab } from './test_pipeline_tabs'; export { DocumentsTab } from './tab_documents'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/flyout_tabs/tab_documents.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/tab_documents.tsx similarity index 68% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/flyout_tabs/tab_documents.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/tab_documents.tsx index 794d9355712107..8968416683c3e8 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/flyout_tabs/tab_documents.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/tab_documents.tsx @@ -16,50 +16,59 @@ import { JsonEditorField, Form, useForm, - FormConfig, useKibana, } from '../../../../../../shared_imports'; -import { useTestConfigContext, TestConfig } from '../../../context'; - -import { documentsSchema } from './schema'; +import { TestPipelineContext } from '../../../context'; +import { Document } from '../../../types'; +import { DeserializeResult } from '../../../deserialize'; +import { HandleTestPipelineArgs } from '../test_pipeline_flyout'; +import { documentsSchema } from './documents_schema'; const UseField = getUseField({ component: Field }); interface Props { - handleExecute: (documents: object[], verbose: boolean) => void; - isExecuting: boolean; + handleTestPipeline: (data: HandleTestPipelineArgs) => void; + setPerProcessorOutput: (documents: Document[] | undefined, processors: DeserializeResult) => void; + isRunningTest: boolean; + processors: DeserializeResult; + testPipelineData: TestPipelineContext['testPipelineData']; } -export const DocumentsTab: React.FunctionComponent = ({ handleExecute, isExecuting }) => { +export const DocumentsTab: React.FunctionComponent = ({ + handleTestPipeline, + isRunningTest, + setPerProcessorOutput, + processors, + testPipelineData, +}) => { const { services } = useKibana(); - const { setCurrentTestConfig, testConfig } = useTestConfigContext(); - const { verbose: cachedVerbose, documents: cachedDocuments } = testConfig; + const { + config: { documents: cachedDocuments, verbose: cachedVerbose }, + } = testPipelineData; + + const testPipeline = async () => { + const { isValid, data } = await form.submit(); - const executePipeline: FormConfig['onSubmit'] = async (formData, isValid) => { if (!isValid) { return; } - const { documents } = formData as TestConfig; + const { documents } = data as { documents: Document[] }; - // Update context - setCurrentTestConfig({ - ...testConfig, - documents, - }); + await handleTestPipeline({ documents: documents!, verbose: cachedVerbose }); - handleExecute(documents!, cachedVerbose); + // This is necessary to update the status and output of each processor + // as verbose may not be enabled + setPerProcessorOutput(documents, processors); }; const { form } = useForm({ schema: documentsSchema, defaultValue: { documents: cachedDocuments || '', - verbose: cachedVerbose || false, }, - onSubmit: executePipeline, }); return ( @@ -79,7 +88,7 @@ export const DocumentsTab: React.FunctionComponent = ({ handleExecute, is {i18n.translate( 'xpack.ingestPipelines.testPipelineFlyout.documentsTab.simulateDocumentionLink', { - defaultMessage: 'Learn more', + defaultMessage: 'Learn more.', } )} @@ -95,6 +104,7 @@ export const DocumentsTab: React.FunctionComponent = ({ handleExecute, is form={form} data-test-subj="testPipelineForm" isInvalid={form.isSubmitted && !form.isValid} + onSubmit={testPipeline} error={form.getErrors()} > {/* Documents editor */} @@ -118,12 +128,12 @@ export const DocumentsTab: React.FunctionComponent = ({ handleExecute, is - {isExecuting ? ( + {isRunningTest ? ( void; - isExecuting: boolean; + handleTestPipeline: (data: HandleTestPipelineArgs) => void; + isRunningTest: boolean; + cachedVerbose?: boolean; + cachedDocuments: Document[]; + testOutput?: any; } export const OutputTab: React.FunctionComponent = ({ - executeOutput, - handleExecute, - isExecuting, + handleTestPipeline, + isRunningTest, + cachedVerbose, + cachedDocuments, + testOutput, }) => { - const { setCurrentTestConfig, testConfig } = useTestConfigContext(); - const { verbose: cachedVerbose, documents: cachedDocuments } = testConfig; + const [isVerboseEnabled, setIsVerboseEnabled] = useState(Boolean(cachedVerbose)); - const onEnableVerbose = (isVerboseEnabled: boolean) => { - setCurrentTestConfig({ - ...testConfig, - verbose: isVerboseEnabled, - }); + const onEnableVerbose = (isVerbose: boolean) => { + setIsVerboseEnabled(isVerbose); - handleExecute(cachedDocuments!, isVerboseEnabled); + handleTestPipeline({ documents: cachedDocuments!, verbose: isVerbose }); }; let content: React.ReactNode | undefined; - if (isExecuting) { + if (isRunningTest) { content = ; - } else if (executeOutput) { + } else if (testOutput) { content = ( - {JSON.stringify(executeOutput, null, 2)} + {JSON.stringify(testOutput, null, 2)} ); } @@ -76,14 +77,16 @@ export const OutputTab: React.FunctionComponent = ({ defaultMessage="View verbose output" /> } - checked={cachedVerbose} + checked={isVerboseEnabled} onChange={(e) => onEnableVerbose(e.target.checked)} />
handleExecute(cachedDocuments!, cachedVerbose)} + onClick={() => + handleTestPipeline({ documents: cachedDocuments!, verbose: isVerboseEnabled }) + } iconType="refresh" > void; - selectedTab: Tab; - getIsDisabled: (tab: Tab) => boolean; + onTabChange: (tab: TestPipelineFlyoutTab) => void; + selectedTab: TestPipelineFlyoutTab; + getIsDisabled: (tab: TestPipelineFlyoutTab) => boolean; } export const Tabs: React.FunctionComponent = ({ @@ -22,15 +22,15 @@ export const Tabs: React.FunctionComponent = ({ getIsDisabled, }) => { const tabs: Array<{ - id: Tab; + id: TestPipelineFlyoutTab; name: React.ReactNode; }> = [ { id: 'documents', name: ( ), }, diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context/context.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context/context.tsx index a1ea0fd9d0b9ed..1023385ccc2998 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context/context.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context/context.tsx @@ -10,7 +10,7 @@ import { PipelineProcessorsContextProvider, Props as ProcessorsContextProps, } from './processors_context'; -import { TestConfigContextProvider } from './test_config_context'; +import { TestPipelineContextProvider } from './test_pipeline_context'; interface Props extends ProcessorsContextProps { children: React.ReactNode; @@ -23,7 +23,7 @@ export const ProcessorsEditorContextProvider: FunctionComponent = ({ onFlyoutOpen, }: Props) => { return ( - + = ({ > {children} - + ); }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context/index.ts index 1664b3410c1c0d..5b152f074f9cdf 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context/index.ts @@ -6,7 +6,12 @@ export { ProcessorsEditorContextProvider } from './context'; -export { TestConfigContextProvider, useTestConfigContext, TestConfig } from './test_config_context'; +export { + TestPipelineContextProvider, + useTestPipelineContext, + TestPipelineData, + TestPipelineContext, +} from './test_pipeline_context'; export { PipelineProcessorsContextProvider, diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context/processors_context.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context/processors_context.tsx index f83803da7bf912..8c59d484acd08e 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context/processors_context.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context/processors_context.tsx @@ -44,6 +44,8 @@ import { import { getValue } from '../utils'; +import { useTestPipelineContext } from './test_pipeline_context'; + const PipelineProcessorsContext = createContext({} as any); export interface Props { @@ -79,6 +81,12 @@ export const PipelineProcessorsContextProvider: FunctionComponent = ({ ); const [processorsState, processorsDispatch] = useProcessorsState(deserializedResult); + const { updateTestOutputPerProcessor, testPipelineData } = useTestPipelineContext(); + + const { + config: { documents }, + } = testPipelineData; + useEffect(() => { if (initRef.current) { processorsDispatch({ @@ -120,8 +128,10 @@ export const PipelineProcessorsContextProvider: FunctionComponent = ({ }, getData: () => serialize({ - onFailure: onFailureProcessors, - processors, + pipeline: { + onFailure: onFailureProcessors, + processors, + }, }), }); }, [processors, onFailureProcessors, onUpdate, formState, mode]); @@ -183,7 +193,7 @@ export const PipelineProcessorsContextProvider: FunctionComponent = ({ break; } }, - [processorsDispatch, setMode] + [processorsDispatch] ); // Memoize the state object to ensure we do not trigger unnecessary re-renders and so @@ -198,6 +208,12 @@ export const PipelineProcessorsContextProvider: FunctionComponent = ({ }; }, [mode, setMode, processorsState, processorsDispatch]); + // Update the test output whenever the processorsState changes (e.g., on move, update, delete) + // Note: updateTestOutputPerProcessor() will only simulate if the user has added sample documents + useEffect(() => { + updateTestOutputPerProcessor(documents, processorsState); + }, [documents, processorsState, updateTestOutputPerProcessor]); + return ( void; -} - -const TEST_CONFIG_DEFAULT_VALUE = { - testConfig: { - verbose: false, - }, - setCurrentTestConfig: () => {}, -}; - -const TestConfigContext = React.createContext(TEST_CONFIG_DEFAULT_VALUE); - -export const useTestConfigContext = () => { - const ctx = useContext(TestConfigContext); - if (!ctx) { - throw new Error( - '"useTestConfigContext" can only be called inside of TestConfigContext.Provider!' - ); - } - return ctx; -}; - -export const TestConfigContextProvider = ({ children }: { children: React.ReactNode }) => { - const [testConfig, setTestConfig] = useState({ - verbose: false, - }); - - const setCurrentTestConfig = useCallback((currentTestConfig: TestConfig): void => { - setTestConfig(currentTestConfig); - }, []); - - return ( - - {children} - - ); -}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context/test_pipeline_context.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context/test_pipeline_context.tsx new file mode 100644 index 00000000000000..f764f403de79b3 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/context/test_pipeline_context.tsx @@ -0,0 +1,189 @@ +/* + * 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, { useCallback, useContext, useReducer, Reducer } from 'react'; +import { useKibana } from '../../../../shared_imports'; +import { + DeserializedProcessorResult, + deserializeVerboseTestOutput, + DeserializeResult, +} from '../deserialize'; +import { serialize } from '../serialize'; +import { Document } from '../types'; + +export interface TestPipelineData { + config: { + documents?: Document[]; + verbose?: boolean; + selectedDocumentIndex: number; + }; + testOutputPerProcessor?: DeserializedProcessorResult[]; + isExecutingPipeline?: boolean; +} + +type Action = + | { + type: 'updateOutputPerProcessor'; + payload: { + testOutputPerProcessor?: DeserializedProcessorResult[]; + isExecutingPipeline: boolean; + }; + } + | { + type: 'updateConfig'; + payload: { + config: { + documents: Document[]; + verbose?: boolean; + }; + }; + } + | { + type: 'updateActiveDocument'; + payload: Pick; + } + | { + type: 'updateIsExecutingPipeline'; + payload: Pick; + }; + +export interface TestPipelineContext { + testPipelineData: TestPipelineData; + setCurrentTestPipelineData: (data: Action) => void; + updateTestOutputPerProcessor: ( + documents: Document[] | undefined, + processors: DeserializeResult + ) => void; +} + +const DEFAULT_TEST_PIPELINE_CONTEXT = { + testPipelineData: { + config: { + selectedDocumentIndex: 0, + }, + isExecutingPipeline: false, + }, + setCurrentTestPipelineData: () => {}, + updateTestOutputPerProcessor: () => {}, +}; + +const TestPipelineContext = React.createContext(DEFAULT_TEST_PIPELINE_CONTEXT); + +export const useTestPipelineContext = () => { + const ctx = useContext(TestPipelineContext); + if (!ctx) { + throw new Error( + '"useTestPipelineContext" can only be called inside of TestPipelineContextProvider.Provider!' + ); + } + return ctx; +}; + +export const reducer: Reducer = (state, action) => { + if (action.type === 'updateOutputPerProcessor') { + return { + ...state, + testOutputPerProcessor: action.payload.testOutputPerProcessor, + isExecutingPipeline: false, + }; + } + + if (action.type === 'updateConfig') { + return { + ...action.payload, + config: { + ...action.payload.config, + selectedDocumentIndex: state.config.selectedDocumentIndex, + }, + testOutputPerProcessor: state.testOutputPerProcessor, + }; + } + + if (action.type === 'updateActiveDocument') { + return { + ...state, + config: { + ...state.config, + selectedDocumentIndex: action.payload.config.selectedDocumentIndex, + }, + }; + } + + if (action.type === 'updateIsExecutingPipeline') { + return { + ...state, + isExecutingPipeline: action.payload.isExecutingPipeline, + }; + } + + return state; +}; + +export const TestPipelineContextProvider = ({ children }: { children: React.ReactNode }) => { + const [state, dispatch] = useReducer(reducer, DEFAULT_TEST_PIPELINE_CONTEXT.testPipelineData); + const { services } = useKibana(); + + const updateTestOutputPerProcessor = useCallback( + async (documents: Document[] | undefined, processors: DeserializeResult) => { + if (!documents) { + return; + } + + dispatch({ + type: 'updateIsExecutingPipeline', + payload: { + isExecutingPipeline: true, + }, + }); + + const serializedProcessorsWithTag = serialize({ + pipeline: { processors: processors.processors, onFailure: processors.onFailure }, + copyIdToTag: true, + }); + + const { data: verboseResults, error } = await services.api.simulatePipeline({ + documents, + verbose: true, + pipeline: { ...serializedProcessorsWithTag }, + }); + + if (error) { + dispatch({ + type: 'updateOutputPerProcessor', + payload: { + isExecutingPipeline: false, + // reset the output if there is an error + // this will result to the status changing to "inactive" + testOutputPerProcessor: undefined, + }, + }); + + return; + } + + dispatch({ + type: 'updateOutputPerProcessor', + payload: { + testOutputPerProcessor: deserializeVerboseTestOutput(verboseResults), + isExecutingPipeline: false, + }, + }); + }, + [services.api] + ); + + return ( + + {children} + + ); +}; 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 1e9a97e189a5ee..01788c49ec2f14 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 @@ -5,7 +5,7 @@ */ import uuid from 'uuid'; import { Processor } from '../../../../common/types'; -import { ProcessorInternal } from './types'; +import { ProcessorInternal, VerboseTestOutput, ProcessorResult } from './types'; export interface DeserializeArgs { processors: Processor[]; @@ -58,3 +58,48 @@ export const deserialize = ({ processors, onFailure }: DeserializeArgs): Deseria onFailure: onFailure ? convertProcessors(onFailure) : undefined, }; }; + +export interface DeserializedProcessorResult { + [key: string]: ProcessorResult; +} +/** + * This function takes the verbose response of the simulate API + * and maps the results to each processor in the pipeline by the "tag" field + */ +export const deserializeVerboseTestOutput = ( + output: VerboseTestOutput +): DeserializedProcessorResult[] => { + const { docs } = output; + + const deserializedOutput = docs.map((doc) => { + return doc.processor_results.reduce( + ( + processorResultsById: DeserializedProcessorResult, + currentResult: ProcessorResult, + index: number + ) => { + const result = { ...currentResult }; + const resultId = result.tag; + + if (index !== 0) { + // Add the result from the previous processor so that the user + // can easily compare current output to the previous output + // This may be a result from an on_failure processor + result.prevProcessorResult = doc.processor_results[index - 1]; + } + + // The tag is added programatically as a way to map + // the results to each processor + // It is not something we need to surface to the user, so we delete it + delete result.tag; + + processorResultsById[resultId] = result; + + return processorResultsById; + }, + {} + ); + }); + + return deserializedOutput; +}; 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 d2342bbd2ab1a2..71b2e2fa8f7f19 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 @@ -14,4 +14,9 @@ export { OnUpdateHandlerArg, OnUpdateHandler } from './types'; export { SerializeResult } from './serialize'; -export { LoadFromJsonButton, OnDoneLoadJsonHandler, TestPipelineButton } from './components'; +export { + LoadFromJsonButton, + OnDoneLoadJsonHandler, + TestPipelineActions, + DocumentsDropdown, +} from './components'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/serialize.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/serialize.ts index 153c9e252ccc0f..edf787f12620c2 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/serialize.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/serialize.ts @@ -5,18 +5,32 @@ */ import { Processor } from '../../../../common/types'; -import { DeserializeResult } from './deserialize'; import { ProcessorInternal } from './types'; -type SerializeArgs = DeserializeResult; +interface SerializeArgs { + /** + * The deserialized pipeline to convert + */ + pipeline: { + processors: ProcessorInternal[]; + onFailure?: ProcessorInternal[]; + }; + /** + * For simulation, we add the "tag" field equal to the internal processor id so that we can map the simulate results to each processor + */ + copyIdToTag?: boolean; +} export interface SerializeResult { processors: Processor[]; on_failure?: Processor[]; } -const convertProcessorInternalToProcessor = (processor: ProcessorInternal): Processor => { - const { options, onFailure, type } = processor; +const convertProcessorInternalToProcessor = ( + processor: ProcessorInternal, + copyIdToTag?: boolean +): Processor => { + const { options, onFailure, type, id } = processor; const outProcessor = { [type]: { ...options, @@ -24,26 +38,32 @@ const convertProcessorInternalToProcessor = (processor: ProcessorInternal): Proc }; if (onFailure?.length) { - outProcessor[type].on_failure = convertProcessors(onFailure); - } else if (onFailure) { - outProcessor[type].on_failure = []; + outProcessor[type].on_failure = convertProcessors(onFailure, copyIdToTag); + } + + if (copyIdToTag) { + outProcessor[type].tag = id; } return outProcessor; }; -const convertProcessors = (processors: ProcessorInternal[]) => { +const convertProcessors = (processors: ProcessorInternal[], copyIdToTag?: boolean) => { const convertedProcessors = []; for (const processor of processors) { - convertedProcessors.push(convertProcessorInternalToProcessor(processor)); + convertedProcessors.push(convertProcessorInternalToProcessor(processor, copyIdToTag)); } + return convertedProcessors; }; -export const serialize = ({ processors, onFailure }: SerializeArgs): SerializeResult => { +export const serialize = ({ + pipeline: { processors, onFailure }, + copyIdToTag = false, +}: SerializeArgs): SerializeResult => { return { - processors: convertProcessors(processors), - on_failure: onFailure?.length ? convertProcessors(onFailure) : undefined, + processors: convertProcessors(processors, copyIdToTag), + on_failure: onFailure?.length ? convertProcessors(onFailure, copyIdToTag) : undefined, }; }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/types.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/types.ts index 67920ffafb71a0..9083985b0ff2eb 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 @@ -73,3 +73,35 @@ export interface ContextValue { onTreeAction: OnActionHandler; state: ContextValueState; } + +export interface Document { + _id: string; + [key: string]: any; +} + +export type ProcessorStatus = + | 'success' + | 'error' + | 'error_ignored' + | 'dropped' + | 'skipped' + | 'inactive'; + +export interface ProcessorResult { + processor_type: string; + status: ProcessorStatus; + doc: Document; + tag: string; + ignored_error?: any; + error?: any; + prevProcessorResult?: ProcessorResult; + [key: string]: any; +} + +export interface ProcessorResults { + processor_results: ProcessorResult[]; +} + +export interface VerboseTestOutput { + docs: ProcessorResults[]; +} diff --git a/x-pack/plugins/ingest_pipelines/public/application/services/api.ts b/x-pack/plugins/ingest_pipelines/public/application/services/api.ts index 0ca1bc328987ff..552e0ed0c41b22 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/services/api.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/services/api.ts @@ -105,7 +105,7 @@ export class ApiService { return result; } - public async simulatePipeline(testConfig: { + public async simulatePipeline(reqBody: { documents: object[]; verbose?: boolean; pipeline: Pick; @@ -113,7 +113,7 @@ export class ApiService { const result = await this.sendRequest({ path: `${API_BASE_PATH}/simulate`, method: 'post', - body: JSON.stringify(testConfig), + body: JSON.stringify(reqBody), }); this.trackUiMetric(UIM_PIPELINE_SIMULATE); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index d7de5f780d032b..245fa36040cd65 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9829,7 +9829,6 @@ "xpack.ingestPipelines.requestFlyout.unnamedTitle": "リクエスト", "xpack.ingestPipelines.settingsFormOnFailureFlyout.addButtonLabel": "追加", "xpack.ingestPipelines.settingsFormOnFailureFlyout.cancelButtonLabel": "キャンセル", - "xpack.ingestPipelines.tabs.documentsTabTitle": "ドキュメント", "xpack.ingestPipelines.tabs.outputTabTitle": "アウトプット", "xpack.ingestPipelines.testPipelineFlyout.documentsForm.documentsFieldLabel": "ドキュメント", "xpack.ingestPipelines.testPipelineFlyout.documentsForm.documentsJsonError": "ドキュメントJSONが無効です。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 7795959a8e3622..77b2825be0298d 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -9832,7 +9832,6 @@ "xpack.ingestPipelines.requestFlyout.unnamedTitle": "请求", "xpack.ingestPipelines.settingsFormOnFailureFlyout.addButtonLabel": "添加", "xpack.ingestPipelines.settingsFormOnFailureFlyout.cancelButtonLabel": "取消", - "xpack.ingestPipelines.tabs.documentsTabTitle": "文档", "xpack.ingestPipelines.tabs.outputTabTitle": "输出", "xpack.ingestPipelines.testPipelineFlyout.documentsForm.documentsFieldLabel": "文档", "xpack.ingestPipelines.testPipelineFlyout.documentsForm.documentsJsonError": "文档 JSON 无效。",