From 93e37af4f188e6611c7ccec1eabc499c37f885ac Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 15 Sep 2020 12:07:28 -0700 Subject: [PATCH 1/9] [@kbn/utils] Adds missing dependency (#77536) Signed-off-by: Tyler Smalley --- packages/kbn-utils/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/kbn-utils/package.json b/packages/kbn-utils/package.json index 2ad742650f74d..15fe5c6df5648 100644 --- a/packages/kbn-utils/package.json +++ b/packages/kbn-utils/package.json @@ -10,7 +10,8 @@ "kbn:watch": "yarn build --watch" }, "dependencies": { - "@kbn/config-schema": "1.0.0" + "@kbn/config-schema": "1.0.0", + "load-json-file": "^6.2.0" }, "devDependencies": { "typescript": "4.0.2" From 309fe76742f934f4f328927c1492dbc146eb7231 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Tue, 15 Sep 2020 16:24:04 -0400 Subject: [PATCH 2/9] [Ingest pipelines] Polish pipeline debugging workflow (#76058) --- .../pipeline_processors_editor.helpers.tsx | 10 +- .../pipeline_processors_editor.test.tsx | 2 +- .../__jest__/test_pipeline.helpers.tsx | 7 +- .../__jest__/test_pipeline.test.tsx | 17 +- .../documents_dropdown.scss | 3 - .../documents_dropdown/documents_dropdown.tsx | 70 ----- .../components/index.ts | 8 +- .../manage_processor_form.container.tsx | 74 ----- .../manage_processor_form.tsx | 236 ---------------- .../processor_output.tsx | 217 --------------- .../pipeline_processors_editor_item.scss | 2 +- ...pipeline_processors_editor_item_status.tsx | 21 +- .../processor_form/add_processor_form.tsx | 134 ++++++++++ .../documentation_button.tsx | 0 .../processor_form/edit_processor_form.tsx | 253 ++++++++++++++++++ .../field_components/index.ts | 0 .../field_components/text_editor.tsx | 0 .../field_components/xjson_editor.tsx | 0 .../index.ts | 6 +- .../processor_form.container.tsx | 127 +++++++++ .../processor_form/processor_output/index.ts | 7 + .../processor_output/processor_output.scss | 12 + .../processor_output/processor_output.tsx | 240 +++++++++++++++++ .../processor_settings_fields.tsx | 0 .../processors/append.tsx | 0 .../processors/bytes.tsx | 0 .../processors/circle.tsx | 0 .../common_fields/common_processor_fields.tsx | 0 .../common_fields/field_name_field.tsx | 0 .../common_fields/ignore_missing_field.tsx | 0 .../processors/common_fields/index.ts | 0 .../common_fields/processor_type_field.tsx | 0 .../common_fields/properties_field.tsx | 0 .../processors/common_fields/target_field.tsx | 0 .../processors/convert.tsx | 0 .../processors/csv.tsx | 0 .../processors/custom.tsx | 3 +- .../processors/date.tsx | 0 .../processors/date_index_name.tsx | 0 .../processors/dissect.tsx | 0 .../processors/dot_expander.tsx | 0 .../processors/drop.tsx | 0 .../processors/enrich.tsx | 0 .../processors/fail.tsx | 0 .../processors/foreach.tsx | 0 .../processors/geoip.tsx | 0 .../processors/grok.tsx | 0 .../processors/gsub.tsx | 0 .../processors/html_strip.tsx | 0 .../processors/index.ts | 0 .../processors/inference.tsx | 0 .../processors/join.tsx | 0 .../processors/json.tsx | 0 .../processors/kv.tsx | 0 .../processors/lowercase.tsx | 0 .../processors/pipeline.tsx | 0 .../processors/remove.tsx | 0 .../processors/rename.tsx | 0 .../processors/script.tsx | 0 .../processors/set.tsx | 0 .../processors/set_security_user.tsx | 0 .../processors/shared.ts | 0 .../processors/sort.tsx | 0 .../processors/split.tsx | 0 .../processors/trim.tsx | 0 .../processors/uppercase.tsx | 0 .../processors/url_decode.tsx | 0 .../processors/user_agent.tsx | 0 .../components/shared/index.ts | 2 + .../shared/map_processor_type_to_form.tsx | 2 +- .../shared/status_icons/error_icon.tsx | 18 ++ .../status_icons/error_ignored_icon.tsx | 24 ++ .../components/shared/status_icons/index.ts | 9 + .../shared/status_icons/skipped_icon.tsx | 18 ++ .../test_pipeline/add_documents_button.tsx | 8 +- .../documents_dropdown.scss | 3 + .../documents_dropdown/documents_dropdown.tsx | 138 ++++++++++ .../documents_dropdown/index.ts | 0 .../test_pipeline/test_output_button.tsx | 40 +-- .../test_pipeline/test_pipeline_actions.tsx | 56 ++-- .../test_pipeline_flyout.container.tsx | 177 ++++++++++++ .../test_pipeline/test_pipeline_flyout.tsx | 151 ++++------- .../tab_documents.tsx | 137 +++------- .../test_pipeline_flyout_tabs/tab_output.tsx | 27 +- .../test_pipeline_tabs.tsx | 12 +- .../context/processors_context.tsx | 12 +- .../context/test_pipeline_context.tsx | 10 +- .../pipeline_processors_editor/deserialize.ts | 41 ++- .../pipeline_processors_editor/index.ts | 7 +- .../pipeline_processors_editor/types.ts | 2 +- .../translations/translations/ja-JP.json | 3 - .../translations/translations/zh-CN.json | 3 - 92 files changed, 1411 insertions(+), 938 deletions(-) delete mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/documents_dropdown/documents_dropdown.scss delete mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/documents_dropdown/documents_dropdown.tsx delete mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/manage_processor_form.container.tsx delete mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/manage_processor_form.tsx delete 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/processor_form/add_processor_form.tsx rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/{manage_processor_form => processor_form}/documentation_button.tsx (100%) create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/edit_processor_form.tsx rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/{manage_processor_form => processor_form}/field_components/index.ts (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/{manage_processor_form => processor_form}/field_components/text_editor.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/{manage_processor_form => processor_form}/field_components/xjson_editor.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/{manage_processor_form => processor_form}/index.ts (71%) create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processor_form.container.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processor_output/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processor_output/processor_output.scss create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processor_output/processor_output.tsx rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/{manage_processor_form => processor_form}/processor_settings_fields.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/{manage_processor_form => processor_form}/processors/append.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/{manage_processor_form => processor_form}/processors/bytes.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/{manage_processor_form => processor_form}/processors/circle.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/{manage_processor_form => processor_form}/processors/common_fields/common_processor_fields.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/{manage_processor_form => processor_form}/processors/common_fields/field_name_field.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/{manage_processor_form => processor_form}/processors/common_fields/ignore_missing_field.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/{manage_processor_form => processor_form}/processors/common_fields/index.ts (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/{manage_processor_form => processor_form}/processors/common_fields/processor_type_field.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/{manage_processor_form => processor_form}/processors/common_fields/properties_field.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/{manage_processor_form => processor_form}/processors/common_fields/target_field.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/{manage_processor_form => processor_form}/processors/convert.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/{manage_processor_form => processor_form}/processors/csv.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/{manage_processor_form => processor_form}/processors/custom.tsx (96%) rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/{manage_processor_form => processor_form}/processors/date.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/{manage_processor_form => processor_form}/processors/date_index_name.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/{manage_processor_form => processor_form}/processors/dissect.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/{manage_processor_form => processor_form}/processors/dot_expander.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/{manage_processor_form => processor_form}/processors/drop.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/{manage_processor_form => processor_form}/processors/enrich.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/{manage_processor_form => processor_form}/processors/fail.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/{manage_processor_form => processor_form}/processors/foreach.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/{manage_processor_form => processor_form}/processors/geoip.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/{manage_processor_form => processor_form}/processors/grok.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/{manage_processor_form => processor_form}/processors/gsub.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/{manage_processor_form => processor_form}/processors/html_strip.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/{manage_processor_form => processor_form}/processors/index.ts (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/{manage_processor_form => processor_form}/processors/inference.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/{manage_processor_form => processor_form}/processors/join.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/{manage_processor_form => processor_form}/processors/json.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/{manage_processor_form => processor_form}/processors/kv.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/{manage_processor_form => processor_form}/processors/lowercase.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/{manage_processor_form => processor_form}/processors/pipeline.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/{manage_processor_form => processor_form}/processors/remove.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/{manage_processor_form => processor_form}/processors/rename.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/{manage_processor_form => processor_form}/processors/script.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/{manage_processor_form => processor_form}/processors/set.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/{manage_processor_form => processor_form}/processors/set_security_user.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/{manage_processor_form => processor_form}/processors/shared.ts (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/{manage_processor_form => processor_form}/processors/sort.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/{manage_processor_form => processor_form}/processors/split.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/{manage_processor_form => processor_form}/processors/trim.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/{manage_processor_form => processor_form}/processors/uppercase.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/{manage_processor_form => processor_form}/processors/url_decode.tsx (100%) rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/{manage_processor_form => processor_form}/processors/user_agent.tsx (100%) create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/status_icons/error_icon.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/status_icons/error_ignored_icon.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/status_icons/index.ts create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/status_icons/skipped_icon.tsx create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/documents_dropdown/documents_dropdown.scss create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/documents_dropdown/documents_dropdown.tsx rename x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/{ => test_pipeline}/documents_dropdown/index.ts (100%) create mode 100644 x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout.container.tsx 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 2e7a47e0c93de..e46e5156e30f3 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 @@ -126,7 +126,7 @@ const createActions = (testBed: TestBed) => { }); }); await act(async () => { - find('processorSettingsForm.submitButton').simulate('click'); + find('addProcessorForm.submitButton').simulate('click'); }); }, @@ -166,7 +166,7 @@ const createActions = (testBed: TestBed) => { }); }); await act(async () => { - find('processorSettingsForm.submitButton').simulate('click'); + find('addProcessorForm.submitButton').simulate('click'); }); }, @@ -202,8 +202,10 @@ type TestSubject = | 'pipelineEditorDoneButton' | 'pipelineEditorOnFailureToggle' | 'addProcessorsButtonLevel1' - | 'processorSettingsForm' - | 'processorSettingsForm.submitButton' + | 'editProcessorForm' + | 'editProcessorForm.submitButton' + | 'addProcessorForm.submitButton' + | 'addProcessorForm' | 'processorOptionsEditor' | 'processorSettingsFormFlyout' | 'processorTypeSelector' 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 38c652f41e5e1..74ae8b8894b9f 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 @@ -180,7 +180,7 @@ describe('Pipeline Editor', () => { it('prevents moving a processor while in edit mode', () => { const { find, exists } = testBed; find('processors>0.manageItemButton').simulate('click'); - expect(exists('processorSettingsForm')).toBe(true); + expect(exists('editProcessorForm')).toBe(true); expect(find('processors>0.moveItemButton').props().disabled).toBe(true); expect(find('processors>1.moveItemButton').props().disabled).toBe(true); }); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/test_pipeline.helpers.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/test_pipeline.helpers.tsx index fec3259fa019b..f4c89d7a1058a 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/test_pipeline.helpers.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/test_pipeline.helpers.tsx @@ -140,8 +140,8 @@ const createActions = (testBed: TestBed) => { component.update(); }, - clickProcessorOutputTab() { - act(() => { + async clickProcessorOutputTab() { + await act(async () => { find('outputTab').simulate('click'); }); component.update(); @@ -224,7 +224,8 @@ type TestSubject = | 'processorStatusIcon' | 'documentsTab' | 'manageItemButton' - | 'processorSettingsForm' + | 'addProcessorForm' + | 'editProcessorForm' | 'configurationTab' | 'outputTab' | 'processorOutputTabContent' diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/test_pipeline.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/test_pipeline.test.tsx index 339c840bb86f1..e5118a6e465af 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/test_pipeline.test.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/test_pipeline.test.tsx @@ -44,7 +44,7 @@ describe('Test pipeline', () => { describe('Test pipeline actions', () => { it('should successfully add sample documents and execute the pipeline', async () => { - const { find, actions, exists } = testBed; + const { actions, exists } = testBed; httpRequestsMockHelpers.setSimulatePipelineResponse(SIMULATE_RESPONSE); @@ -59,7 +59,6 @@ describe('Test pipeline', () => { expect(exists('testPipelineFlyout')).toBe(true); expect(exists('documentsTabContent')).toBe(true); expect(exists('outputTabContent')).toBe(false); - expect(find('outputTab').props().disabled).toEqual(true); // Add sample documents and click run actions.addDocumentsJson(JSON.stringify(DOCUMENTS)); @@ -89,21 +88,25 @@ describe('Test pipeline', () => { }); // Verify output tab is active - expect(find('outputTab').props().disabled).toEqual(false); expect(exists('documentsTabContent')).toBe(false); expect(exists('outputTabContent')).toBe(true); // Click reload button and verify request const totalRequests = server.requests.length; await actions.clickRefreshOutputButton(); - expect(server.requests.length).toBe(totalRequests + 1); + // There will be two requests made to the simulate API + // the second request will have verbose enabled to update the processor results + expect(server.requests.length).toBe(totalRequests + 2); + expect(server.requests[server.requests.length - 2].url).toBe( + '/api/ingest_pipelines/simulate' + ); expect(server.requests[server.requests.length - 1].url).toBe( '/api/ingest_pipelines/simulate' ); // Click verbose toggle and verify request await actions.toggleVerboseSwitch(); - expect(server.requests.length).toBe(totalRequests + 2); + expect(server.requests.length).toBe(totalRequests + 3); expect(server.requests[server.requests.length - 1].url).toBe( '/api/ingest_pipelines/simulate' ); @@ -228,10 +231,10 @@ describe('Test pipeline', () => { // Click processor to open manage flyout await actions.clickProcessor('processors>0'); // Verify flyout opened - expect(exists('processorSettingsForm')).toBe(true); + expect(exists('editProcessorForm')).toBe(true); // Navigate to "Output" tab - actions.clickProcessorOutputTab(); + await actions.clickProcessorOutputTab(); // Verify content expect(exists('processorOutputTabContent')).toBe(true); }); 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 deleted file mode 100644 index c5b14dc129b0e..0000000000000 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/documents_dropdown/documents_dropdown.scss +++ /dev/null @@ -1,3 +0,0 @@ -.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 deleted file mode 100644 index e26b6a2890fe4..0000000000000 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/documents_dropdown/documents_dropdown.tsx +++ /dev/null @@ -1,70 +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 { 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} - data-test-subj="documentsDropdown" - /> - - - ); -}; 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 435d0ed66c4b0..d476202aa43bb 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 @@ -4,11 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export { - ManageProcessorForm, - ManageProcessorFormOnSubmitArg, - OnSubmitHandler, -} from './manage_processor_form'; +export { ProcessorForm, ProcessorFormOnSubmitArg, OnSubmitHandler } from './processor_form'; export { ProcessorsTree, ProcessorInfo, OnActionHandler } from './processors_tree'; @@ -22,6 +18,4 @@ export { OnDoneLoadJsonHandler, LoadFromJsonButton } from './load_from_json'; 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/manage_processor_form/manage_processor_form.container.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/manage_processor_form.container.tsx deleted file mode 100644 index 083529921b0a7..0000000000000 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/manage_processor_form.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, useCallback, useEffect } from 'react'; - -import { useForm, OnFormUpdateArg, FormData, useKibana } from '../../../../../shared_imports'; -import { ProcessorInternal } from '../../types'; - -import { ManageProcessorForm as ViewComponent } from './manage_processor_form'; - -export type ManageProcessorFormOnSubmitArg = Omit; - -export type OnSubmitHandler = (processor: ManageProcessorFormOnSubmitArg) => void; - -export type OnFormUpdateHandler = (form: OnFormUpdateArg) => void; - -interface Props { - onFormUpdate: OnFormUpdateHandler; - onSubmit: OnSubmitHandler; - isOnFailure: boolean; - onOpen: () => void; - onClose: () => void; - processor?: ProcessorInternal; -} - -export const ManageProcessorForm: FunctionComponent = ({ - processor, - onFormUpdate, - onSubmit, - ...rest -}) => { - const { services } = useKibana(); - - const handleSubmit = useCallback( - async (data: FormData, isValid: boolean) => { - if (isValid) { - const { type, customOptions, fields } = data; - onSubmit({ - type, - options: customOptions ? customOptions : fields, - }); - } - }, - [onSubmit] - ); - - const maybeProcessorOptions = processor?.options; - const { form } = useForm({ - defaultValue: { fields: maybeProcessorOptions ?? {} }, - onSubmit: handleSubmit, - }); - - useEffect(() => { - const subscription = form.subscribe(onFormUpdate); - return subscription.unsubscribe; - - // TODO: Address this issue - // For some reason adding `form` object to the dependencies array here is causing an - // infinite update loop. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [onFormUpdate]); - - return ( - - ); -}; 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 deleted file mode 100644 index ee8ca71e58446..0000000000000 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/manage_processor_form.tsx +++ /dev/null @@ -1,236 +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 { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React, { FunctionComponent, memo, useEffect, useState } from 'react'; -import { - EuiButton, - EuiButtonEmpty, - EuiFlyout, - EuiFlyoutHeader, - EuiFlyoutBody, - EuiFlyoutFooter, - EuiTabs, - EuiTab, - EuiTitle, - EuiFlexGroup, - EuiFlexItem, - EuiSpacer, -} from '@elastic/eui'; - -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; - processor?: ProcessorInternal; - form: FormHook; - onClose: () => void; - onOpen: () => void; - esDocsBasePath: string; -} - -const updateButtonLabel = i18n.translate( - 'xpack.ingestPipelines.settingsFormOnFailureFlyout.updateButtonLabel', - { defaultMessage: 'Update' } -); - -const addButtonLabel = i18n.translate( - 'xpack.ingestPipelines.settingsFormOnFailureFlyout.addButtonLabel', - { defaultMessage: 'Add' } -); - -const cancelButtonLabel = i18n.translate( - 'xpack.ingestPipelines.settingsFormOnFailureFlyout.cancelButtonLabel', - { defaultMessage: 'Cancel' } -); - -export type TabType = 'configuration' | 'output'; - -interface Tab { - id: TabType; - name: string; -} - -const tabs: Tab[] = [ - { - id: 'configuration', - name: i18n.translate( - 'xpack.ingestPipelines.settingsFormOnFailureFlyout.configurationTabTitle', - { - defaultMessage: 'Configuration', - } - ), - }, - { - id: 'output', - name: i18n.translate('xpack.ingestPipelines.settingsFormOnFailureFlyout.outputTabTitle', { - defaultMessage: 'Output', - }), - }, -]; - -const getFlyoutTitle = (isOnFailure: boolean, isExistingProcessor: boolean) => { - if (isExistingProcessor) { - return isOnFailure ? ( - - ) : ( - - ); - } - - return isOnFailure ? ( - - ) : ( - - ); -}; - -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(); - }, - [] /* eslint-disable-line react-hooks/exhaustive-deps */ - ); - - const [activeTab, setActiveTab] = useState('configuration'); - - let flyoutContent: React.ReactNode; - - if (activeTab === 'output') { - flyoutContent = ( - - ); - } else { - flyoutContent = ; - } - - return ( -
- - - - -
- -

{getFlyoutTitle(isOnFailure, Boolean(processor))}

-
-
-
- - - {({ type }) => { - const formDescriptor = getProcessorDescriptor(type as any); - - if (formDescriptor) { - return ( - - ); - } - return null; - }} - - -
-
- - {processor ? ( - <> - - {tabs.map((tab) => ( - { - setActiveTab(tab.id); - }} - 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} - - ))} - - - - ) : undefined} - - {flyoutContent} - - - - - {cancelButtonLabel} - - - - {processor ? updateButtonLabel : addButtonLabel} - - - - -
-
- ); - }, - (previous, current) => { - return previous.processor === current.processor; - } -); 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 deleted file mode 100644 index c30fdad969b24..0000000000000 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processor_output.tsx +++ /dev/null @@ -1,217 +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 { - 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 d9c3d84eec082..55630fa96d9b0 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 @@ -63,6 +63,6 @@ &__statusContainer { // Prevent content jump when spinner renders - min-width: 12px; + min-width: 15px; } } diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item_status.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item_status.tsx index a58d482022b4d..08d456b47180c 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item_status.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item_status.tsx @@ -6,11 +6,12 @@ import React, { FunctionComponent } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiToolTip, EuiIcon } from '@elastic/eui'; +import { EuiToolTip, EuiIcon, IconType } from '@elastic/eui'; import { ProcessorStatus } from '../types'; +import { ErrorIcon, ErrorIgnoredIcon, SkippedIcon } from './shared'; interface ProcessorStatusIcon { - icon: string; + icon: IconType; iconColor: string; label: string; } @@ -24,28 +25,28 @@ const processorStatusToIconMap: Record = { }), }, error: { - icon: 'crossInACircleFilled', + icon: ErrorIcon, iconColor: 'danger', label: i18n.translate('xpack.ingestPipelines.pipelineEditorItem.errorStatusAriaLabel', { defaultMessage: 'Error', }), }, error_ignored: { - icon: 'alert', - iconColor: 'warning', + icon: ErrorIgnoredIcon, + iconColor: 'danger', label: i18n.translate('xpack.ingestPipelines.pipelineEditorItem.errorIgnoredStatusAriaLabel', { defaultMessage: 'Error ignored', }), }, dropped: { - icon: 'alert', - iconColor: 'warning', + icon: 'indexClose', + iconColor: 'subdued', label: i18n.translate('xpack.ingestPipelines.pipelineEditorItem.droppedStatusAriaLabel', { defaultMessage: 'Dropped', }), }, skipped: { - icon: 'dot', + icon: SkippedIcon, iconColor: 'subdued', label: i18n.translate('xpack.ingestPipelines.pipelineEditorItem.skippedStatusAriaLabel', { defaultMessage: 'Skipped', @@ -53,7 +54,7 @@ const processorStatusToIconMap: Record = { }, inactive: { icon: 'dot', - iconColor: 'subdued', + iconColor: '#D3DAE6', // $euiColorLightShade label: i18n.translate('xpack.ingestPipelines.pipelineEditorItem.inactiveStatusAriaLabel', { defaultMessage: 'Not run', }), @@ -64,7 +65,7 @@ const processorStatusToIconMap: Record = { // This is not expected and likely means we need to modify the code to support a new status const unknownStatus = { icon: 'dot', - iconColor: 'subdued', + iconColor: '#D3DAE6', // $euiColorLightShade label: i18n.translate('xpack.ingestPipelines.pipelineEditorItem.unknownStatusAriaLabel', { defaultMessage: 'Unknown', }), diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/add_processor_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/add_processor_form.tsx new file mode 100644 index 0000000000000..5231a3d17811b --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/add_processor_form.tsx @@ -0,0 +1,134 @@ +/* + * 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, useEffect } from 'react'; +import { + EuiButton, + EuiButtonEmpty, + EuiFlyout, + EuiFlyoutHeader, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiTitle, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; + +import { Form, FormDataProvider, FormHook } from '../../../../../shared_imports'; +import { getProcessorDescriptor } from '../shared'; + +import { DocumentationButton } from './documentation_button'; +import { ProcessorSettingsFields } from './processor_settings_fields'; + +interface Fields { + fields: { [key: string]: any }; +} +export interface Props { + isOnFailure: boolean; + form: FormHook; + onOpen: () => void; + esDocsBasePath: string; + closeFlyout: () => void; + handleSubmit: (shouldCloseFlyout?: boolean) => Promise; +} + +const addButtonLabel = i18n.translate( + 'xpack.ingestPipelines.addProcessorFormOnFailureFlyout.addButtonLabel', + { defaultMessage: 'Add' } +); + +const cancelButtonLabel = i18n.translate( + 'xpack.ingestPipelines.addProcesorFormOnFailureFlyout.cancelButtonLabel', + { defaultMessage: 'Cancel' } +); + +const getFlyoutTitle = (isOnFailure: boolean) => { + return isOnFailure ? ( + + ) : ( + + ); +}; + +export const AddProcessorForm: FunctionComponent = ({ + isOnFailure, + onOpen, + form, + esDocsBasePath, + closeFlyout, + handleSubmit, +}) => { + useEffect( + () => { + onOpen(); + }, + [] /* eslint-disable-line react-hooks/exhaustive-deps */ + ); + + return ( +
+ + + + +
+ +

{getFlyoutTitle(isOnFailure)}

+
+
+
+ + + {({ type }) => { + const formDescriptor = getProcessorDescriptor(type as any); + + if (formDescriptor) { + return ( + + ); + } + return null; + }} + + +
+
+ + + + + + + {cancelButtonLabel} + + + { + await handleSubmit(); + }} + > + {addButtonLabel} + + + + +
+
+ ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/documentation_button.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/documentation_button.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/documentation_button.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/documentation_button.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/edit_processor_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/edit_processor_form.tsx new file mode 100644 index 0000000000000..e449ed75b6343 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/edit_processor_form.tsx @@ -0,0 +1,253 @@ +/* + * 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, useEffect, useState } from 'react'; +import { + EuiButton, + EuiButtonEmpty, + EuiFlyout, + EuiFlyoutHeader, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiTabs, + EuiTab, + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, +} from '@elastic/eui'; + +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'; +import { Fields } from './processor_form.container'; + +export interface Props { + isOnFailure: boolean; + form: FormHook; + onOpen: () => void; + esDocsBasePath: string; + closeFlyout: () => void; + resetProcessors: () => void; + handleSubmit: (shouldCloseFlyout?: boolean) => Promise; + getProcessor: () => ProcessorInternal; +} + +const updateButtonLabel = i18n.translate( + 'xpack.ingestPipelines.processorFormFlyout.updateButtonLabel', + { defaultMessage: 'Update' } +); + +const cancelButtonLabel = i18n.translate( + 'xpack.ingestPipelines.processorFormFlyout.cancelButtonLabel', + { defaultMessage: 'Cancel' } +); + +export type TabType = 'configuration' | 'output'; + +interface Tab { + id: TabType; + name: string; +} + +const tabs: Tab[] = [ + { + id: 'configuration', + name: i18n.translate( + 'xpack.ingestPipelines.settingsFormOnFailureFlyout.configurationTabTitle', + { + defaultMessage: 'Configuration', + } + ), + }, + { + id: 'output', + name: i18n.translate('xpack.ingestPipelines.settingsFormOnFailureFlyout.outputTabTitle', { + defaultMessage: 'Output', + }), + }, +]; + +const getFlyoutTitle = (isOnFailure: boolean) => { + return isOnFailure ? ( + + ) : ( + + ); +}; + +export const EditProcessorForm: FunctionComponent = ({ + getProcessor, + form, + isOnFailure, + onOpen, + esDocsBasePath, + closeFlyout, + handleSubmit, + resetProcessors, +}) => { + const { testPipelineData, setCurrentTestPipelineData } = useTestPipelineContext(); + const { + testOutputPerProcessor, + config: { selectedDocumentIndex, documents }, + isExecutingPipeline, + } = testPipelineData; + + const processor = getProcessor(); + + const processorOutput = + processor && + testOutputPerProcessor && + testOutputPerProcessor[selectedDocumentIndex][processor.id]; + + const updateSelectedDocument = (index: number) => { + setCurrentTestPipelineData({ + type: 'updateActiveDocument', + payload: { + config: { + selectedDocumentIndex: index, + }, + }, + }); + }; + + useEffect( + () => { + onOpen(); + }, + [] /* eslint-disable-line react-hooks/exhaustive-deps */ + ); + + const [activeTab, setActiveTab] = useState('configuration'); + + let flyoutContent: React.ReactNode; + + if (activeTab === 'output') { + flyoutContent = ( + + ); + } else { + flyoutContent = ; + } + + return ( +
+ { + resetProcessors(); + closeFlyout(); + }} + > + + + +
+ +

{getFlyoutTitle(isOnFailure)}

+
+
+
+ + + {({ type }) => { + const formDescriptor = getProcessorDescriptor(type as any); + + if (formDescriptor) { + return ( + + ); + } + return null; + }} + + +
+
+ + + {tabs.map((tab) => ( + { + if (tab.id === 'output') { + await handleSubmit(false); + } else { + form.reset({ defaultValue: { fields: processor.options } }); + } + setActiveTab(tab.id); + }} + 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} + + ))} + + + + + {flyoutContent} + + + + + { + resetProcessors(); + closeFlyout(); + }} + > + {cancelButtonLabel} + + + + { + if (activeTab === 'output') { + return closeFlyout(); + } + await handleSubmit(); + }} + > + {updateButtonLabel} + + + + +
+
+ ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/field_components/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/field_components/index.ts similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/field_components/index.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/field_components/index.ts diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/field_components/text_editor.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/field_components/text_editor.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/field_components/text_editor.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/field_components/text_editor.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/field_components/xjson_editor.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/field_components/xjson_editor.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/field_components/xjson_editor.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/field_components/xjson_editor.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/index.ts similarity index 71% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/index.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/index.ts index 986bd52e911bf..5a8d2522f1376 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/index.ts @@ -5,7 +5,7 @@ */ export { - ManageProcessorForm, - ManageProcessorFormOnSubmitArg, + ProcessorFormContainer as ProcessorForm, + ProcessorFormOnSubmitArg, OnSubmitHandler, -} from './manage_processor_form.container'; +} from './processor_form.container'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processor_form.container.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processor_form.container.tsx new file mode 100644 index 0000000000000..332908d0756f2 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processor_form.container.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, { FunctionComponent, useCallback, useEffect, useRef } from 'react'; + +import { useForm, OnFormUpdateArg, FormData, useKibana } from '../../../../../shared_imports'; +import { ProcessorInternal } from '../../types'; + +import { EditProcessorForm } from './edit_processor_form'; +import { AddProcessorForm } from './add_processor_form'; + +export type ProcessorFormOnSubmitArg = Omit; + +export type OnSubmitHandler = (processor: ProcessorFormOnSubmitArg) => void; + +export type OnFormUpdateHandler = (form: OnFormUpdateArg) => void; + +export interface Fields { + fields: { [key: string]: any }; +} + +interface Props { + onFormUpdate: OnFormUpdateHandler; + onSubmit: OnSubmitHandler; + isOnFailure: boolean; + onOpen: () => void; + onClose: () => void; + processor?: ProcessorInternal; +} + +export const ProcessorFormContainer: FunctionComponent = ({ + processor, + onFormUpdate, + onSubmit, + onClose, + ...rest +}) => { + const { services } = useKibana(); + + // We need to keep track of the processor form state if the user + // has made config changes, navigated between tabs (Configuration vs. Output) + // and has not yet submitted the form + const unsavedFormState = useRef(); + + const getProcessor = useCallback((): ProcessorInternal => { + let options; + + if (unsavedFormState?.current) { + options = unsavedFormState.current; + } else { + options = processor?.options ?? {}; + } + + return { ...processor, options } as ProcessorInternal; + }, [processor, unsavedFormState]); + + const { form } = useForm({ + defaultValue: { fields: getProcessor().options }, + }); + + const handleSubmit = useCallback( + async (shouldCloseFlyout: boolean = true) => { + const { isValid, data } = await form.submit(); + + if (isValid) { + const { type, customOptions, fields } = data as FormData; + const options = customOptions ? customOptions : fields; + + unsavedFormState.current = options; + + onSubmit({ + type, + options, + }); + + if (shouldCloseFlyout) { + onClose(); + } + } + }, + [form, onClose, onSubmit] + ); + + const resetProcessors = useCallback(() => { + onSubmit({ + type: processor!.type, + options: processor?.options || {}, + }); + }, [onSubmit, processor]); + + useEffect(() => { + const subscription = form.subscribe(onFormUpdate); + return subscription.unsubscribe; + + // TODO: Address this issue + // For some reason adding `form` object to the dependencies array here is causing an + // infinite update loop. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [onFormUpdate]); + + if (processor) { + return ( + + ); + } else { + return ( + + ); + } +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processor_output/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processor_output/index.ts new file mode 100644 index 0000000000000..3b506fc9296e3 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processor_output/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 { ProcessorOutput } from './processor_output'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processor_output/processor_output.scss b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processor_output/processor_output.scss new file mode 100644 index 0000000000000..e1b5eb83584ff --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processor_output/processor_output.scss @@ -0,0 +1,12 @@ +.processorOutput { + &__callOut { + &--customIcon { + .euiCallOutHeader { + align-items: center; + } + } + &__codeBlock > pre { + background: transparent; + } + } +} diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processor_output/processor_output.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processor_output/processor_output.tsx new file mode 100644 index 0000000000000..bd0ce6ca2cd52 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processor_output/processor_output.tsx @@ -0,0 +1,240 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +import { + EuiAccordion, + EuiCallOut, + EuiCodeBlock, + EuiText, + EuiSpacer, + EuiSelect, +} from '@elastic/eui'; + +import { SectionLoading } from '../../../../../../shared_imports'; +import { ProcessorResult, Document } from '../../../types'; +import { ErrorIcon, ErrorIgnoredIcon, SkippedIcon } from '../../shared'; + +import './processor_output.scss'; + +export interface Props { + processorOutput?: ProcessorResult; + documents: Document[]; + selectedDocumentIndex: number; + updateSelectedDocument: (index: number) => void; + isExecuting?: boolean; +} + +const i18nTexts = { + 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.', + }), + noOutputCalloutTitle: i18n.translate( + 'xpack.ingestPipelines.processorOutput.noOutputCalloutTitle', + { + defaultMessage: 'Output is not available for this processor.', + } + ), + processorOutputLabel: i18n.translate( + 'xpack.ingestPipelines.processorOutput.processorOutputCodeBlockLabel', + { + defaultMessage: 'Data out', + } + ), + processorErrorTitle: i18n.translate( + 'xpack.ingestPipelines.processorOutput.processorErrorCodeBlockLabel', + { + defaultMessage: 'There was an error', + } + ), + prevProcessorLabel: i18n.translate( + 'xpack.ingestPipelines.processorOutput.processorInputCodeBlockLabel', + { + defaultMessage: 'Data in', + } + ), + processorIgnoredErrorTitle: i18n.translate( + 'xpack.ingestPipelines.processorOutput.ignoredErrorCodeBlockLabel', + { + defaultMessage: 'There was an error that was ignored', + } + ), + documentsDropdownLabel: i18n.translate( + 'xpack.ingestPipelines.processorOutput.documentsDropdownLabel', + { + defaultMessage: 'Test data:', + } + ), + loadingMessage: i18n.translate('xpack.ingestPipelines.processorOutput.loadingMessage', { + defaultMessage: 'Loading processor output…', + }), +}; + +export const ProcessorOutput: FunctionComponent = ({ + processorOutput, + documents, + selectedDocumentIndex, + updateSelectedDocument, + isExecuting, +}) => { + if (isExecuting) { + return {i18nTexts.loadingMessage}; + } + + if (!processorOutput) { + return ; + } + + const { + processorInput, + doc: currentResult, + ignored_error: ignoredError, + error, + status, + } = processorOutput!; + + const NoOutputCallOut: FunctionComponent = () => ( + + ); + + const getOutputContent = () => { + switch (status) { + case 'skipped': + return ( + + ); + case 'dropped': + return ; + case 'success': + if (currentResult) { + return ( + + {JSON.stringify(currentResult, null, 2)} + + ); + } + + return ; + case 'error': + return ( + + + {JSON.stringify(error, null, 2)} + + + ); + case 'error_ignored': + return ( + + + {JSON.stringify(ignoredError, null, 2)} + + + ); + default: + return ; + } + }; + + return ( +
+ +

{i18nTexts.tabDescription}

+
+ + + + {/* Documents dropdown */} + ({ + value: index, + text: i18n.translate('xpack.ingestPipelines.processorOutput.documentLabel', { + defaultMessage: 'Document {number}', + values: { + number: index + 1, + }, + }), + }))} + value={selectedDocumentIndex} + onChange={(e) => { + updateSelectedDocument(Number(e.target.value)); + }} + aria-label={i18nTexts.documentsDropdownLabel} + prepend={i18nTexts.documentsDropdownLabel} + /> + + + + {/* Data-in accordion */} + +

{i18nTexts.prevProcessorLabel}

+ + } + > + <> + + + + {/* If there is no processorInput defined (i.e., it's the first processor), we provide the sample document */} + {JSON.stringify( + processorInput ? processorInput : documents[selectedDocumentIndex], + null, + 2 + )} + + +
+ + + + {/* Data-out content */} + + {i18nTexts.processorOutputLabel} + + + + + {getOutputContent()} +
+ ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processor_settings_fields.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processor_settings_fields.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processor_settings_fields.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processor_settings_fields.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/append.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/append.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/append.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/append.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/bytes.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/bytes.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/bytes.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/bytes.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/circle.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/circle.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/circle.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/circle.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/common_processor_fields.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/common_fields/common_processor_fields.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/common_processor_fields.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/common_fields/common_processor_fields.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/field_name_field.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/common_fields/field_name_field.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/field_name_field.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/common_fields/field_name_field.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/ignore_missing_field.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/common_fields/ignore_missing_field.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/ignore_missing_field.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/common_fields/ignore_missing_field.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/common_fields/index.ts similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/index.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/common_fields/index.ts diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/processor_type_field.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/common_fields/processor_type_field.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/processor_type_field.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/common_fields/processor_type_field.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/properties_field.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/common_fields/properties_field.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/properties_field.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/common_fields/properties_field.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/target_field.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/common_fields/target_field.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/target_field.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/common_fields/target_field.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/convert.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/convert.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/convert.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/convert.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/csv.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/csv.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/csv.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/csv.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/custom.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/custom.tsx similarity index 96% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/custom.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/custom.tsx index c2aab62cf8933..f49e77501f931 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/custom.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/custom.tsx @@ -17,6 +17,7 @@ import { const { emptyField, isJsonField } = fieldValidators; import { XJsonEditor } from '../field_components'; +import { Fields } from '../processor_form.container'; import { EDITOR_PX_HEIGHT } from './shared'; const customConfig: FieldConfig = { @@ -60,7 +61,7 @@ const customConfig: FieldConfig = { }; interface Props { - defaultOptions?: any; + defaultOptions?: Fields['fields']; } /** diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/date.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/date.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/date.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/date.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/date_index_name.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/date_index_name.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/date_index_name.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/date_index_name.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/dissect.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/dissect.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/dissect.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/dissect.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/dot_expander.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/dot_expander.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/dot_expander.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/dot_expander.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/drop.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/drop.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/drop.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/drop.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/enrich.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/enrich.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/enrich.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/enrich.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/fail.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/fail.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/fail.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/fail.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/foreach.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/foreach.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/foreach.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/foreach.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/geoip.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/geoip.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/geoip.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/geoip.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/grok.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/grok.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/grok.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/grok.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/gsub.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/gsub.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/gsub.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/gsub.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/html_strip.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/html_strip.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/html_strip.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/html_strip.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/index.ts similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/index.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/index.ts diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/inference.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/inference.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/inference.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/inference.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/join.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/join.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/join.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/join.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/json.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/json.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/json.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/json.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/kv.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/kv.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/kv.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/kv.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/lowercase.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/lowercase.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/lowercase.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/lowercase.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/pipeline.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/pipeline.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/pipeline.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/pipeline.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/remove.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/remove.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/remove.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/remove.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/rename.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/rename.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/rename.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/rename.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/script.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/script.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/script.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/script.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/set.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/set.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set_security_user.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/set_security_user.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set_security_user.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/set_security_user.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/shared.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/shared.ts similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/shared.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/shared.ts diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/sort.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/sort.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/sort.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/sort.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/split.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/split.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/split.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/split.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/trim.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/trim.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/trim.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/trim.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/uppercase.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/uppercase.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/uppercase.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/uppercase.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/url_decode.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/url_decode.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/url_decode.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/url_decode.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/user_agent.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/user_agent.tsx similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/user_agent.tsx rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processor_form/processors/user_agent.tsx diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/index.ts index 1b4b975b5305e..3f258bf279e42 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/index.ts @@ -9,3 +9,5 @@ export { mapProcessorTypeToDescriptor, ProcessorType, } from './map_processor_type_to_form'; + +export { ErrorIcon, ErrorIgnoredIcon, SkippedIcon } from './status_icons'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/map_processor_type_to_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/map_processor_type_to_form.tsx index 95a8d35c119a6..8d9260f3c822c 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/map_processor_type_to_form.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/map_processor_type_to_form.tsx @@ -46,7 +46,7 @@ import { UrlDecode, UserAgent, FormFieldsComponent, -} from '../manage_processor_form/processors'; +} from '../processor_form/processors'; interface FieldDescriptor { FieldsComponent?: FormFieldsComponent; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/status_icons/error_icon.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/status_icons/error_icon.tsx new file mode 100644 index 0000000000000..58cb56d4f768d --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/status_icons/error_icon.tsx @@ -0,0 +1,18 @@ +/* + * 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'; + +export const ErrorIcon: FunctionComponent = () => ( + + + +); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/status_icons/error_ignored_icon.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/status_icons/error_ignored_icon.tsx new file mode 100644 index 0000000000000..74ceda7687f02 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/status_icons/error_ignored_icon.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; + +export const ErrorIgnoredIcon: FunctionComponent = () => ( + + + + +); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/status_icons/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/status_icons/index.ts new file mode 100644 index 0000000000000..9fe0871e445eb --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/status_icons/index.ts @@ -0,0 +1,9 @@ +/* + * 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 { ErrorIcon } from './error_icon'; +export { ErrorIgnoredIcon } from './error_ignored_icon'; +export { SkippedIcon } from './skipped_icon'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/status_icons/skipped_icon.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/status_icons/skipped_icon.tsx new file mode 100644 index 0000000000000..c540bd3790fb0 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/status_icons/skipped_icon.tsx @@ -0,0 +1,18 @@ +/* + * 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'; + +export const SkippedIcon: FunctionComponent = () => ( + + + +); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/add_documents_button.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/add_documents_button.tsx index e3ef9a9ee5390..26492454cbcf5 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/add_documents_button.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/add_documents_button.tsx @@ -6,6 +6,7 @@ import { i18n } from '@kbn/i18n'; import React, { FunctionComponent } from 'react'; import { EuiButtonEmpty } from '@elastic/eui'; +import { TestPipelineFlyoutTab } from './test_pipeline_flyout_tabs'; const i18nTexts = { buttonLabel: i18n.translate('xpack.ingestPipelines.pipelineEditor.testPipeline.buttonLabel', { @@ -14,16 +15,15 @@ const i18nTexts = { }; interface Props { - openTestPipelineFlyout: () => void; + openFlyout: (activeFlyoutTab: TestPipelineFlyoutTab) => void; } -export const AddDocumentsButton: FunctionComponent = ({ openTestPipelineFlyout }) => { +export const AddDocumentsButton: FunctionComponent = ({ openFlyout }) => { return ( openFlyout('documents')} data-test-subj="addDocumentsButton" - iconType="plusInCircleFilled" > {i18nTexts.buttonLabel} diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/documents_dropdown/documents_dropdown.scss b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/documents_dropdown/documents_dropdown.scss new file mode 100644 index 0000000000000..5deb48a2f01a7 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/documents_dropdown/documents_dropdown.scss @@ -0,0 +1,3 @@ +.documentsDropdownPanel { + min-width: 200px; +} diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/documents_dropdown/documents_dropdown.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/documents_dropdown/documents_dropdown.tsx new file mode 100644 index 0000000000000..269a697a33c17 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/documents_dropdown/documents_dropdown.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 React, { FunctionComponent, useState } from 'react'; +import { + EuiButton, + EuiPopover, + EuiButtonEmpty, + EuiPopoverTitle, + EuiSelectable, + EuiHorizontalRule, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, +} from '@elastic/eui'; + +import { Document } from '../../../types'; + +import { TestPipelineFlyoutTab } from '../test_pipeline_flyout_tabs'; + +import './documents_dropdown.scss'; + +const i18nTexts = { + dropdownLabel: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.testPipeline.documentsdropdown.dropdownLabel', + { + defaultMessage: 'Documents:', + } + ), + addDocumentsButtonLabel: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.testPipeline.documentsDropdown.buttonLabel', + { + defaultMessage: 'Add documents', + } + ), + popoverTitle: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.testPipeline.documentsDropdown.popoverTitle', + { + defaultMessage: 'Test documents', + } + ), +}; + +interface Props { + documents: Document[]; + selectedDocumentIndex: number; + updateSelectedDocument: (index: number) => void; + openFlyout: (activeFlyoutTab: TestPipelineFlyoutTab) => void; +} + +export const DocumentsDropdown: FunctionComponent = ({ + documents, + selectedDocumentIndex, + updateSelectedDocument, + openFlyout, +}) => { + const [showPopover, setShowPopover] = useState(false); + + const managePipelineButton = ( + setShowPopover((previousBool) => !previousBool)} + iconType="arrowDown" + iconSide="right" + > + {i18n.translate('xpack.ingestPipelines.pipelineEditor.testPipeline.selectedDocumentLabel', { + defaultMessage: 'Document {selectedDocument}', + values: { + selectedDocument: selectedDocumentIndex + 1, + }, + })} + + ); + + return ( + setShowPopover(false)} + button={managePipelineButton} + panelPaddingSize="none" + withTitle + repositionOnScroll + data-test-subj="documentsDropdown" + panelClassName="documentsDropdownPanel" + > + ({ + key: index.toString(), + checked: selectedDocumentIndex === index ? 'on' : undefined, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.testPipeline.documentLabel', { + defaultMessage: 'Document {documentNumber}', + values: { + documentNumber: index + 1, + }, + }), + }))} + onChange={(newOptions) => { + const selectedOption = newOptions.find((option) => option.checked === 'on'); + if (selectedOption) { + updateSelectedDocument(Number(selectedOption.key!)); + } + + setShowPopover(false); + }} + > + {(list, search) => ( +
+ {i18nTexts.popoverTitle} + {list} +
+ )} +
+ + + + + + { + openFlyout('documents'); + setShowPopover(false); + }} + data-test-subj="addDocumentsButton" + > + {i18nTexts.addDocumentsButtonLabel} + + + + + +
+ ); +}; 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/test_pipeline/documents_dropdown/index.ts similarity index 100% rename from x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/documents_dropdown/index.ts rename to x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/documents_dropdown/index.ts 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 index 6fd1adad54f84..9018042229590 100644 --- 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 @@ -5,7 +5,8 @@ */ import { i18n } from '@kbn/i18n'; import React, { FunctionComponent } from 'react'; -import { EuiButton, EuiToolTip } from '@elastic/eui'; +import { EuiButton } from '@elastic/eui'; +import { TestPipelineFlyoutTab } from './test_pipeline_flyout_tabs'; const i18nTexts = { buttonLabel: i18n.translate( @@ -14,46 +15,15 @@ const i18nTexts = { defaultMessage: 'View output', } ), - disabledButtonTooltipLabel: i18n.translate( - 'xpack.ingestPipelines.pipelineEditor.testPipeline.outputButtonTooltipLabel', - { - defaultMessage: 'Add documents to view the output', - } - ), }; interface Props { - isDisabled: boolean; - openTestPipelineFlyout: () => void; + openFlyout: (activeFlyoutTab: TestPipelineFlyoutTab) => void; } -export const TestOutputButton: FunctionComponent = ({ - isDisabled, - openTestPipelineFlyout, -}) => { - if (isDisabled) { - return ( - {i18nTexts.disabledButtonTooltipLabel}

}> - - {i18nTexts.buttonLabel} - -
- ); - } - +export const TestOutputButton: FunctionComponent = ({ openFlyout }) => { return ( - + openFlyout('output')} data-test-subj="viewOutputButton"> {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 index eb9d9352e4b90..cec02db26729d 100644 --- 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 @@ -4,15 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ import React, { FunctionComponent, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { useTestPipelineContext, usePipelineProcessorsContext } from '../../context'; - -import { DocumentsDropdown } from '../documents_dropdown'; +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'; +import { TestPipelineFlyout } from './test_pipeline_flyout.container'; + +const i18nTexts = { + testPipelineActionsLabel: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.testPipeline.testPipelineActionsLabel', + { + defaultMessage: 'Test pipeline:', + } + ), +}; export const TestPipelineActions: FunctionComponent = () => { const { testPipelineData, setCurrentTestPipelineData } = useTestPipelineContext(); @@ -40,35 +49,42 @@ export const TestPipelineActions: FunctionComponent = () => { }); }; + const openFlyout = (activeTab: TestPipelineFlyoutTab) => { + setOpenTestPipelineFlyout(true); + setActiveFlyoutTab(activeTab); + }; + return ( <> - + + + + + {i18nTexts.testPipelineActionsLabel} + + + + {documents ? ( ) : ( - { - setOpenTestPipelineFlyout(true); - setActiveFlyoutTab('documents'); - }} - /> + )} - - { - setOpenTestPipelineFlyout(true); - setActiveFlyoutTab('output'); - }} - /> - + + {testOutputPerProcessor && ( + + + + )} + {openTestPipelineFlyout && ( void; + processors: DeserializeResult; +} + +export interface TestPipelineConfig { + 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 { form } = useForm({ + schema: documentsSchema, + defaultValue: { + documents: cachedDocuments || '', + }, + }); + + const [selectedTab, setSelectedTab] = useState(activeTab); + + const [isRunningTest, setIsRunningTest] = useState(false); + const [testingError, setTestingError] = useState(null); + const [testOutput, setTestOutput] = useState(undefined); + + const handleTestPipeline = useCallback( + async ( + { documents, verbose }: TestPipelineConfig, + updateProcessorOutput?: boolean + ): Promise<{ isSuccessful: boolean }> => { + 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); + + // reset the per-processor output + // this is needed in the scenario where the pipeline has already executed, + // but you modified the sample documents and there was an error on re-execution + setCurrentTestPipelineData({ + type: 'updateOutputPerProcessor', + payload: { + isExecutingPipeline: false, + testOutputPerProcessor: undefined, + }, + }); + + return { isSuccessful: false }; + } + + setCurrentTestPipelineData({ + type: 'updateConfig', + payload: { + config: { + documents, + verbose, + }, + }, + }); + + // We sometimes need to manually refresh the per-processor output + // e.g., when clicking the "Refresh output" button and there have been no state changes + if (updateProcessorOutput) { + updateTestOutputPerProcessor(documents, processors); + } + + setTestOutput(currentTestOutput); + + services.notifications.toasts.addSuccess( + i18n.translate('xpack.ingestPipelines.testPipelineFlyout.successNotificationText', { + defaultMessage: 'Pipeline executed', + }), + { + toastLifeTimeMs: 1000, + } + ); + + return { isSuccessful: true }; + }, + [ + processors, + services.api, + services.notifications.toasts, + setCurrentTestPipelineData, + updateTestOutputPerProcessor, + ] + ); + + const validateAndTestPipeline = async () => { + const { isValid, data } = await form.submit(); + + if (!isValid) { + return; + } + + const { documents } = data as { documents: Document[] }; + + const { isSuccessful } = await handleTestPipeline({ + documents: documents!, + verbose: cachedVerbose, + }); + + if (isSuccessful) { + setSelectedTab('output'); + } + }; + + useEffect(() => { + if (cachedDocuments && activeTab === 'output') { + handleTestPipeline({ documents: cachedDocuments, verbose: cachedVerbose }, true); + } + // We only want to know on initial mount if + // there are cached documents and we are on the output tab + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + + ); +}; 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 index b26c6f536366d..46271a6bce51c 100644 --- 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 @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, useCallback, useEffect } from 'react'; +import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; import { EuiFlyout, @@ -17,112 +16,46 @@ import { EuiCallOut, } from '@elastic/eui'; -import { useKibana } from '../../../../../shared_imports'; -import { useTestPipelineContext } from '../../context'; -import { serialize } from '../../serialize'; -import { DeserializeResult } from '../../deserialize'; +import { Form, FormHook } from '../../../../../shared_imports'; import { Document } from '../../types'; import { Tabs, TestPipelineFlyoutTab, OutputTab, DocumentsTab } from './test_pipeline_flyout_tabs'; export interface Props { - activeTab: TestPipelineFlyoutTab; onClose: () => void; - processors: DeserializeResult; + handleTestPipeline: ( + testPipelineConfig: TestPipelineConfig, + refreshOutputPerProcessor?: boolean + ) => Promise<{ isSuccessful: boolean }>; + isRunningTest: boolean; + cachedVerbose?: boolean; + cachedDocuments?: Document[]; + testOutput?: any; + form: FormHook; + validateAndTestPipeline: () => Promise; + selectedTab: TestPipelineFlyoutTab; + setSelectedTab: (selectedTa: TestPipelineFlyoutTab) => void; + testingError: any; } -export interface HandleTestPipelineArgs { +export interface TestPipelineConfig { documents: Document[]; verbose?: boolean; } export const TestPipelineFlyout: React.FunctionComponent = ({ + handleTestPipeline, + isRunningTest, + cachedVerbose, + cachedDocuments, + testOutput, + form, + validateAndTestPipeline, + selectedTab, + setSelectedTab, + testingError, 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') { @@ -138,13 +71,19 @@ export const TestPipelineFlyout: React.FunctionComponent = ({ } else { // default to "Documents" tab tabContent = ( - +
+ + ); } @@ -163,9 +102,17 @@ export const TestPipelineFlyout: React.FunctionComponent = ({ { + if (nextTab === 'output') { + // When switching to the output tab, + // we automatically run the pipeline if documents are defined + validateAndTestPipeline(); + } else { + form.reset({ defaultValue: { documents: cachedDocuments! } }); + setSelectedTab(nextTab); + } + }} selectedTab={selectedTab} - getIsDisabled={(tabId) => !testOutput && tabId === 'output'} /> diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/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 index dd12cdab0c934..b2326644340a7 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/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 @@ -10,67 +10,23 @@ import { i18n } from '@kbn/i18n'; import { EuiSpacer, EuiText, EuiButton, EuiLink } from '@elastic/eui'; -import { - getUseField, - Field, - JsonEditorField, - Form, - useForm, - useKibana, -} from '../../../../../../shared_imports'; - -import { TestPipelineContext } from '../../../context'; -import { Document } from '../../../types'; -import { DeserializeResult } from '../../../deserialize'; -import { HandleTestPipelineArgs } from '../test_pipeline_flyout'; -import { documentsSchema } from './documents_schema'; +import { getUseField, Field, JsonEditorField, useKibana } from '../../../../../../shared_imports'; const UseField = getUseField({ component: Field }); interface Props { - handleTestPipeline: (data: HandleTestPipelineArgs) => void; - setPerProcessorOutput: (documents: Document[] | undefined, processors: DeserializeResult) => void; + validateAndTestPipeline: () => Promise; isRunningTest: boolean; - processors: DeserializeResult; - testPipelineData: TestPipelineContext['testPipelineData']; + isSubmitButtonDisabled: boolean; } export const DocumentsTab: React.FunctionComponent = ({ - handleTestPipeline, + validateAndTestPipeline, + isSubmitButtonDisabled, isRunningTest, - setPerProcessorOutput, - processors, - testPipelineData, }) => { const { services } = useKibana(); - const { - config: { documents: cachedDocuments, verbose: cachedVerbose }, - } = testPipelineData; - - const testPipeline = async () => { - const { isValid, data } = await form.submit(); - - if (!isValid) { - return; - } - - const { documents } = data as { documents: Document[] }; - - await handleTestPipeline({ documents: documents!, verbose: 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 || '', - }, - }); - return (
@@ -100,53 +56,46 @@ export const DocumentsTab: React.FunctionComponent = ({ -
- {/* Documents editor */} - + {/* Documents editor */} + - + - - {isRunningTest ? ( - - ) : ( - - )} - - + + {isRunningTest ? ( + + ) : ( + + )} +
); }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/tab_output.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/tab_output.tsx index 926bab6da993c..db6a020e307a5 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/tab_output.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/tab_output.tsx @@ -18,10 +18,13 @@ import { } from '@elastic/eui'; import { Document } from '../../../types'; -import { HandleTestPipelineArgs } from '../test_pipeline_flyout'; +import { TestPipelineConfig } from '../test_pipeline_flyout.container'; interface Props { - handleTestPipeline: (data: HandleTestPipelineArgs) => void; + handleTestPipeline: ( + testPipelineConfig: TestPipelineConfig, + refreshOutputPerProcessor?: boolean + ) => Promise<{ isSuccessful: boolean }>; isRunningTest: boolean; cachedVerbose?: boolean; cachedDocuments: Document[]; @@ -37,12 +40,6 @@ export const OutputTab: React.FunctionComponent = ({ }) => { const [isVerboseEnabled, setIsVerboseEnabled] = useState(Boolean(cachedVerbose)); - const onEnableVerbose = (isVerbose: boolean) => { - setIsVerboseEnabled(isVerbose); - - handleTestPipeline({ documents: cachedDocuments!, verbose: isVerbose }); - }; - let content: React.ReactNode | undefined; if (isRunningTest) { @@ -78,15 +75,23 @@ export const OutputTab: React.FunctionComponent = ({ /> } checked={isVerboseEnabled} - onChange={(e) => onEnableVerbose(e.target.checked)} data-test-subj="verboseOutputToggle" + onChange={async (e) => { + const isVerbose = e.target.checked; + setIsVerboseEnabled(isVerbose); + + await handleTestPipeline({ documents: cachedDocuments!, verbose: isVerbose }); + }} /> - handleTestPipeline({ documents: cachedDocuments!, verbose: isVerboseEnabled }) + onClick={async () => + await handleTestPipeline( + { documents: cachedDocuments!, verbose: isVerboseEnabled }, + true + ) } iconType="refresh" data-test-subj="refreshOutputButton" diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/test_pipeline_tabs.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/test_pipeline_tabs.tsx index abfb86c2afda1..b13fb2df90984 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/test_pipeline_tabs.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/test_pipeline/test_pipeline_flyout_tabs/test_pipeline_tabs.tsx @@ -13,14 +13,9 @@ export type TestPipelineFlyoutTab = 'documents' | 'output'; interface Props { onTabChange: (tab: TestPipelineFlyoutTab) => void; selectedTab: TestPipelineFlyoutTab; - getIsDisabled: (tab: TestPipelineFlyoutTab) => boolean; } -export const Tabs: React.FunctionComponent = ({ - onTabChange, - selectedTab, - getIsDisabled, -}) => { +export const Tabs: React.FunctionComponent = ({ onTabChange, selectedTab }) => { const tabs: Array<{ id: TestPipelineFlyoutTab; name: React.ReactNode; @@ -29,8 +24,8 @@ export const Tabs: React.FunctionComponent = ({ id: 'documents', name: ( ), }, @@ -49,7 +44,6 @@ export const Tabs: React.FunctionComponent = ({ onClick={() => onTabChange(tab.id)} isSelected={tab.id === selectedTab} key={tab.id} - disabled={getIsDisabled(tab.id)} data-test-subj={tab.id.toLowerCase() + 'Tab'} > {tab.name} 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 8c59d484acd08..6595437c01810 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 @@ -38,7 +38,7 @@ import { OnActionHandler } from '../components/processors_tree'; import { ProcessorRemoveModal, PipelineProcessorsItemTooltip, - ManageProcessorForm, + ProcessorForm, OnSubmitHandler, } from '../components'; @@ -159,12 +159,12 @@ export const PipelineProcessorsContextProvider: FunctionComponent = ({ selector: mode.arg.selector, }, }); + break; default: } - setMode({ id: 'idle' }); }, - [processorsDispatch, mode, setMode] + [processorsDispatch, mode] ); const onCloseSettingsForm = useCallback(() => { @@ -208,8 +208,8 @@ 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 + // Make a request to the simulate API and update the processor output + // whenever the documents or processorsState changes (e.g., on move, update, delete) useEffect(() => { updateTestOutputPerProcessor(documents, processorsState); }, [documents, processorsState, updateTestOutputPerProcessor]); @@ -233,7 +233,7 @@ export const PipelineProcessorsContextProvider: FunctionComponent = ({ )} {mode.id === 'managingProcessor' || mode.id === 'creatingProcessor' ? ( - { + const previousProcessorIndex = processorIndex - count; + + if (previousProcessorIndex >= 0) { + const processorResult = document.processor_results[previousProcessorIndex]; + + if (!processorResult.doc) { + const newCount = count + 1; + return getProcessorInput(processorIndex, document, newCount); + } + + return processorResult.doc; + } + + return undefined; +}; + /** * This function takes the verbose response of the simulate API * and maps the results to each processor in the pipeline by the "tag" field @@ -81,11 +114,9 @@ export const deserializeVerboseTestOutput = ( const result = { ...currentResult }; const resultId = result.tag; + // We skip index 0, as the first processor will not have a previous result 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]; + result.processorInput = getProcessorInput(index, doc); } // The tag is added programatically as a way to map 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 71b2e2fa8f7f1..c462b19c79327 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,9 +14,4 @@ export { OnUpdateHandlerArg, OnUpdateHandler } from './types'; export { SerializeResult } from './serialize'; -export { - LoadFromJsonButton, - OnDoneLoadJsonHandler, - TestPipelineActions, - DocumentsDropdown, -} from './components'; +export { LoadFromJsonButton, OnDoneLoadJsonHandler, TestPipelineActions } from './components'; 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 5229f5eb0bb21..42201b3102c28 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 @@ -94,7 +94,7 @@ export interface ProcessorResult { tag: string; ignored_error?: any; error?: any; - prevProcessorResult?: ProcessorResult; + processorInput?: Document; [key: string]: any; } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 0977e99fa0c3c..68f6bc166cd1d 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9497,9 +9497,6 @@ "xpack.ingestPipelines.requestFlyout.descriptionText": "このElasticsearchリクエストは、このパイプラインを作成または更新します。", "xpack.ingestPipelines.requestFlyout.namedTitle": "「{name}」のリクエスト", "xpack.ingestPipelines.requestFlyout.unnamedTitle": "リクエスト", - "xpack.ingestPipelines.settingsFormOnFailureFlyout.addButtonLabel": "追加", - "xpack.ingestPipelines.settingsFormOnFailureFlyout.cancelButtonLabel": "キャンセル", - "xpack.ingestPipelines.settingsFormOnFailureFlyout.updateButtonLabel": "更新", "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 7f8f2a98abae3..cb43cefdc3655 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -9503,9 +9503,6 @@ "xpack.ingestPipelines.requestFlyout.descriptionText": "此 Elasticsearch 请求将创建或更新管道。", "xpack.ingestPipelines.requestFlyout.namedTitle": "对“{name}”的请求", "xpack.ingestPipelines.requestFlyout.unnamedTitle": "请求", - "xpack.ingestPipelines.settingsFormOnFailureFlyout.addButtonLabel": "添加", - "xpack.ingestPipelines.settingsFormOnFailureFlyout.cancelButtonLabel": "取消", - "xpack.ingestPipelines.settingsFormOnFailureFlyout.updateButtonLabel": "更新", "xpack.ingestPipelines.tabs.outputTabTitle": "输出", "xpack.ingestPipelines.testPipelineFlyout.documentsForm.documentsFieldLabel": "文档", "xpack.ingestPipelines.testPipelineFlyout.documentsForm.documentsJsonError": "文档 JSON 无效。", From 6dd558e59cc85ebf3d49c0278c2372aa978f276b Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Tue, 15 Sep 2020 15:30:41 -0500 Subject: [PATCH 3/9] [Security Solution][Detections] Integration test for Editing a Rule (#77090) * Add cypress test around editing a detection rule Right now this just navigates around and verifies that the form is correctly repopulated; next step will be to modify/asset some changes. * Add assertions for editing a rule We already were asserting on the population of the Edit form after creation; this additionally makes modifications, saves them, and asserts the resulting values on the Rule Details page. * Remove unused imports * Inline our cypress expectations So that expectation failures are less obfuscated, the decision was previously made to abstract user navigation into functions, but to leave expectations directly within the test body. * Dynamically assert Rule Details based on titles Rule Details are unfortunately unstructured: they're an array of
s and
s without any hierarchy. To address this, tests were previously hardcoding the order of these fields, and assertions were performed by querying for all
s and then indexing with the hardcoded number (e.g. ABOUT_FALSE_POSITIVES). However, in addition to being unstructured, these fields are also _dynamic_, and will be present/absent depending on the data of the given rule. Thus, we started needing multiple orderings for the different combinations of rule fields/rule types. In the absence of refactoring how we build rule details, I'm introducing a simple helper function to fetch the relevant
by the corresponding
s text. This should be more robust to change and more declarative. * Fix bad merge conflict Lots of these variables no longer exist upstream and this new test needed to be refactored. Co-authored-by: Elastic Machine --- .../alerts_detection_rules_custom.spec.ts | 194 ++++++++++++++---- .../alerts_detection_rules_eql.spec.ts | 64 +++--- .../alerts_detection_rules_ml.spec.ts | 81 ++++---- .../alerts_detection_rules_override.spec.ts | 112 +++++----- .../alerts_detection_rules_threshold.spec.ts | 77 +++---- .../security_solution/cypress/objects/rule.ts | 25 +++ .../cypress/screens/alerts_detection_rules.ts | 2 + .../cypress/screens/create_new_rule.ts | 29 +++ .../cypress/screens/edit_rule.ts | 7 + .../cypress/screens/rule_details.ts | 61 ++---- .../cypress/tasks/alerts_detection_rules.ts | 6 + .../cypress/tasks/create_new_rule.ts | 67 +++--- .../cypress/tasks/edit_rule.ts | 12 ++ .../rules/schedule_item_form/index.tsx | 2 + .../detection_engine/rules/all/columns.tsx | 1 + .../detection_engine/rules/edit/index.tsx | 5 + .../es_archives/custom_rules/data.json.gz | Bin 2885 -> 2975 bytes 17 files changed, 443 insertions(+), 302 deletions(-) create mode 100644 x-pack/plugins/security_solution/cypress/screens/edit_rule.ts create mode 100644 x-pack/plugins/security_solution/cypress/tasks/edit_rule.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts index d9d9fde8fc8cc..17ff1dad79960 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_custom.spec.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { newRule } from '../objects/rule'; +import { newRule, existingRule } from '../objects/rule'; import { CUSTOM_RULES_BTN, @@ -16,26 +16,16 @@ import { SHOWING_RULES_TEXT, } from '../screens/alerts_detection_rules'; import { - ABOUT_FALSE_POSITIVES, ABOUT_INVESTIGATION_NOTES, - ABOUT_MITRE, - ABOUT_RISK, ABOUT_RULE_DESCRIPTION, - ABOUT_SEVERITY, - ABOUT_STEP, - ABOUT_TAGS, - ABOUT_URLS, - DEFINITION_CUSTOM_QUERY, - DEFINITION_INDEX_PATTERNS, - DEFINITION_TIMELINE, - DEFINITION_STEP, INVESTIGATION_NOTES_MARKDOWN, INVESTIGATION_NOTES_TOGGLE, RULE_ABOUT_DETAILS_HEADER_TOGGLE, RULE_NAME_HEADER, - SCHEDULE_LOOPBACK, - SCHEDULE_RUNS, - SCHEDULE_STEP, + getDescriptionForTitle, + ABOUT_DETAILS, + DEFINITION_DETAILS, + SCHEDULE_DETAILS, } from '../screens/rule_details'; import { @@ -53,18 +43,38 @@ import { selectNumberOfRules, waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded, waitForRulesToBeLoaded, + editFirstRule, } from '../tasks/alerts_detection_rules'; import { createAndActivateRule, fillAboutRuleAndContinue, fillDefineCustomRuleWithImportedQueryAndContinue, - expectDefineFormToRepopulateAndContinue, - expectAboutFormToRepopulateAndContinue, + goToAboutStepTab, + goToScheduleStepTab, + goToActionsStepTab, + fillAboutRule, } from '../tasks/create_new_rule'; import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; import { DETECTIONS_URL } from '../urls/navigation'; +import { + ACTIONS_THROTTLE_INPUT, + CUSTOM_QUERY_INPUT, + DEFINE_INDEX_INPUT, + RULE_NAME_INPUT, + RULE_DESCRIPTION_INPUT, + TAGS_FIELD, + SEVERITY_DROPDOWN, + RISK_INPUT, + SCHEDULE_INTERVAL_AMOUNT_INPUT, + SCHEDULE_INTERVAL_UNITS_INPUT, + DEFINE_EDIT_BUTTON, + DEFINE_CONTINUE_BUTTON, + ABOUT_EDIT_BUTTON, + ABOUT_CONTINUE_BTN, +} from '../screens/create_new_rule'; +import { saveEditedRule } from '../tasks/edit_rule'; describe('Detection rules, custom', () => { before(() => { @@ -84,8 +94,19 @@ describe('Detection rules, custom', () => { goToCreateNewRule(); fillDefineCustomRuleWithImportedQueryAndContinue(newRule); fillAboutRuleAndContinue(newRule); - expectDefineFormToRepopulateAndContinue(newRule); - expectAboutFormToRepopulateAndContinue(newRule); + + // expect define step to repopulate + cy.get(DEFINE_EDIT_BUTTON).click(); + cy.get(CUSTOM_QUERY_INPUT).invoke('text').should('eq', newRule.customQuery); + cy.get(DEFINE_CONTINUE_BUTTON).should('exist').click({ force: true }); + cy.get(DEFINE_CONTINUE_BUTTON).should('not.exist'); + + // expect about step to populate + cy.get(ABOUT_EDIT_BUTTON).click(); + cy.get(RULE_NAME_INPUT).invoke('val').should('eq', newRule.name); + cy.get(ABOUT_CONTINUE_BTN).should('exist').click({ force: true }); + cy.get(ABOUT_CONTINUE_BTN).should('not.exist'); + createAndActivateRule(); cy.get(CUSTOM_RULES_BTN).invoke('text').should('eql', 'Custom rules (1)'); @@ -142,32 +163,35 @@ describe('Detection rules, custom', () => { cy.get(RULE_NAME_HEADER).invoke('text').should('eql', `${newRule.name} Beta`); cy.get(ABOUT_RULE_DESCRIPTION).invoke('text').should('eql', newRule.description); - cy.get(ABOUT_STEP).eq(ABOUT_SEVERITY).invoke('text').should('eql', newRule.severity); - cy.get(ABOUT_STEP).eq(ABOUT_RISK).invoke('text').should('eql', newRule.riskScore); - cy.get(ABOUT_STEP).eq(ABOUT_URLS).invoke('text').should('eql', expectedUrls); - cy.get(ABOUT_STEP) - .eq(ABOUT_FALSE_POSITIVES) - .invoke('text') - .should('eql', expectedFalsePositives); - cy.get(ABOUT_STEP).eq(ABOUT_MITRE).invoke('text').should('eql', expectedMitre); - cy.get(ABOUT_STEP).eq(ABOUT_TAGS).invoke('text').should('eql', expectedTags); + cy.get(ABOUT_DETAILS).within(() => { + getDescriptionForTitle('Severity').invoke('text').should('eql', newRule.severity); + getDescriptionForTitle('Risk score').invoke('text').should('eql', newRule.riskScore); + getDescriptionForTitle('Reference URLs').invoke('text').should('eql', expectedUrls); + getDescriptionForTitle('False positive examples') + .invoke('text') + .should('eql', expectedFalsePositives); + getDescriptionForTitle('MITRE ATT&CK').invoke('text').should('eql', expectedMitre); + getDescriptionForTitle('Tags').invoke('text').should('eql', expectedTags); + }); cy.get(RULE_ABOUT_DETAILS_HEADER_TOGGLE).eq(INVESTIGATION_NOTES_TOGGLE).click({ force: true }); cy.get(ABOUT_INVESTIGATION_NOTES).invoke('text').should('eql', INVESTIGATION_NOTES_MARKDOWN); - cy.get(DEFINITION_INDEX_PATTERNS).then((patterns) => { - cy.wrap(patterns).each((pattern, index) => { - cy.wrap(pattern).invoke('text').should('eql', expectedIndexPatterns[index]); - }); + cy.get(DEFINITION_DETAILS).within(() => { + getDescriptionForTitle('Index patterns') + .invoke('text') + .should('eql', expectedIndexPatterns.join('')); + getDescriptionForTitle('Custom query') + .invoke('text') + .should('eql', `${newRule.customQuery} `); + getDescriptionForTitle('Rule type').invoke('text').should('eql', 'Query'); + getDescriptionForTitle('Timeline template').invoke('text').should('eql', 'None'); + }); + + cy.get(SCHEDULE_DETAILS).within(() => { + getDescriptionForTitle('Runs every').invoke('text').should('eql', '5m'); + getDescriptionForTitle('Additional look-back time').invoke('text').should('eql', '1m'); }); - cy.get(DEFINITION_STEP) - .eq(DEFINITION_CUSTOM_QUERY) - .invoke('text') - .should('eql', `${newRule.customQuery} `); - cy.get(DEFINITION_STEP).eq(DEFINITION_TIMELINE).invoke('text').should('eql', 'None'); - - cy.get(SCHEDULE_STEP).eq(SCHEDULE_RUNS).invoke('text').should('eql', '5m'); - cy.get(SCHEDULE_STEP).eq(SCHEDULE_LOOPBACK).invoke('text').should('eql', '1m'); }); }); @@ -233,4 +257,94 @@ describe('Deletes custom rules', () => { .should('eql', `Custom rules (${expectedNumberOfRulesAfterDeletion})`); }); }); + + it('Allows a rule to be edited', () => { + editFirstRule(); + + // expect define step to populate + cy.get(CUSTOM_QUERY_INPUT).invoke('text').should('eq', existingRule.customQuery); + if (existingRule.index && existingRule.index.length > 0) { + cy.get(DEFINE_INDEX_INPUT).invoke('text').should('eq', existingRule.index.join('')); + } + + goToAboutStepTab(); + + // expect about step to populate + cy.get(RULE_NAME_INPUT).invoke('val').should('eql', existingRule.name); + cy.get(RULE_DESCRIPTION_INPUT).invoke('text').should('eql', existingRule.description); + cy.get(TAGS_FIELD).invoke('text').should('eql', existingRule.tags.join('')); + + cy.get(SEVERITY_DROPDOWN).invoke('text').should('eql', existingRule.severity); + cy.get(RISK_INPUT).invoke('val').should('eql', existingRule.riskScore); + + goToScheduleStepTab(); + + // expect schedule step to populate + const intervalParts = existingRule.interval && existingRule.interval.match(/[0-9]+|[a-zA-Z]+/g); + if (intervalParts) { + const [amount, unit] = intervalParts; + cy.get(SCHEDULE_INTERVAL_AMOUNT_INPUT).invoke('val').should('eql', amount); + cy.get(SCHEDULE_INTERVAL_UNITS_INPUT).invoke('val').should('eql', unit); + } else { + throw new Error('Cannot assert scheduling info on a rule without an interval'); + } + + goToActionsStepTab(); + + cy.get(ACTIONS_THROTTLE_INPUT).invoke('val').should('eql', 'no_actions'); + + goToAboutStepTab(); + + const editedRule = { + ...existingRule, + severity: 'Medium', + description: 'Edited Rule description', + }; + + fillAboutRule(editedRule); + saveEditedRule(); + + const expectedTags = editedRule.tags.join(''); + const expectedIndexPatterns = + editedRule.index && editedRule.index.length + ? editedRule.index + : [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ]; + + cy.get(RULE_NAME_HEADER).invoke('text').should('eql', `${editedRule.name} Beta`); + + cy.get(ABOUT_RULE_DESCRIPTION).invoke('text').should('eql', editedRule.description); + cy.get(ABOUT_DETAILS).within(() => { + getDescriptionForTitle('Severity').invoke('text').should('eql', editedRule.severity); + getDescriptionForTitle('Risk score').invoke('text').should('eql', editedRule.riskScore); + getDescriptionForTitle('Tags').invoke('text').should('eql', expectedTags); + }); + + cy.get(RULE_ABOUT_DETAILS_HEADER_TOGGLE).eq(INVESTIGATION_NOTES_TOGGLE).click({ force: true }); + cy.get(ABOUT_INVESTIGATION_NOTES).invoke('text').should('eql', editedRule.note); + + cy.get(DEFINITION_DETAILS).within(() => { + getDescriptionForTitle('Index patterns') + .invoke('text') + .should('eql', expectedIndexPatterns.join('')); + getDescriptionForTitle('Custom query') + .invoke('text') + .should('eql', `${editedRule.customQuery} `); + getDescriptionForTitle('Rule type').invoke('text').should('eql', 'Query'); + getDescriptionForTitle('Timeline template').invoke('text').should('eql', 'None'); + }); + + if (editedRule.interval) { + cy.get(SCHEDULE_DETAILS).within(() => { + getDescriptionForTitle('Runs every').invoke('text').should('eql', editedRule.interval); + }); + } + }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_eql.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_eql.spec.ts index c65cd8406099a..76871929fe050 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_eql.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_eql.spec.ts @@ -15,26 +15,16 @@ import { SEVERITY, } from '../screens/alerts_detection_rules'; import { - ABOUT_FALSE_POSITIVES, + ABOUT_DETAILS, ABOUT_INVESTIGATION_NOTES, - ABOUT_MITRE, - ABOUT_RISK, ABOUT_RULE_DESCRIPTION, - ABOUT_SEVERITY, - ABOUT_STEP, - ABOUT_TAGS, - ABOUT_URLS, - DEFINITION_CUSTOM_QUERY, - DEFINITION_INDEX_PATTERNS, - DEFINITION_TIMELINE, - DEFINITION_STEP, + DEFINITION_DETAILS, + getDescriptionForTitle, INVESTIGATION_NOTES_MARKDOWN, INVESTIGATION_NOTES_TOGGLE, RULE_ABOUT_DETAILS_HEADER_TOGGLE, RULE_NAME_HEADER, - SCHEDULE_LOOPBACK, - SCHEDULE_RUNS, - SCHEDULE_STEP, + SCHEDULE_DETAILS, } from '../screens/rule_details'; import { @@ -136,32 +126,34 @@ describe('Detection rules, EQL', () => { cy.get(RULE_NAME_HEADER).invoke('text').should('eql', `${eqlRule.name} Beta`); cy.get(ABOUT_RULE_DESCRIPTION).invoke('text').should('eql', eqlRule.description); - cy.get(ABOUT_STEP).eq(ABOUT_SEVERITY).invoke('text').should('eql', eqlRule.severity); - cy.get(ABOUT_STEP).eq(ABOUT_RISK).invoke('text').should('eql', eqlRule.riskScore); - cy.get(ABOUT_STEP).eq(ABOUT_URLS).invoke('text').should('eql', expectedUrls); - cy.get(ABOUT_STEP) - .eq(ABOUT_FALSE_POSITIVES) - .invoke('text') - .should('eql', expectedFalsePositives); - cy.get(ABOUT_STEP).eq(ABOUT_MITRE).invoke('text').should('eql', expectedMitre); - cy.get(ABOUT_STEP).eq(ABOUT_TAGS).invoke('text').should('eql', expectedTags); + cy.get(ABOUT_DETAILS).within(() => { + getDescriptionForTitle('Severity').invoke('text').should('eql', eqlRule.severity); + getDescriptionForTitle('Risk score').invoke('text').should('eql', eqlRule.riskScore); + getDescriptionForTitle('Reference URLs').invoke('text').should('eql', expectedUrls); + getDescriptionForTitle('False positive examples') + .invoke('text') + .should('eql', expectedFalsePositives); + getDescriptionForTitle('MITRE ATT&CK').invoke('text').should('eql', expectedMitre); + getDescriptionForTitle('Tags').invoke('text').should('eql', expectedTags); + }); cy.get(RULE_ABOUT_DETAILS_HEADER_TOGGLE).eq(INVESTIGATION_NOTES_TOGGLE).click({ force: true }); cy.get(ABOUT_INVESTIGATION_NOTES).invoke('text').should('eql', INVESTIGATION_NOTES_MARKDOWN); - cy.get(DEFINITION_INDEX_PATTERNS).then((patterns) => { - cy.wrap(patterns).each((pattern, index) => { - cy.wrap(pattern).invoke('text').should('eql', expectedIndexPatterns[index]); - }); + cy.get(DEFINITION_DETAILS).within(() => { + getDescriptionForTitle('Index patterns') + .invoke('text') + .should('eql', expectedIndexPatterns.join('')); + getDescriptionForTitle('Custom query') + .invoke('text') + .should('eql', `${eqlRule.customQuery} `); + getDescriptionForTitle('Rule type').invoke('text').should('eql', 'Event Correlation'); + getDescriptionForTitle('Timeline template').invoke('text').should('eql', 'None'); + }); + + cy.get(SCHEDULE_DETAILS).within(() => { + getDescriptionForTitle('Runs every').invoke('text').should('eql', '5m'); + getDescriptionForTitle('Additional look-back time').invoke('text').should('eql', '1m'); }); - cy.get(DEFINITION_STEP) - .eq(DEFINITION_CUSTOM_QUERY) - .invoke('text') - .should('eql', `${eqlRule.customQuery} `); - cy.get(DEFINITION_STEP).eq(2).invoke('text').should('eql', 'Event Correlation'); - cy.get(DEFINITION_STEP).eq(DEFINITION_TIMELINE).invoke('text').should('eql', 'None'); - - cy.get(SCHEDULE_STEP).eq(SCHEDULE_RUNS).invoke('text').should('eql', '5m'); - cy.get(SCHEDULE_STEP).eq(SCHEDULE_LOOPBACK).invoke('text').should('eql', '1m'); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_ml.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_ml.spec.ts index b6b30ef550eb1..47e49d48e2aec 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_ml.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_ml.spec.ts @@ -16,24 +16,14 @@ import { SEVERITY, } from '../screens/alerts_detection_rules'; import { - ABOUT_FALSE_POSITIVES, - ABOUT_MITRE, - ABOUT_RISK, ABOUT_RULE_DESCRIPTION, - ABOUT_SEVERITY, - ABOUT_STEP, - ABOUT_TAGS, - ABOUT_URLS, - ANOMALY_SCORE, - DEFINITION_TIMELINE, - DEFINITION_STEP, MACHINE_LEARNING_JOB_ID, MACHINE_LEARNING_JOB_STATUS, RULE_NAME_HEADER, - SCHEDULE_LOOPBACK, - SCHEDULE_RUNS, - SCHEDULE_STEP, - RULE_TYPE, + getDescriptionForTitle, + ABOUT_DETAILS, + DEFINITION_DETAILS, + SCHEDULE_DETAILS, } from '../screens/rule_details'; import { @@ -126,36 +116,37 @@ describe('Detection rules, machine learning', () => { cy.get(RULE_NAME_HEADER).invoke('text').should('eql', `${machineLearningRule.name} Beta`); cy.get(ABOUT_RULE_DESCRIPTION).invoke('text').should('eql', machineLearningRule.description); - cy.get(ABOUT_STEP) - .eq(ABOUT_SEVERITY) - .invoke('text') - .should('eql', machineLearningRule.severity); - cy.get(ABOUT_STEP).eq(ABOUT_RISK).invoke('text').should('eql', machineLearningRule.riskScore); - cy.get(ABOUT_STEP).eq(ABOUT_URLS).invoke('text').should('eql', expectedUrls); - cy.get(ABOUT_STEP) - .eq(ABOUT_FALSE_POSITIVES) - .invoke('text') - .should('eql', expectedFalsePositives); - cy.get(ABOUT_STEP).eq(ABOUT_MITRE).invoke('text').should('eql', expectedMitre); - cy.get(ABOUT_STEP).eq(ABOUT_TAGS).invoke('text').should('eql', expectedTags); - - cy.get(DEFINITION_STEP).eq(RULE_TYPE).invoke('text').should('eql', 'Machine Learning'); - cy.get(DEFINITION_STEP) - .eq(ANOMALY_SCORE) - .invoke('text') - .should('eql', machineLearningRule.anomalyScoreThreshold); - cy.get(DEFINITION_STEP) - .get(MACHINE_LEARNING_JOB_STATUS) - .invoke('text') - .should('eql', 'Stopped'); - cy.get(DEFINITION_STEP) - .get(MACHINE_LEARNING_JOB_ID) - .invoke('text') - .should('eql', machineLearningRule.machineLearningJob); - - cy.get(DEFINITION_STEP).eq(DEFINITION_TIMELINE).invoke('text').should('eql', 'None'); - - cy.get(SCHEDULE_STEP).eq(SCHEDULE_RUNS).invoke('text').should('eql', '5m'); - cy.get(SCHEDULE_STEP).eq(SCHEDULE_LOOPBACK).invoke('text').should('eql', '1m'); + cy.get(ABOUT_DETAILS).within(() => { + getDescriptionForTitle('Severity').invoke('text').should('eql', machineLearningRule.severity); + getDescriptionForTitle('Risk score') + .invoke('text') + .should('eql', machineLearningRule.riskScore); + getDescriptionForTitle('Reference URLs').invoke('text').should('eql', expectedUrls); + getDescriptionForTitle('False positive examples') + .invoke('text') + .should('eql', expectedFalsePositives); + getDescriptionForTitle('MITRE ATT&CK').invoke('text').should('eql', expectedMitre); + getDescriptionForTitle('Tags').invoke('text').should('eql', expectedTags); + }); + + cy.get(DEFINITION_DETAILS).within(() => { + getDescriptionForTitle('Anomaly score') + .invoke('text') + .should('eql', machineLearningRule.anomalyScoreThreshold); + getDescriptionForTitle('Anomaly score') + .invoke('text') + .should('eql', machineLearningRule.anomalyScoreThreshold); + getDescriptionForTitle('Rule type').invoke('text').should('eql', 'Machine Learning'); + getDescriptionForTitle('Timeline template').invoke('text').should('eql', 'None'); + cy.get(MACHINE_LEARNING_JOB_STATUS).invoke('text').should('eql', 'Stopped'); + cy.get(MACHINE_LEARNING_JOB_ID) + .invoke('text') + .should('eql', machineLearningRule.machineLearningJob); + }); + + cy.get(SCHEDULE_DETAILS).within(() => { + getDescriptionForTitle('Runs every').invoke('text').should('eql', '5m'); + getDescriptionForTitle('Additional look-back time').invoke('text').should('eql', '1m'); + }); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_override.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_override.spec.ts index e3526c63e2310..4edf5e1866087 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_override.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_override.spec.ts @@ -16,29 +16,17 @@ import { } from '../screens/alerts_detection_rules'; import { ABOUT_INVESTIGATION_NOTES, - ABOUT_OVERRIDE_FALSE_POSITIVES, - ABOUT_OVERRIDE_MITRE, - ABOUT_OVERRIDE_NAME_OVERRIDE, - ABOUT_OVERRIDE_RISK, - ABOUT_OVERRIDE_RISK_OVERRIDE, - ABOUT_OVERRIDE_SEVERITY_OVERRIDE, - ABOUT_OVERRIDE_TAGS, - ABOUT_OVERRIDE_TIMESTAMP_OVERRIDE, - ABOUT_OVERRIDE_URLS, ABOUT_RULE_DESCRIPTION, - ABOUT_SEVERITY, - ABOUT_STEP, - DEFINITION_CUSTOM_QUERY, - DEFINITION_INDEX_PATTERNS, - DEFINITION_TIMELINE, - DEFINITION_STEP, INVESTIGATION_NOTES_MARKDOWN, INVESTIGATION_NOTES_TOGGLE, RULE_ABOUT_DETAILS_HEADER_TOGGLE, RULE_NAME_HEADER, - SCHEDULE_LOOPBACK, - SCHEDULE_RUNS, - SCHEDULE_STEP, + ABOUT_DETAILS, + getDescriptionForTitle, + DEFINITION_DETAILS, + SCHEDULE_DETAILS, + DETAILS_TITLE, + DETAILS_DESCRIPTION, } from '../screens/rule_details'; import { @@ -141,56 +129,56 @@ describe('Detection rules, override', () => { const expectedOverrideSeverities = ['Low', 'Medium', 'High', 'Critical']; - cy.get(ABOUT_STEP).eq(ABOUT_SEVERITY).invoke('text').should('eql', newOverrideRule.severity); - newOverrideRule.severityOverride.forEach((severity, i) => { - cy.get(ABOUT_STEP) - .eq(ABOUT_OVERRIDE_SEVERITY_OVERRIDE + i) + cy.get(ABOUT_DETAILS).within(() => { + getDescriptionForTitle('Severity').invoke('text').should('eql', newOverrideRule.severity); + getDescriptionForTitle('Risk score').invoke('text').should('eql', newOverrideRule.riskScore); + getDescriptionForTitle('Risk score override') .invoke('text') - .should( - 'eql', - `${severity.sourceField}:${severity.sourceValue}${expectedOverrideSeverities[i]}` - ); + .should('eql', `${newOverrideRule.riskOverride}signal.rule.risk_score`); + getDescriptionForTitle('Rule name override') + .invoke('text') + .should('eql', newOverrideRule.nameOverride); + getDescriptionForTitle('Reference URLs').invoke('text').should('eql', expectedUrls); + getDescriptionForTitle('False positive examples') + .invoke('text') + .should('eql', expectedFalsePositives); + getDescriptionForTitle('MITRE ATT&CK').invoke('text').should('eql', expectedMitre); + getDescriptionForTitle('Tags').invoke('text').should('eql', expectedTags); + getDescriptionForTitle('Timestamp override') + .invoke('text') + .should('eql', newOverrideRule.timestampOverride); + cy.contains(DETAILS_TITLE, 'Severity override') + .invoke('index', DETAILS_TITLE) // get index relative to other titles, not all siblings + .then((severityOverrideIndex) => { + newOverrideRule.severityOverride.forEach((severity, i) => { + cy.get(DETAILS_DESCRIPTION) + .eq(severityOverrideIndex + i) + .invoke('text') + .should( + 'eql', + `${severity.sourceField}:${severity.sourceValue}${expectedOverrideSeverities[i]}` + ); + }); + }); }); - cy.get(ABOUT_STEP) - .eq(ABOUT_OVERRIDE_RISK) - .invoke('text') - .should('eql', newOverrideRule.riskScore); - cy.get(ABOUT_STEP) - .eq(ABOUT_OVERRIDE_RISK_OVERRIDE) - .invoke('text') - .should('eql', `${newOverrideRule.riskOverride}signal.rule.risk_score`); - cy.get(ABOUT_STEP).eq(ABOUT_OVERRIDE_URLS).invoke('text').should('eql', expectedUrls); - cy.get(ABOUT_STEP) - .eq(ABOUT_OVERRIDE_FALSE_POSITIVES) - .invoke('text') - .should('eql', expectedFalsePositives); - cy.get(ABOUT_STEP) - .eq(ABOUT_OVERRIDE_NAME_OVERRIDE) - .invoke('text') - .should('eql', newOverrideRule.nameOverride); - cy.get(ABOUT_STEP).eq(ABOUT_OVERRIDE_MITRE).invoke('text').should('eql', expectedMitre); - cy.get(ABOUT_STEP) - .eq(ABOUT_OVERRIDE_TIMESTAMP_OVERRIDE) - .invoke('text') - .should('eql', newOverrideRule.timestampOverride); - cy.get(ABOUT_STEP).eq(ABOUT_OVERRIDE_TAGS).invoke('text').should('eql', expectedTags); - cy.get(RULE_ABOUT_DETAILS_HEADER_TOGGLE).eq(INVESTIGATION_NOTES_TOGGLE).click({ force: true }); cy.get(ABOUT_INVESTIGATION_NOTES).invoke('text').should('eql', INVESTIGATION_NOTES_MARKDOWN); - cy.get(DEFINITION_INDEX_PATTERNS).then((patterns) => { - cy.wrap(patterns).each((pattern, index) => { - cy.wrap(pattern).invoke('text').should('eql', expectedIndexPatterns[index]); - }); + cy.get(DEFINITION_DETAILS).within(() => { + getDescriptionForTitle('Index patterns') + .invoke('text') + .should('eql', expectedIndexPatterns.join('')); + getDescriptionForTitle('Custom query') + .invoke('text') + .should('eql', `${newOverrideRule.customQuery} `); + getDescriptionForTitle('Rule type').invoke('text').should('eql', 'Query'); + getDescriptionForTitle('Timeline template').invoke('text').should('eql', 'None'); + }); + + cy.get(SCHEDULE_DETAILS).within(() => { + getDescriptionForTitle('Runs every').invoke('text').should('eql', '5m'); + getDescriptionForTitle('Additional look-back time').invoke('text').should('eql', '1m'); }); - cy.get(DEFINITION_STEP) - .eq(DEFINITION_CUSTOM_QUERY) - .invoke('text') - .should('eql', `${newOverrideRule.customQuery} `); - cy.get(DEFINITION_STEP).eq(DEFINITION_TIMELINE).invoke('text').should('eql', 'None'); - - cy.get(SCHEDULE_STEP).eq(SCHEDULE_RUNS).invoke('text').should('eql', '5m'); - cy.get(SCHEDULE_STEP).eq(SCHEDULE_LOOPBACK).invoke('text').should('eql', '1m'); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts index 10f9ebb5623df..00175ed3baeb8 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_threshold.spec.ts @@ -15,27 +15,16 @@ import { SEVERITY, } from '../screens/alerts_detection_rules'; import { - ABOUT_FALSE_POSITIVES, ABOUT_INVESTIGATION_NOTES, - ABOUT_MITRE, - ABOUT_RISK, ABOUT_RULE_DESCRIPTION, - ABOUT_SEVERITY, - ABOUT_STEP, - ABOUT_TAGS, - ABOUT_URLS, - DEFINITION_CUSTOM_QUERY, - DEFINITION_INDEX_PATTERNS, - DEFINITION_THRESHOLD, - DEFINITION_TIMELINE, - DEFINITION_STEP, INVESTIGATION_NOTES_MARKDOWN, INVESTIGATION_NOTES_TOGGLE, RULE_ABOUT_DETAILS_HEADER_TOGGLE, RULE_NAME_HEADER, - SCHEDULE_LOOPBACK, - SCHEDULE_RUNS, - SCHEDULE_STEP, + getDescriptionForTitle, + ABOUT_DETAILS, + DEFINITION_DETAILS, + SCHEDULE_DETAILS, } from '../screens/rule_details'; import { @@ -137,38 +126,40 @@ describe('Detection rules, threshold', () => { cy.get(RULE_NAME_HEADER).invoke('text').should('eql', `${newThresholdRule.name} Beta`); cy.get(ABOUT_RULE_DESCRIPTION).invoke('text').should('eql', newThresholdRule.description); - cy.get(ABOUT_STEP).eq(ABOUT_SEVERITY).invoke('text').should('eql', newThresholdRule.severity); - cy.get(ABOUT_STEP).eq(ABOUT_RISK).invoke('text').should('eql', newThresholdRule.riskScore); - cy.get(ABOUT_STEP).eq(ABOUT_URLS).invoke('text').should('eql', expectedUrls); - cy.get(ABOUT_STEP) - .eq(ABOUT_FALSE_POSITIVES) - .invoke('text') - .should('eql', expectedFalsePositives); - cy.get(ABOUT_STEP).eq(ABOUT_MITRE).invoke('text').should('eql', expectedMitre); - cy.get(ABOUT_STEP).eq(ABOUT_TAGS).invoke('text').should('eql', expectedTags); + cy.get(ABOUT_DETAILS).within(() => { + getDescriptionForTitle('Severity').invoke('text').should('eql', newThresholdRule.severity); + getDescriptionForTitle('Risk score').invoke('text').should('eql', newThresholdRule.riskScore); + getDescriptionForTitle('Reference URLs').invoke('text').should('eql', expectedUrls); + getDescriptionForTitle('False positive examples') + .invoke('text') + .should('eql', expectedFalsePositives); + getDescriptionForTitle('MITRE ATT&CK').invoke('text').should('eql', expectedMitre); + getDescriptionForTitle('Tags').invoke('text').should('eql', expectedTags); + }); cy.get(RULE_ABOUT_DETAILS_HEADER_TOGGLE).eq(INVESTIGATION_NOTES_TOGGLE).click({ force: true }); cy.get(ABOUT_INVESTIGATION_NOTES).invoke('text').should('eql', INVESTIGATION_NOTES_MARKDOWN); - cy.get(DEFINITION_INDEX_PATTERNS).then((patterns) => { - cy.wrap(patterns).each((pattern, index) => { - cy.wrap(pattern).invoke('text').should('eql', expectedIndexPatterns[index]); - }); + cy.get(DEFINITION_DETAILS).within(() => { + getDescriptionForTitle('Index patterns') + .invoke('text') + .should('eql', expectedIndexPatterns.join('')); + getDescriptionForTitle('Custom query') + .invoke('text') + .should('eql', `${newThresholdRule.customQuery} `); + getDescriptionForTitle('Rule type').invoke('text').should('eql', 'Threshold'); + getDescriptionForTitle('Timeline template').invoke('text').should('eql', 'None'); + getDescriptionForTitle('Threshold') + .invoke('text') + .should( + 'eql', + `Results aggregated by ${newThresholdRule.thresholdField} >= ${newThresholdRule.threshold}` + ); + }); + + cy.get(SCHEDULE_DETAILS).within(() => { + getDescriptionForTitle('Runs every').invoke('text').should('eql', '5m'); + getDescriptionForTitle('Additional look-back time').invoke('text').should('eql', '1m'); }); - cy.get(DEFINITION_STEP) - .eq(DEFINITION_CUSTOM_QUERY) - .invoke('text') - .should('eql', `${newThresholdRule.customQuery} `); - cy.get(DEFINITION_STEP).eq(DEFINITION_TIMELINE).invoke('text').should('eql', 'None'); - cy.get(DEFINITION_STEP) - .eq(DEFINITION_THRESHOLD) - .invoke('text') - .should( - 'eql', - `Results aggregated by ${newThresholdRule.thresholdField} >= ${newThresholdRule.threshold}` - ); - - cy.get(SCHEDULE_STEP).eq(SCHEDULE_RUNS).invoke('text').should('eql', '5m'); - cy.get(SCHEDULE_STEP).eq(SCHEDULE_LOOPBACK).invoke('text').should('eql', '1m'); }); }); diff --git a/x-pack/plugins/security_solution/cypress/objects/rule.ts b/x-pack/plugins/security_solution/cypress/objects/rule.ts index 0624606fe8481..2a5c60815f450 100644 --- a/x-pack/plugins/security_solution/cypress/objects/rule.ts +++ b/x-pack/plugins/security_solution/cypress/objects/rule.ts @@ -27,6 +27,8 @@ export interface CustomRule { customQuery: string; name: string; description: string; + index?: string[]; + interval?: string; severity: string; riskScore: string; tags: string[]; @@ -109,6 +111,29 @@ export const newRule: CustomRule = { timelineId: '0162c130-78be-11ea-9718-118a926974a4', }; +export const existingRule: CustomRule = { + customQuery: 'host.name:*', + name: 'Rule 1', + description: 'Description for Rule 1', + index: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + interval: '4m', + severity: 'High', + riskScore: '19', + tags: ['rule1'], + referenceUrls: [], + falsePositivesExamples: [], + mitre: [], + note: 'This is my note', + timelineId: '', +}; + export const newOverrideRule: OverrideRule = { customQuery: 'host.name:*', name: 'New Rule Test', diff --git a/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts b/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts index a41b8296f83e4..14f5383939a94 100644 --- a/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts +++ b/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts @@ -14,6 +14,8 @@ export const CUSTOM_RULES_BTN = '[data-test-subj="show-custom-rules-filter-butto export const DELETE_RULE_ACTION_BTN = '[data-test-subj="deleteRuleAction"]'; +export const EDIT_RULE_ACTION_BTN = '[data-test-subj="editRuleAction"]'; + export const DELETE_RULE_BULK_BTN = '[data-test-subj="deleteRuleBulk"]'; export const ELASTIC_RULES_BTN = '[data-test-subj="show-elastic-rules-filter-button"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts index 1c25ed88c3bee..dda371126d5aa 100644 --- a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts @@ -8,6 +8,13 @@ export const ABOUT_CONTINUE_BTN = '[data-test-subj="about-continue"]'; export const ABOUT_EDIT_BUTTON = '[data-test-subj="edit-about-rule"]'; +export const ABOUT_EDIT_TAB = '[data-test-subj="edit-rule-about-tab"]'; + +export const ACTIONS_EDIT_TAB = '[data-test-subj="edit-rule-actions-tab"]'; + +export const ACTIONS_THROTTLE_INPUT = + '[data-test-subj="stepRuleActions"] [data-test-subj="select"]'; + export const ADD_FALSE_POSITIVE_BTN = '[data-test-subj="detectionEngineStepAboutRuleFalsePositives"] .euiButtonEmpty__text'; @@ -30,6 +37,11 @@ export const DEFINE_CONTINUE_BUTTON = '[data-test-subj="define-continue"]'; export const DEFINE_EDIT_BUTTON = '[data-test-subj="edit-define-rule"]'; +export const DEFINE_EDIT_TAB = '[data-test-subj="edit-rule-define-tab"]'; + +export const DEFINE_INDEX_INPUT = + '[data-test-subj="detectionEngineStepDefineRuleIndices"] [data-test-subj="input"]'; + export const EQL_TYPE = '[data-test-subj="eqlRuleType"]'; export const EQL_QUERY_INPUT = '[data-test-subj="eqlQueryBarTextInput"]'; @@ -81,6 +93,20 @@ export const RULE_TIMESTAMP_OVERRIDE = export const SCHEDULE_CONTINUE_BUTTON = '[data-test-subj="schedule-continue"]'; +export const SCHEDULE_EDIT_TAB = '[data-test-subj="edit-rule-schedule-tab"]'; + +export const SCHEDULE_INTERVAL_AMOUNT_INPUT = + '[data-test-subj="detectionEngineStepScheduleRuleInterval"] [data-test-subj="schedule-amount-input"]'; + +export const SCHEDULE_INTERVAL_UNITS_INPUT = + '[data-test-subj="detectionEngineStepScheduleRuleInterval"] [data-test-subj="schedule-units-input"]'; + +export const SCHEDULE_LOOKBACK_AMOUNT_INPUT = + '[data-test-subj="detectionEngineStepScheduleRuleFrom"] [data-test-subj="schedule-amount-input"]'; + +export const SCHEDULE_LOOKBACK_UNITS_INPUT = + '[data-test-subj="detectionEngineStepScheduleRuleFrom"] [data-test-subj="schedule-units-input"]'; + export const SEVERITY_DROPDOWN = '[data-test-subj="detectionEngineStepAboutRuleSeverity"] [data-test-subj="select"]'; @@ -88,6 +114,9 @@ export const SEVERITY_MAPPING_OVERRIDE_OPTION = '#severity-mapping-override'; export const SEVERITY_OVERRIDE_ROW = '[data-test-subj="severityOverrideRow"]'; +export const TAGS_FIELD = + '[data-test-subj="detectionEngineStepAboutRuleTags"] [data-test-subj="comboBoxInput"]'; + export const TAGS_INPUT = '[data-test-subj="detectionEngineStepAboutRuleTags"] [data-test-subj="comboBoxSearchInput"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/edit_rule.ts b/x-pack/plugins/security_solution/cypress/screens/edit_rule.ts new file mode 100644 index 0000000000000..1bf0ff34ebd94 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/screens/edit_rule.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 const EDIT_SUBMIT_BUTTON = '[data-test-subj="ruleEditSubmitButton"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts index b221709966943..98fc7b06a9908 100644 --- a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts +++ b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts @@ -4,55 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -export const ABOUT_FALSE_POSITIVES = 3; +export const getDescriptionForTitle = (title: string) => + cy.get(DETAILS_TITLE).contains(title).next(DETAILS_DESCRIPTION); -export const ABOUT_INVESTIGATION_NOTES = '[data-test-subj="stepAboutDetailsNoteContent"]'; - -export const ABOUT_MITRE = 4; - -export const ABOUT_OVERRIDE_FALSE_POSITIVES = 8; - -export const ABOUT_OVERRIDE_MITRE = 10; - -export const ABOUT_OVERRIDE_NAME_OVERRIDE = 9; - -export const ABOUT_OVERRIDE_RISK = 5; +export const DETAILS_DESCRIPTION = '.euiDescriptionList__description'; +export const DETAILS_TITLE = '.euiDescriptionList__title'; -export const ABOUT_OVERRIDE_RISK_OVERRIDE = 6; - -export const ABOUT_OVERRIDE_SEVERITY_OVERRIDE = 1; - -export const ABOUT_OVERRIDE_TAGS = 12; - -export const ABOUT_OVERRIDE_TIMESTAMP_OVERRIDE = 11; - -export const ABOUT_OVERRIDE_URLS = 7; +export const ABOUT_INVESTIGATION_NOTES = '[data-test-subj="stepAboutDetailsNoteContent"]'; export const ABOUT_RULE_DESCRIPTION = '[data-test-subj=stepAboutRuleDetailsToggleDescriptionText]'; -export const ABOUT_RISK = 1; +export const ABOUT_DETAILS = + '[data-test-subj="aboutRule"] [data-test-subj="listItemColumnStepRuleDescription"]'; -export const ABOUT_SEVERITY = 0; - -export const ABOUT_STEP = '[data-test-subj="aboutRule"] .euiDescriptionList__description'; - -export const ABOUT_TAGS = 5; - -export const ABOUT_URLS = 2; - -export const ANOMALY_SCORE = 1; - -export const DEFINITION_CUSTOM_QUERY = 1; - -export const DEFINITION_THRESHOLD = 4; - -export const DEFINITION_TIMELINE = 3; - -export const DEFINITION_INDEX_PATTERNS = - '[data-test-subj=definitionRule] [data-test-subj="listItemColumnStepRuleDescription"] .euiDescriptionList__description .euiBadge__text'; - -export const DEFINITION_STEP = - '[data-test-subj=definitionRule] [data-test-subj="listItemColumnStepRuleDescription"] .euiDescriptionList__description'; +export const DEFINITION_DETAILS = + '[data-test-subj=definitionRule] [data-test-subj="listItemColumnStepRuleDescription"]'; export const INVESTIGATION_NOTES_MARKDOWN = 'test markdown'; @@ -60,16 +26,13 @@ export const INVESTIGATION_NOTES_TOGGLE = 1; export const MACHINE_LEARNING_JOB_ID = '[data-test-subj="machineLearningJobId"]'; -export const MACHINE_LEARNING_JOB_STATUS = '[data-test-subj="machineLearningJobStatus" ]'; +export const MACHINE_LEARNING_JOB_STATUS = '[data-test-subj="machineLearningJobStatus"]'; export const RULE_ABOUT_DETAILS_HEADER_TOGGLE = '[data-test-subj="stepAboutDetailsToggle"]'; export const RULE_NAME_HEADER = '[data-test-subj="header-page-title"]'; -export const RULE_TYPE = 0; +export const SCHEDULE_DETAILS = + '[data-test-subj=schedule] [data-test-subj="listItemColumnStepRuleDescription"]'; export const SCHEDULE_STEP = '[data-test-subj="schedule"] .euiDescriptionList__description'; - -export const SCHEDULE_RUNS = 0; - -export const SCHEDULE_LOOPBACK = 1; diff --git a/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts b/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts index 5ec5bb97250db..c530594508f95 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts @@ -24,6 +24,7 @@ import { SORT_RULES_BTN, THREE_HUNDRED_ROWS, EXPORT_ACTION_BTN, + EDIT_RULE_ACTION_BTN, } from '../screens/alerts_detection_rules'; export const activateRule = (rulePosition: number) => { @@ -35,6 +36,11 @@ export const changeToThreeHundredRowsPerPage = () => { cy.get(THREE_HUNDRED_ROWS).click(); }; +export const editFirstRule = () => { + cy.get(COLLAPSED_ACTION_BTN).first().click({ force: true }); + cy.get(EDIT_RULE_ACTION_BTN).click(); +}; + export const deleteFirstRule = () => { cy.get(COLLAPSED_ACTION_BTN).first().click({ force: true }); cy.get(DELETE_RULE_ACTION_BTN).click(); diff --git a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts index f26a77171462c..0daff52de7063 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts @@ -13,14 +13,17 @@ import { } from '../objects/rule'; import { ABOUT_CONTINUE_BTN, - ANOMALY_THRESHOLD_INPUT, + ABOUT_EDIT_TAB, + ACTIONS_EDIT_TAB, ADD_FALSE_POSITIVE_BTN, ADD_REFERENCE_URL_BTN, ADVANCED_SETTINGS_BTN, + ANOMALY_THRESHOLD_INPUT, COMBO_BOX_INPUT, CREATE_AND_ACTIVATE_BTN, CUSTOM_QUERY_INPUT, DEFINE_CONTINUE_BUTTON, + DEFINE_EDIT_TAB, FALSE_POSITIVES_INPUT, IMPORT_QUERY_FROM_SAVED_TIMELINE_LINK, INPUT, @@ -32,8 +35,8 @@ import { MITRE_TACTIC, MITRE_TACTIC_DROPDOWN, MITRE_TECHNIQUES_INPUT, - RISK_INPUT, REFERENCE_URLS_INPUT, + RISK_INPUT, RISK_MAPPING_OVERRIDE_OPTION, RISK_OVERRIDE, RULE_DESCRIPTION_INPUT, @@ -41,6 +44,7 @@ import { RULE_NAME_OVERRIDE, RULE_TIMESTAMP_OVERRIDE, SCHEDULE_CONTINUE_BUTTON, + SCHEDULE_EDIT_TAB, SEVERITY_DROPDOWN, SEVERITY_MAPPING_OVERRIDE_OPTION, SEVERITY_OVERRIDE_ROW, @@ -48,8 +52,6 @@ import { THRESHOLD_FIELD_SELECTION, THRESHOLD_INPUT_AREA, THRESHOLD_TYPE, - DEFINE_EDIT_BUTTON, - ABOUT_EDIT_BUTTON, EQL_TYPE, EQL_QUERY_INPUT, } from '../screens/create_new_rule'; @@ -61,11 +63,9 @@ export const createAndActivateRule = () => { cy.get(CREATE_AND_ACTIVATE_BTN).should('not.exist'); }; -export const fillAboutRuleAndContinue = ( - rule: CustomRule | MachineLearningRule | ThresholdRule -) => { - cy.get(RULE_NAME_INPUT).type(rule.name, { force: true }); - cy.get(RULE_DESCRIPTION_INPUT).type(rule.description, { force: true }); +export const fillAboutRule = (rule: CustomRule | MachineLearningRule | ThresholdRule) => { + cy.get(RULE_NAME_INPUT).clear({ force: true }).type(rule.name, { force: true }); + cy.get(RULE_DESCRIPTION_INPUT).clear({ force: true }).type(rule.description, { force: true }); cy.get(SEVERITY_DROPDOWN).click({ force: true }); cy.get(`#${rule.severity.toLowerCase()}`).click(); @@ -79,12 +79,15 @@ export const fillAboutRuleAndContinue = ( cy.get(ADVANCED_SETTINGS_BTN).click({ force: true }); rule.referenceUrls.forEach((url, index) => { - cy.get(REFERENCE_URLS_INPUT).eq(index).type(url, { force: true }); + cy.get(REFERENCE_URLS_INPUT).eq(index).clear({ force: true }).type(url, { force: true }); cy.get(ADD_REFERENCE_URL_BTN).click({ force: true }); }); rule.falsePositivesExamples.forEach((falsePositive, index) => { - cy.get(FALSE_POSITIVES_INPUT).eq(index).type(falsePositive, { force: true }); + cy.get(FALSE_POSITIVES_INPUT) + .eq(index) + .clear({ force: true }) + .type(falsePositive, { force: true }); cy.get(ADD_FALSE_POSITIVE_BTN).click({ force: true }); }); @@ -93,14 +96,22 @@ export const fillAboutRuleAndContinue = ( cy.contains(MITRE_TACTIC, mitre.tactic).click(); mitre.techniques.forEach((technique) => { - cy.get(MITRE_TECHNIQUES_INPUT).eq(index).type(`${technique}{enter}`, { force: true }); + cy.get(MITRE_TECHNIQUES_INPUT) + .eq(index) + .clear({ force: true }) + .type(`${technique}{enter}`, { force: true }); }); cy.get(MITRE_BTN).click({ force: true }); }); - cy.get(INVESTIGATION_NOTES_TEXTAREA).type(rule.note, { force: true }); + cy.get(INVESTIGATION_NOTES_TEXTAREA).clear({ force: true }).type(rule.note, { force: true }); +}; +export const fillAboutRuleAndContinue = ( + rule: CustomRule | MachineLearningRule | ThresholdRule +) => { + fillAboutRule(rule); cy.get(ABOUT_CONTINUE_BTN).should('exist').click({ force: true }); }; @@ -179,20 +190,6 @@ export const fillDefineCustomRuleWithImportedQueryAndContinue = ( cy.get(CUSTOM_QUERY_INPUT).should('not.exist'); }; -export const expectDefineFormToRepopulateAndContinue = (rule: CustomRule) => { - cy.get(DEFINE_EDIT_BUTTON).click(); - cy.get(CUSTOM_QUERY_INPUT).invoke('text').should('eq', rule.customQuery); - cy.get(DEFINE_CONTINUE_BUTTON).should('exist').click({ force: true }); - cy.get(DEFINE_CONTINUE_BUTTON).should('not.exist'); -}; - -export const expectAboutFormToRepopulateAndContinue = (rule: CustomRule) => { - cy.get(ABOUT_EDIT_BUTTON).click(); - cy.get(RULE_NAME_INPUT).invoke('val').should('eq', rule.name); - cy.get(ABOUT_CONTINUE_BTN).should('exist').click({ force: true }); - cy.get(ABOUT_CONTINUE_BTN).should('not.exist'); -}; - export const fillDefineThresholdRuleAndContinue = (rule: ThresholdRule) => { const thresholdField = 0; const threshold = 1; @@ -230,6 +227,22 @@ export const fillDefineMachineLearningRuleAndContinue = (rule: MachineLearningRu cy.get(MACHINE_LEARNING_DROPDOWN).should('not.exist'); }; +export const goToDefineStepTab = () => { + cy.get(DEFINE_EDIT_TAB).click({ force: true }); +}; + +export const goToAboutStepTab = () => { + cy.get(ABOUT_EDIT_TAB).click({ force: true }); +}; + +export const goToScheduleStepTab = () => { + cy.get(SCHEDULE_EDIT_TAB).click({ force: true }); +}; + +export const goToActionsStepTab = () => { + cy.get(ACTIONS_EDIT_TAB).click({ force: true }); +}; + export const selectMachineLearningRuleType = () => { cy.get(MACHINE_LEARNING_TYPE).click({ force: true }); }; diff --git a/x-pack/plugins/security_solution/cypress/tasks/edit_rule.ts b/x-pack/plugins/security_solution/cypress/tasks/edit_rule.ts new file mode 100644 index 0000000000000..690a36058ec33 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/tasks/edit_rule.ts @@ -0,0 +1,12 @@ +/* + * 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 { EDIT_SUBMIT_BUTTON } from '../screens/edit_rule'; + +export const saveEditedRule = () => { + cy.get(EDIT_SUBMIT_BUTTON).should('exist').click({ force: true }); + cy.get(EDIT_SUBMIT_BUTTON).should('not.exist'); +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/schedule_item_form/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/schedule_item_form/index.tsx index bb33767f4f5d5..867be1c1270e8 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/schedule_item_form/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/schedule_item_form/index.tsx @@ -145,6 +145,7 @@ export const ScheduleItem = ({ void ) => [ { + 'data-test-subj': 'editRuleAction', description: i18n.EDIT_RULE_SETTINGS, icon: 'controlsHorizontal', name: i18n.EDIT_RULE_SETTINGS, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx index 5f4fd59669103..e2772af72da06 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/edit/index.tsx @@ -118,6 +118,7 @@ const EditRulePageComponent: FC = () => { const tabs = useMemo( () => [ { + 'data-test-subj': 'edit-rule-define-tab', id: RuleStep.defineRule, name: ruleI18n.DEFINITION, disabled: rule?.immutable, @@ -140,6 +141,7 @@ const EditRulePageComponent: FC = () => { ), }, { + 'data-test-subj': 'edit-rule-about-tab', id: RuleStep.aboutRule, name: ruleI18n.ABOUT, disabled: rule?.immutable, @@ -163,6 +165,7 @@ const EditRulePageComponent: FC = () => { ), }, { + 'data-test-subj': 'edit-rule-schedule-tab', id: RuleStep.scheduleRule, name: ruleI18n.SCHEDULE, disabled: rule?.immutable, @@ -185,6 +188,7 @@ const EditRulePageComponent: FC = () => { ), }, { + 'data-test-subj': 'edit-rule-actions-tab', id: RuleStep.ruleActions, name: ruleI18n.ACTIONS, content: ( @@ -387,6 +391,7 @@ const EditRulePageComponent: FC = () => { QOZ*BnXTWNFZHW&VWe+6$owN33eK|XhY zJ^kRgT(z@ zOy+S{c4TSDMK|ECCh#I(e~^${kDe-si}cX)+<+GTeqZ>zjfcT=l=uKq1KkJXD^X9T+%_`U3`4KLSkNfhWX7L5#x^fLIbh$i( zSLf1ULk&(w^=ncOFN~qvb>XlvTU=aUU0MMcct+b*Oli?1z9F>dliB4_wdMB?-RkAV zkvH>-Q*c$COR(ma#p;FW*rUP(Ra|mp4~r7qy|oSNrkR+QB19UTgbM}!dF31YA9J=&aUE4}YktjWS zg15u`!h}9@gB4GH$qX8|h_DrE6eeb?in!RMO5CoXiy^zajFCPfj|?}nT-!6(b&o2+ zMy?qlGl6at5r2`@f`jIrHF@(nz;W415~2-QQ)&gy=8VFj?}cHaQL6NtJ5Q2o^jBtl z*9()j-Juoa=*L;)lA=r_FAB#|cz*BHViGZHhr?iEpvVrhJtoOAY1^R}glufL`K<*yOo?xWtJz!TFx!#pm!kdTltarQw&jxD!{60*trbe!4ZJmBHUzJ? zs>!wi(fI}|ii7*&6v_vIJ|sA@$)>KpRuA{IV53X0z{`waG|Ml(LpZfayY8tGH@50KcN`8$ZoP_J z((8X|W??Hxf{jz^k|LixA2^iO3fY?QomN*fd#m9ovEhAy#i<-816AckC__$F!5gjX zc|KAy3Q`_Otg6B{@Xh>&kQz7vsq2Nv-c=cpFtl8AD`XFb-i(6Qvw{fO z)-rKHi8k&+!Iucnf_frk&bm#Yi;s0~zGy<@5v>?~$u#@M z?4EkQUvEmq_eB|+>uaOhc7o4f0s#wn^$nOHsz*ytEb}G`C>A%V!x*yjM3I3ZA+Se$ z__9RF({eYw;CQ@>R)-YK^qxtn9{a|I+o30UlF**zpePFIKQX;`Mvg}OhEb%#N68Uw zgLw4Z?u?&T^=rb$XIA_Y$c4Ai@HxmazzXJ!LWi1-uw7;ii;qn z|K0;}G0#I*k>r0H@r`-B3i0Gp!w>)zB|0`hoWwwEsEnc;kUnC-Q~l{04?pXr#%;A! zwSv;{qSLdWqXc24doj7bJtMb+u2#0qnRzALO7qEK&zPfAL%AHDbmp>;>)hFyI5fOP z!|F{21FteO&YLwuRB^R2)l6GFop^&YFE|bh;^jiEAIZg|u;rKOuwkj$yjmFEtc^jT zcYRssJCbOSuDdh~ZS8n)>OdeA%csK05ig0u(!`rcUMaX-I6^II8s%lZQ}Ao;?s5zi zl}^l?G|hffcG~TK>QaM#c$Jk^{&QYx@Grj9*y@)1=u3=uy2eOdQamd8JGeR6%3r_X zvVhujh%9RJtWOU&)DP$KxnFW`zBdrFig~yEdT_yj00D4B zM_N|;p7#d!R`&h}y*2>caPNBqPSgwp#UzG6ue>+daY^uU5WoLR0{Rg+R^X*K1ON#y zF$qaRbcVPlH7_RCDnp%i7zKGw76Ih&K7hSvF67yXZNwF^+Ey0!f@mh`szvswm~4Gr zO0QD1%J@INRhdaPb;AZ?)K8IRM?Oh5+{deHBnVc{d`FuL&l?qxJ}RLdZZ&DqiFB&6 z;E`Qj5Lf|%%&wm5^dR~mh)W12>6%~1j z`z`L=Uran}6nhgV7WJ{j!J?q57{2>y>BETgg5Ng%Sj6GgJOC`*_>Iqp5C^o zk_?;kctetbAXTEl3Zn}G$3THl$D#tr5JiWgfFS^q@L7`#C%)6+7oBRW&iDD!>8Lj; zjc)_(n)J2hq=p7PrG3=AnqjwmDs{a{K`<{G9^n=(F)*9u#;`I}PtN)kQ|!pSqNI@H zX^$({@ytWlV+Yx?!S%0n(!HGa&66PD4vW=}5wxLGghs`gmu6F?fosFgNUD_lS>!dP z7PipQKpc*WT_EGyVyGVa$CYK-U5xqpRampd`gnOXy_9+N+zhVz;t+|I0;iuW-DviY zBpIYU2lG&2RppcZa+1&eUW!%>A%A=&9MXNi5)L1lZ~&a4D5^ovss^NSmZ5O;tf~`+ z23Qh3YiZO$Y*^M;!r}D^2Swxz0F^kNkhN7jfkX%xq6!=jp&&w-iE8&uIJ_Lh?>*tL z^5`ndi*GqMt%3nj5e$k7)tNys1Bs|G2uhfdGz5si8HNtj^9PGg+9g2Z{n94|aa(w1YW9}*ys+OBR4f{tr1N4>gKQ;uhi`s@lg2n19@Z36tz4m~1Y3r$K>xNg7inzX=Yjlld0 z29oz5-X08my+U!fnE&!C6z>_AyysgJj|L<3u+8(T1Q=crHF_-5Wd;!h8A;(WJ#Qmf zSER3CGs`AlkT_Ca|c;~!sX8K#?dDpz|ohX5O%j*sMF=u`C`+YFy1u4rf=DdzM VkQLM)fjOId&foFMcHvn-004vgz_(-RVC{TO3U06) zCnf*z2ma`gq!zn0jPl4JreYE(n1m<-(k1?lN~&O*8o)YK2qk7ZNYc+IbeeP(hmMJT zdxuR7uRw} zjw@AEKJ4zgv-VjpY^q+B&ByKLr1|Hc_g8s3@oAVWmAaTkj+Jd;ab3pVqB_ylN?k~C zM9F@Fz>WkYAOKmA)D8rBVB~=&Dynh0p+jM|MHuz5>Cyz3D2!>Q!iyui@4CFg<~Xg_ zkYk@k*ta;O+i~}?mzrxUc4-b~rH*ijJ+@*+yp&L8h0Z83rj6ZrlXNbPfH{$KLsuoQ z$WEF%ZbZXJ+Ju3Zi1LGpAgh3_3UfTZ)I>e^m5?D#cJvfqPxwQKLhQ2zFaB5=G-(kN zCo(xK!o4c$lRj74{R+Mqy1B~;TSK~KxQXMtL4R4dRS7Qk`!VjPPz_@m&Q`VH;c0tG z)_e{Euv$qYyar=NtWIWmpDt&HynQPS3rU3aBCh@(*)9pCw_ zgeSt%rwD}FICi7eWsK96@wS72MPg!_`Bi=Ggw&=Xi7*@DFI%LZI`m^GjzcImnUaD2{K zzNaO_D&Ee6eMtwi;X>I2nNYzR5L6o(0)!L`x}quk8zdSpM+Vfg-Y3HSb%;~w)_l#? zRXQJTQdddsl|%B+hcz?Eojb{=&SYn!&&&{H6^Na9MDBa?FKY_gY{Yj0?m%1Y;&;W% zv9Iu!eMJ|61TPoTzI{plzkT`g&(lK^zNOhtKd*{F1Oj$P0}Njhdn|@o4L;1+aY;Q+ z{Ed!OEu*kxNryq`;V2)?6A5RX;)@lw6#ZfiFz%;me$_KQYwe!X4{_oxyLaTuXdxXw z8A%nnlApcG+wPeW*EZwMz+pN5i3T*cTtzPJ^`9)Wh{Muw;*h(fIHZq<2BoD!?oIes ztBaYv*6{Si@ZQ1V3;;A}7?OfC1PlYd(z+h!BPXM*=AkMY26_b_Jv?GUL|w}F?{Jo1 zeSDF1u8y#JJcy#uF?rC)c>G<%qkCZ=j&M62krjj3kM744_$Gk>ti=*MFxgbby$pDXP&yCR2#QJ@y2;2K}8Xd5+^`5ZYfX_ zfetQT?2k8^?BhGxirAMhCw7EHqGRP3q!408!z&&=`%%6Y%pKRodx{8hztynCcm9A4 z{+m1I5rCWH1-a99?t;hAR}fvPzvuQxBbT02v+6{9imZvUn)|Iy#aLS*FH*v6r=?Di7IK2gti>&>Y6u4skk*3zi%JHbaVK~RK} z@d`{Z*P~;lmU$I3j>T2x_=Vj()ir3V7(O9Bd|oQ#VYzEwa5P#(s{;;ZcF*KgPkiI4 z-{xm^n$lhbNKxeMpOW1>W6z}Fsa@p4hv}hgjd=WI?u{N-^-IFWM^^kC$Yt=3kP}FB z0_^lipnU+yp&~+lfY84q{t)2c34n7gD=3eU$d4ueCg7QQ$nVc_@N;l{J#BX%a3qVm ztN{JZ@cus|II>Nd`$+I{d^*QPRFkf^KLzm53XJYIx&dTn9L5Gg7j8)Bde zh}edpTQ=g41WH_gmMNj1_1faPTBMw{obL6pj<2;$_IOrstiiw zV5|lub~f|mTHLV9^Lo1w)>@tU2ALyV8bQB^FjRhmjwJH15uXLR|Ehl zFA0rkN_2txeQsVtqE&%A?I>n>peYam@)W?{G8gjb#Ma`9L~RQTdqy;~Of9>^V!HKp zIK4{oBICb(tLmrO)HNGOP(Q?u8;3OAaJN_2C}Rs}zM;)Y5DW|08kTUhrA0foxXRd; zU0s$%8NroZJ=5vC=pB}nP(rgeXWV(=&>HO9oEFzhfqXozv6ip;=s2jPG7X(&4vsD=eWk5+i>gldZ@QpdCzAkQjy&vY zHn=#qXy@$aqJO{`*ezDuHftfTi0q0tElqCpQ&JnWhiau1PU4`UHi?7xddgr}>_Cmw zW&>k4+^@{b{%j;o&!d{F)JOBZn=?%^j{EHVLK$GCQUKP$+>a+eB*~!W0m>s?H1wbJ zw~tsr;qaMo_+beL2y9(9Y<^a?5s$NM9q_ZNMFk#Usr;;Eat9%OCLG=> z;h-zPhDcB13C&!@6Iek|poR=2gk%LF^92cq=Y#ms6AlZHZiteizB(9CblK*paGlwd z2#6}WfRRcB)x?m>z_u+&e%4@sA;YpKWI9!Cvp{FE;g+J=G;B5-bspo?b*xM){n0G& zRC7K;)-HwGu;p8JkF|YwKJ3=jn!Z0dtxwLOhvC3b#?|6c>!zknJwIr)3%z!?L?on2 z<=NbAgZ*YhnaSm9Tj|MIi+n6QOmwIts7by+QkYc2VlPk*Ute(JAYH1_S9E53dgfK3|&nd}-nXzBIuP z+md9cP>^KB Date: Tue, 15 Sep 2020 23:48:12 +0300 Subject: [PATCH 4/9] [Security Solutions][Cases - Timeline] Fix bug when adding a timeline to a case (#76967) Co-authored-by: Gloria Hornero Co-authored-by: Elastic Machine --- .../cypress/integration/cases.spec.ts | 4 +- .../timeline_attach_to_case.spec.ts | 76 + .../security_solution/cypress/objects/case.ts | 7 +- .../cypress/objects/timeline.ts | 4 + .../cypress/screens/all_cases.ts | 6 + .../cypress/screens/create_new_case.ts | 3 +- .../cypress/screens/timeline.ts | 11 + .../cypress/tasks/create_new_case.ts | 12 + .../security_solution/cypress/tasks/login.ts | 9 + .../cypress/tasks/timeline.ts | 28 + .../public/cases/components/__mock__/form.ts | 7 + .../components/add_comment/index.test.tsx | 9 +- .../cases/components/add_comment/index.tsx | 26 +- .../cases/components/create/index.test.tsx | 15 +- .../public/cases/components/create/index.tsx | 22 +- .../user_action_tree/index.test.tsx | 69 +- .../user_action_tree/user_action_markdown.tsx | 21 +- .../insert_timeline_popover/index.test.tsx | 55 +- .../insert_timeline_popover/index.tsx | 23 +- .../use_insert_timeline.tsx | 40 +- .../case_and_timeline/data.json.gz | Bin 0 -> 3687 bytes .../case_and_timeline/mappings.json | 2616 +++++++++++++++++ 22 files changed, 2941 insertions(+), 122 deletions(-) create mode 100644 x-pack/plugins/security_solution/cypress/integration/timeline_attach_to_case.spec.ts create mode 100644 x-pack/test/security_solution_cypress/es_archives/case_and_timeline/data.json.gz create mode 100644 x-pack/test/security_solution_cypress/es_archives/case_and_timeline/mappings.json diff --git a/x-pack/plugins/security_solution/cypress/integration/cases.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases.spec.ts index 9438c28f05fef..6194d6892d799 100644 --- a/x-pack/plugins/security_solution/cypress/integration/cases.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/cases.spec.ts @@ -40,7 +40,7 @@ import { TIMELINE_DESCRIPTION, TIMELINE_QUERY, TIMELINE_TITLE } from '../screens import { goToCaseDetails, goToCreateNewCase } from '../tasks/all_cases'; import { openCaseTimeline } from '../tasks/case_details'; -import { backToCases, createNewCase } from '../tasks/create_new_case'; +import { backToCases, createNewCaseWithTimeline } from '../tasks/create_new_case'; import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; @@ -58,7 +58,7 @@ describe('Cases', () => { it('Creates a new case with timeline and opens the timeline', () => { loginAndWaitForPageWithoutDateRange(CASES_URL); goToCreateNewCase(); - createNewCase(case1); + createNewCaseWithTimeline(case1); backToCases(); cy.get(ALL_CASES_PAGE_TITLE).should('have.text', 'Cases Beta'); diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_attach_to_case.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_attach_to_case.spec.ts new file mode 100644 index 0000000000000..6af4d174b9583 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/timeline_attach_to_case.spec.ts @@ -0,0 +1,76 @@ +/* + * 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 { loginAndWaitForTimeline } from '../tasks/login'; +import { + attachTimelineToNewCase, + attachTimelineToExistingCase, + addNewCase, + selectCase, +} from '../tasks/timeline'; +import { DESCRIPTION_INPUT } from '../screens/create_new_case'; +import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; +import { caseTimeline, TIMELINE_CASE_ID } from '../objects/case'; + +describe('attach timeline to case', () => { + beforeEach(() => { + loginAndWaitForTimeline(caseTimeline.id); + }); + context('without cases created', () => { + before(() => { + esArchiverLoad('timeline'); + }); + + after(() => { + esArchiverUnload('timeline'); + }); + + it('attach timeline to a new case', () => { + attachTimelineToNewCase(); + + cy.location('origin').then((origin) => { + cy.get(DESCRIPTION_INPUT).should( + 'have.text', + `[${caseTimeline.title}](${origin}/app/security/timelines?timeline=(id:'${caseTimeline.id}',isOpen:!t))` + ); + }); + }); + + it('attach timeline to an existing case with no case', () => { + attachTimelineToExistingCase(); + addNewCase(); + + cy.location('origin').then((origin) => { + cy.get(DESCRIPTION_INPUT).should( + 'have.text', + `[${caseTimeline.title}](${origin}/app/security/timelines?timeline=(id:'${caseTimeline.id}',isOpen:!t))` + ); + }); + }); + }); + + context('with cases created', () => { + before(() => { + esArchiverLoad('case_and_timeline'); + }); + + after(() => { + esArchiverUnload('case_and_timeline'); + }); + + it('attach timeline to an existing case', () => { + attachTimelineToExistingCase(); + selectCase(TIMELINE_CASE_ID); + + cy.location('origin').then((origin) => { + cy.get(DESCRIPTION_INPUT).should( + 'have.text', + `[${caseTimeline.title}](${origin}/app/security/timelines?timeline=(id:'${caseTimeline.id}',isOpen:!t))` + ); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/objects/case.ts b/x-pack/plugins/security_solution/cypress/objects/case.ts index 12d3f925169af..084df31a604a3 100644 --- a/x-pack/plugins/security_solution/cypress/objects/case.ts +++ b/x-pack/plugins/security_solution/cypress/objects/case.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Timeline } from './timeline'; +import { Timeline, TimelineWithId } from './timeline'; export interface TestCase { name: string; @@ -21,10 +21,11 @@ export interface Connector { password: string; } -const caseTimeline: Timeline = { +export const caseTimeline: TimelineWithId = { title: 'SIEM test', description: 'description', query: 'host.name:*', + id: '0162c130-78be-11ea-9718-118a926974a4', }; export const case1: TestCase = { @@ -41,3 +42,5 @@ export const serviceNowConnector: Connector = { username: 'Username Name', password: 'password', }; + +export const TIMELINE_CASE_ID = '68248e00-f689-11ea-9ab2-59238b522856'; diff --git a/x-pack/plugins/security_solution/cypress/objects/timeline.ts b/x-pack/plugins/security_solution/cypress/objects/timeline.ts index 060a1376b46ce..ff7e80e5661ad 100644 --- a/x-pack/plugins/security_solution/cypress/objects/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/objects/timeline.ts @@ -9,3 +9,7 @@ export interface Timeline { description: string; query: string; } + +export interface TimelineWithId extends Timeline { + id: string; +} diff --git a/x-pack/plugins/security_solution/cypress/screens/all_cases.ts b/x-pack/plugins/security_solution/cypress/screens/all_cases.ts index 4fa6b69eea7c3..dc0e764744f84 100644 --- a/x-pack/plugins/security_solution/cypress/screens/all_cases.ts +++ b/x-pack/plugins/security_solution/cypress/screens/all_cases.ts @@ -4,6 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +export const ALL_CASES_CASE = (id: string) => { + return `[data-test-subj="cases-table-row-${id}"]`; +}; + export const ALL_CASES_CLOSE_ACTION = '[data-test-subj="action-close"]'; export const ALL_CASES_CLOSED_CASES_COUNT = '[data-test-subj="closed-case-count"]'; @@ -14,6 +18,8 @@ export const ALL_CASES_COMMENTS_COUNT = '[data-test-subj="case-table-column-comm export const ALL_CASES_CREATE_NEW_CASE_BTN = '[data-test-subj="createNewCaseBtn"]'; +export const ALL_CASES_CREATE_NEW_CASE_TABLE_BTN = '[data-test-subj="cases-table-add-case"]'; + export const ALL_CASES_DELETE_ACTION = '[data-test-subj="action-delete"]'; export const ALL_CASES_NAME = '[data-test-subj="case-details-link"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/create_new_case.ts b/x-pack/plugins/security_solution/cypress/screens/create_new_case.ts index 6e2beb78fff19..9431c054d96a4 100644 --- a/x-pack/plugins/security_solution/cypress/screens/create_new_case.ts +++ b/x-pack/plugins/security_solution/cypress/screens/create_new_case.ts @@ -6,8 +6,7 @@ export const BACK_TO_CASES_BTN = '[data-test-subj="backToCases"]'; -export const DESCRIPTION_INPUT = - '[data-test-subj="caseDescription"] [data-test-subj="textAreaInput"]'; +export const DESCRIPTION_INPUT = '[data-test-subj="textAreaInput"]'; export const INSERT_TIMELINE_BTN = '[data-test-subj="insert-timeline-button"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/timeline.ts b/x-pack/plugins/security_solution/cypress/screens/timeline.ts index fd41cd63fc090..bcb64fc947feb 100644 --- a/x-pack/plugins/security_solution/cypress/screens/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/screens/timeline.ts @@ -4,8 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ +export const ATTACH_TIMELINE_TO_NEW_CASE_ICON = '[data-test-subj="attach-timeline-case"]'; + +export const ATTACH_TIMELINE_TO_EXISTING_CASE_ICON = + '[data-test-subj="attach-timeline-existing-case"]'; + export const BULK_ACTIONS = '[data-test-subj="utility-bar-action-button"]'; +export const CASE = (id: string) => { + return `[data-test-subj="cases-table-row-${id}"]`; +}; + export const CLOSE_TIMELINE_BTN = '[data-test-subj="close-timeline"]'; export const CREATE_NEW_TIMELINE = '[data-test-subj="timeline-new"]'; @@ -25,6 +34,8 @@ export const ID_FIELD = '[data-test-subj="timeline"] [data-test-subj="field-name export const ID_TOGGLE_FIELD = '[data-test-subj="toggle-field-_id"]'; +export const OPEN_TIMELINE_ICON = '[data-test-subj="open-timeline-button"]'; + export const PIN_EVENT = '[data-test-subj="pin"]'; export const PROVIDER_BADGE = '[data-test-subj="providerBadge"]'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/create_new_case.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_case.ts index b2cde23a8dce2..1d5d240c5c53d 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/create_new_case.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/create_new_case.ts @@ -29,6 +29,18 @@ export const createNewCase = (newCase: TestCase) => { }); cy.get(DESCRIPTION_INPUT).type(`${newCase.description} `, { force: true }); + cy.get(SUBMIT_BTN).click({ force: true }); + cy.get(LOADING_SPINNER).should('exist'); + cy.get(LOADING_SPINNER).should('not.exist'); +}; + +export const createNewCaseWithTimeline = (newCase: TestCase) => { + cy.get(TITLE_INPUT).type(newCase.name, { force: true }); + newCase.tags.forEach((tag) => { + cy.get(TAGS_INPUT).type(`${tag}{enter}`, { force: true }); + }); + cy.get(DESCRIPTION_INPUT).type(`${newCase.description} `, { force: true }); + cy.get(INSERT_TIMELINE_BTN).click({ force: true }); cy.get(TIMELINE_SEARCHBOX).type(`${newCase.timeline.title}{enter}`); cy.get(TIMELINE).should('be.visible'); diff --git a/x-pack/plugins/security_solution/cypress/tasks/login.ts b/x-pack/plugins/security_solution/cypress/tasks/login.ts index ca23a1defd4f5..65f821ec5bfb7 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/login.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/login.ts @@ -5,6 +5,7 @@ */ import * as yaml from 'js-yaml'; +import { TIMELINE_FLYOUT_BODY } from '../screens/timeline'; /** * Credentials in the `kibana.dev.yml` config file will be used to authenticate @@ -143,3 +144,11 @@ export const loginAndWaitForPageWithoutDateRange = (url: string) => { cy.visit(url); cy.get('[data-test-subj="headerGlobalNav"]', { timeout: 120000 }); }; + +export const loginAndWaitForTimeline = (timelineId: string) => { + login(); + cy.viewport('macbook-15'); + cy.visit(`/app/security/timelines?timeline=(id:'${timelineId}',isOpen:!t)`); + cy.get('[data-test-subj="headerGlobalNav"]'); + cy.get(TIMELINE_FLYOUT_BODY).should('be.visible'); +}; diff --git a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts index 6fb8bb5e29ae5..cd8b197fc4dec 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/timeline.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ALL_CASES_CREATE_NEW_CASE_TABLE_BTN } from '../screens/all_cases'; import { BULK_ACTIONS, CLOSE_TIMELINE_BTN, @@ -28,6 +29,10 @@ import { TOGGLE_TIMELINE_EXPAND_EVENT, REMOVE_COLUMN, RESET_FIELDS, + ATTACH_TIMELINE_TO_NEW_CASE_ICON, + OPEN_TIMELINE_ICON, + ATTACH_TIMELINE_TO_EXISTING_CASE_ICON, + CASE, } from '../screens/timeline'; import { drag, drop } from '../tasks/common'; @@ -44,6 +49,20 @@ export const addNameToTimeline = (name: string) => { cy.get(TIMELINE_TITLE).should('have.attr', 'value', name); }; +export const addNewCase = () => { + cy.get(ALL_CASES_CREATE_NEW_CASE_TABLE_BTN).click(); +}; + +export const attachTimelineToNewCase = () => { + cy.get(TIMELINE_SETTINGS_ICON).click({ force: true }); + cy.get(ATTACH_TIMELINE_TO_NEW_CASE_ICON).click({ force: true }); +}; + +export const attachTimelineToExistingCase = () => { + cy.get(TIMELINE_SETTINGS_ICON).click({ force: true }); + cy.get(ATTACH_TIMELINE_TO_EXISTING_CASE_ICON).click({ force: true }); +}; + export const checkIdToggleField = () => { cy.get(ID_HEADER_FIELD).should('not.exist'); @@ -85,6 +104,11 @@ export const openTimelineInspectButton = () => { cy.get(TIMELINE_INSPECT_BUTTON).trigger('click', { force: true }); }; +export const openTimelineFromSettings = () => { + cy.get(TIMELINE_SETTINGS_ICON).click({ force: true }); + cy.get(OPEN_TIMELINE_ICON).click({ force: true }); +}; + export const openTimelineSettings = () => { cy.get(TIMELINE_SETTINGS_ICON).trigger('click', { force: true }); }; @@ -132,6 +156,10 @@ export const resetFields = () => { cy.get(RESET_FIELDS).click({ force: true }); }; +export const selectCase = (caseId: string) => { + cy.get(CASE(caseId)).click(); +}; + export const waitForTimelinesPanelToBeLoaded = () => { cy.get(TIMELINES_TABLE).should('exist'); }; diff --git a/x-pack/plugins/security_solution/public/cases/components/__mock__/form.ts b/x-pack/plugins/security_solution/public/cases/components/__mock__/form.ts index 96c1217577ff2..87f8f46affb52 100644 --- a/x-pack/plugins/security_solution/public/cases/components/__mock__/form.ts +++ b/x-pack/plugins/security_solution/public/cases/components/__mock__/form.ts @@ -4,9 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ import { useForm } from '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form'; +import { useFormData } from '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data'; + jest.mock( '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form' ); +jest.mock( + '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data' +); + export const mockFormHook = { isSubmitted: false, isSubmitting: false, @@ -41,3 +47,4 @@ export const getFormMock = (sampleData: any) => ({ }); export const useFormMock = useForm as jest.Mock; +export const useFormDataMock = useFormData as jest.Mock; diff --git a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx index f697ce443f2c5..a800bd690f710 100644 --- a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx @@ -15,6 +15,7 @@ import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router import { useInsertTimeline } from '../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline'; import { usePostComment } from '../../containers/use_post_comment'; import { useForm } from '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form'; +import { useFormData } from '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data'; // we don't have the types for waitFor just yet, so using "as waitFor" until when we do import { wait as waitFor } from '@testing-library/react'; @@ -23,10 +24,15 @@ jest.mock( '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form' ); +jest.mock( + '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form_data' +); + jest.mock('../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline'); jest.mock('../../containers/use_post_comment'); -export const useFormMock = useForm as jest.Mock; +const useFormMock = useForm as jest.Mock; +const useFormDataMock = useFormData as jest.Mock; const useInsertTimelineMock = useInsertTimeline as jest.Mock; const usePostCommentMock = usePostComment as jest.Mock; @@ -73,6 +79,7 @@ describe('AddComment ', () => { useInsertTimelineMock.mockImplementation(() => defaultInsertTimeline); usePostCommentMock.mockImplementation(() => defaultPostCommment); useFormMock.mockImplementation(() => ({ form: formHookMock })); + useFormDataMock.mockImplementation(() => [{ comment: sampleData.comment }]); jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx index 87bd7bb247056..ef13c87a92dbb 100644 --- a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx @@ -14,7 +14,7 @@ import { Case } from '../../containers/types'; import { MarkdownEditorForm } from '../../../common/components/markdown_editor/form'; import { InsertTimelinePopover } from '../../../timelines/components/timeline/insert_timeline_popover'; import { useInsertTimeline } from '../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline'; -import { Form, useForm, UseField } from '../../../shared_imports'; +import { Form, useForm, UseField, useFormData } from '../../../shared_imports'; import * as i18n from './translations'; import { schema } from './schema'; @@ -46,23 +46,31 @@ export const AddComment = React.memo( forwardRef( ({ caseId, disabled, showLoading = true, onCommentPosted, onCommentSaving }, ref) => { const { isLoading, postComment } = usePostComment(caseId); + const { form } = useForm({ defaultValue: initialCommentValue, options: { stripEmptyFields: false }, schema, }); - const { getFormData, setFieldValue, reset, submit } = form; - const { handleCursorChange, handleOnTimelineChange } = useInsertTimeline( - form, - 'comment' + + const fieldName = 'comment'; + const { setFieldValue, reset, submit } = form; + const [{ comment }] = useFormData({ form, watch: [fieldName] }); + + const onCommentChange = useCallback((newValue) => setFieldValue(fieldName, newValue), [ + setFieldValue, + ]); + + const { handleCursorChange, handleOnTimelineChange } = useInsertTimeline( + comment, + onCommentChange ); const addQuote = useCallback( (quote) => { - const { comment } = getFormData(); - setFieldValue('comment', `${comment}${comment.length > 0 ? '\n\n' : ''}${quote}`); + setFieldValue(fieldName, `${comment}${comment.length > 0 ? '\n\n' : ''}${quote}`); }, - [getFormData, setFieldValue] + [comment, setFieldValue] ); useImperativeHandle(ref, () => ({ @@ -87,7 +95,7 @@ export const AddComment = React.memo( {isLoading && showLoading && }
{ useInsertTimelineMock.mockImplementation(() => defaultInsertTimeline); usePostCaseMock.mockImplementation(() => defaultPostCase); useFormMock.mockImplementation(() => ({ form: formHookMock })); + useFormDataMock.mockImplementation(() => [{ description: sampleData.description }]); jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation); (useGetTags as jest.Mock).mockImplementation(() => ({ tags: sampleTags, diff --git a/x-pack/plugins/security_solution/public/cases/components/create/index.tsx b/x-pack/plugins/security_solution/public/cases/components/create/index.tsx index 31e6da4269ead..3c3cc95218b03 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/index.tsx @@ -24,6 +24,7 @@ import { useForm, UseField, FormDataProvider, + useFormData, } from '../../../shared_imports'; import { usePostCase } from '../../containers/use_post_case'; import { schema } from './schema'; @@ -69,13 +70,18 @@ export const Create = React.memo(() => { options: { stripEmptyFields: false }, schema, }); - const { submit } = form; + + const fieldName = 'description'; + const { submit, setFieldValue } = form; + const [{ description }] = useFormData({ form, watch: [fieldName] }); + const { tags: tagOptions } = useGetTags(); const [options, setOptions] = useState( tagOptions.map((label) => ({ label, })) ); + useEffect( () => setOptions( @@ -85,10 +91,16 @@ export const Create = React.memo(() => { ), [tagOptions] ); - const { handleCursorChange, handleOnTimelineChange } = useInsertTimeline( - form, - 'description' + + const onDescriptionChange = useCallback((newValue) => setFieldValue(fieldName, newValue), [ + setFieldValue, + ]); + + const { handleCursorChange, handleOnTimelineChange } = useInsertTimeline( + description, + onDescriptionChange ); + const handleTimelineClick = useTimelineClick(); const onSubmit = useCallback(async () => { @@ -141,7 +153,7 @@ export const Create = React.memo(() => { { })); const formHookMock = getFormMock(sampleData); useFormMock.mockImplementation(() => ({ form: formHookMock })); + useFormDataMock.mockImplementation(() => [{ content: sampleData.content, comment: '' }]); jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation); }); @@ -69,7 +70,8 @@ describe('UserActionTree ', () => { defaultProps.data.createdBy.username ); }); - it('Renders service now update line with top and bottom when push is required', () => { + + it('Renders service now update line with top and bottom when push is required', async () => { const ourActions = [ getUserAction(['pushed'], 'push-to-service'), getUserAction(['comment'], 'update'), @@ -87,6 +89,7 @@ describe('UserActionTree ', () => { }, caseUserActions: ourActions, }; + const wrapper = mount( @@ -94,10 +97,16 @@ describe('UserActionTree ', () => { ); + + await act(async () => { + wrapper.update(); + }); + expect(wrapper.find(`[data-test-subj="show-top-footer"]`).exists()).toBeTruthy(); expect(wrapper.find(`[data-test-subj="show-bottom-footer"]`).exists()).toBeTruthy(); }); - it('Renders service now update line with top only when push is up to date', () => { + + it('Renders service now update line with top only when push is up to date', async () => { const ourActions = [getUserAction(['pushed'], 'push-to-service')]; const props = { ...defaultProps, @@ -112,6 +121,7 @@ describe('UserActionTree ', () => { }, }, }; + const wrapper = mount( @@ -119,16 +129,22 @@ describe('UserActionTree ', () => { ); + + await act(async () => { + wrapper.update(); + }); + expect(wrapper.find(`[data-test-subj="show-top-footer"]`).exists()).toBeTruthy(); expect(wrapper.find(`[data-test-subj="show-bottom-footer"]`).exists()).toBeFalsy(); }); - it('Outlines comment when update move to link is clicked', () => { + it('Outlines comment when update move to link is clicked', async () => { const ourActions = [getUserAction(['comment'], 'create'), getUserAction(['comment'], 'update')]; const props = { ...defaultProps, caseUserActions: ourActions, }; + const wrapper = mount( @@ -136,6 +152,11 @@ describe('UserActionTree ', () => { ); + + await act(async () => { + wrapper.update(); + }); + expect( wrapper.find(`[data-test-subj="comment-create-action"]`).first().prop('idToOutline') ).toEqual(''); @@ -148,12 +169,13 @@ describe('UserActionTree ', () => { ).toEqual(ourActions[0].commentId); }); - it('Switches to markdown when edit is clicked and back to panel when canceled', () => { + it('Switches to markdown when edit is clicked and back to panel when canceled', async () => { const ourActions = [getUserAction(['comment'], 'create')]; const props = { ...defaultProps, caseUserActions: ourActions, }; + const wrapper = mount( @@ -161,6 +183,11 @@ describe('UserActionTree ', () => { ); + + await act(async () => { + wrapper.update(); + }); + expect( wrapper .find( @@ -168,14 +195,17 @@ describe('UserActionTree ', () => { ) .exists() ).toEqual(false); + wrapper .find(`[data-test-subj="comment-create-action"] [data-test-subj="property-actions-ellipses"]`) .first() .simulate('click'); + wrapper .find(`[data-test-subj="comment-create-action"] [data-test-subj="property-actions-pencil"]`) .first() .simulate('click'); + expect( wrapper .find( @@ -183,12 +213,14 @@ describe('UserActionTree ', () => { ) .exists() ).toEqual(true); + wrapper .find( `[data-test-subj="user-action-${props.data.comments[0].id}"] [data-test-subj="user-action-cancel-markdown"]` ) .first() .simulate('click'); + expect( wrapper .find( @@ -299,23 +331,35 @@ describe('UserActionTree ', () => { ); - wrapper - .find(`[data-test-subj="description-action"] [data-test-subj="property-actions-ellipses"]`) - .first() - .simulate('click'); + + await act(async () => { + await waitFor(() => { + wrapper + .find( + `[data-test-subj="description-action"] [data-test-subj="property-actions-ellipses"]` + ) + .first() + .simulate('click'); + wrapper.update(); + }); + }); + wrapper .find(`[data-test-subj="description-action"] [data-test-subj="property-actions-quote"]`) .first() .simulate('click'); + expect(setFieldValue).toBeCalledWith('comment', `> ${props.data.description} \n`); }); - it('Outlines comment when url param is provided', () => { + + it('Outlines comment when url param is provided', async () => { const commentId = 'neat-comment-id'; const ourActions = [getUserAction(['comment'], 'create')]; const props = { ...defaultProps, caseUserActions: ourActions, }; + jest.spyOn(routeData, 'useParams').mockReturnValue({ commentId }); const wrapper = mount( @@ -324,6 +368,11 @@ describe('UserActionTree ', () => { ); + + await act(async () => { + wrapper.update(); + }); + expect( wrapper.find(`[data-test-subj="comment-create-action"]`).first().prop('idToOutline') ).toEqual(commentId); diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_markdown.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_markdown.tsx index da081fea5eac0..ac2ad179ec60c 100644 --- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_markdown.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_markdown.tsx @@ -10,7 +10,7 @@ import styled from 'styled-components'; import * as i18n from '../case_view/translations'; import { Markdown } from '../../../common/components/markdown'; -import { Form, useForm, UseField } from '../../../shared_imports'; +import { Form, useForm, UseField, useFormData } from '../../../shared_imports'; import { schema, Content } from './schema'; import { InsertTimelinePopover } from '../../../timelines/components/timeline/insert_timeline_popover'; import { useInsertTimeline } from '../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline'; @@ -41,11 +41,20 @@ export const UserActionMarkdown = ({ options: { stripEmptyFields: false }, schema, }); - const { submit } = form; - const { handleCursorChange, handleOnTimelineChange } = useInsertTimeline( - form, - 'content' + + const fieldName = 'content'; + const { submit, setFieldValue } = form; + const [{ content: contentFormValue }] = useFormData({ form, watch: [fieldName] }); + + const onContentChange = useCallback((newValue) => setFieldValue(fieldName, newValue), [ + setFieldValue, + ]); + + const { handleCursorChange, handleOnTimelineChange } = useInsertTimeline( + contentFormValue, + onContentChange ); + const handleCancelAction = useCallback(() => { onChangeEditable(id); }, [id, onChangeEditable]); @@ -93,7 +102,7 @@ export const UserActionMarkdown = ({ return isEditable ? ( { - const reactRedux = jest.requireActual('react-redux'); - return { - ...reactRedux, - useDispatch: () => mockDispatch, - useSelector: jest - .fn() - .mockReturnValueOnce({ - timelineId: 'timeline-id', - timelineSavedObjectId: '34578-3497-5893-47589-34759', - timelineTitle: 'Timeline title', - }) - .mockReturnValue(null), - }; -}); -const mockLocation = { - pathname: '/apath', - hash: '', - search: '', - state: '', -}; const onTimelineChange = jest.fn(); -const defaultProps = { +const props = { isDisabled: false, onTimelineChange, }; describe('Insert timeline popover ', () => { - afterEach(() => { - jest.clearAllMocks(); - }); - - it('should insert a timeline when passed in the router state', () => { - mount(); - expect(mockDispatch.mock.calls[0][0]).toEqual({ - payload: { id: 'timeline-id', show: false }, - type: 'x-pack/security_solution/local/timeline/SHOW_TIMELINE', - }); - expect(onTimelineChange).toBeCalledWith( - 'Timeline title', - '34578-3497-5893-47589-34759', - undefined - ); - expect(mockDispatch.mock.calls[1][0]).toEqual({ - payload: null, - type: 'x-pack/security_solution/local/timeline/SET_INSERT_TIMELINE', - }); - }); - it('should do nothing when router state', () => { - jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation); - mount(); - expect(mockDispatch).toHaveBeenCalledTimes(0); - expect(onTimelineChange).toHaveBeenCalledTimes(0); + it('it renders', () => { + const wrapper = mount(); + expect(wrapper.find('[data-test-subj="insert-timeline-popover"]').exists()).toBeTruthy(); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/index.tsx index 0adf767308269..11ad54321da88 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/index.tsx @@ -5,16 +5,12 @@ */ import { EuiButtonIcon, EuiPopover, EuiSelectableOption, EuiToolTip } from '@elastic/eui'; -import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import React, { memo, useCallback, useMemo, useState } from 'react'; import { OpenTimelineResult } from '../../open_timeline/types'; import { SelectableTimeline } from '../selectable_timeline'; import * as i18n from '../translations'; -import { timelineActions, timelineSelectors } from '../../../../timelines/store/timeline'; import { TimelineType } from '../../../../../common/types/timeline'; -import { State } from '../../../../common/store'; -import { setInsertTimeline } from '../../../store/timeline/actions'; interface InsertTimelinePopoverProps { isDisabled: boolean; @@ -33,25 +29,8 @@ export const InsertTimelinePopoverComponent: React.FC = ({ hideUntitled = false, onTimelineChange, }) => { - const dispatch = useDispatch(); const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const insertTimeline = useSelector((state: State) => { - return timelineSelectors.selectInsertTimeline(state); - }); - useEffect(() => { - if (insertTimeline != null) { - dispatch(timelineActions.showTimeline({ id: insertTimeline.timelineId, show: false })); - onTimelineChange( - insertTimeline.timelineTitle, - insertTimeline.timelineSavedObjectId, - insertTimeline.graphEventId - ); - dispatch(setInsertTimeline(null)); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [insertTimeline, dispatch]); - const handleClosePopover = useCallback(() => { setIsPopoverOpen(false); }, []); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/use_insert_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/use_insert_timeline.tsx index c3bcd1c0ebe51..55c0709bd5543 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/use_insert_timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/insert_timeline_popover/use_insert_timeline.tsx @@ -5,38 +5,60 @@ */ import { isEmpty } from 'lodash/fp'; -import { useCallback, useState } from 'react'; +import { useCallback, useState, useEffect } from 'react'; +import { useDispatch, useSelector, shallowEqual } from 'react-redux'; import { useBasePath } from '../../../../common/lib/kibana'; import { CursorPosition } from '../../../../common/components/markdown_editor'; -import { FormData, FormHook } from '../../../../shared_imports'; +import { timelineActions, timelineSelectors } from '../../../../timelines/store/timeline'; +import { setInsertTimeline } from '../../../store/timeline/actions'; -export const useInsertTimeline = (form: FormHook, fieldName: string) => { +export const useInsertTimeline = (value: string, onChange: (newValue: string) => void) => { const basePath = window.location.origin + useBasePath(); + const dispatch = useDispatch(); const [cursorPosition, setCursorPosition] = useState({ start: 0, end: 0, }); + + const insertTimeline = useSelector(timelineSelectors.selectInsertTimeline, shallowEqual); + const handleOnTimelineChange = useCallback( (title: string, id: string | null, graphEventId?: string) => { const builtLink = `${basePath}/app/security/timelines?timeline=(id:'${id}'${ !isEmpty(graphEventId) ? `,graphEventId:'${graphEventId}'` : '' },isOpen:!t)`; - const currentValue = form.getFormData()[fieldName]; + const newValue: string = [ - currentValue.slice(0, cursorPosition.start), + value.slice(0, cursorPosition.start), cursorPosition.start === cursorPosition.end ? `[${title}](${builtLink})` - : `[${currentValue.slice(cursorPosition.start, cursorPosition.end)}](${builtLink})`, - currentValue.slice(cursorPosition.end), + : `[${value.slice(cursorPosition.start, cursorPosition.end)}](${builtLink})`, + value.slice(cursorPosition.end), ].join(''); - form.setFieldValue(fieldName, newValue); + + onChange(newValue); }, - [basePath, cursorPosition, fieldName, form] + [value, onChange, basePath, cursorPosition] ); + const handleCursorChange = useCallback((cp: CursorPosition) => { setCursorPosition(cp); }, []); + // insertTimeline selector is defined to attached a timeline to a case outside of the case page. + // FYI, if you are in the case page we only use handleOnTimelineChange to attach a timeline to a case. + useEffect(() => { + if (insertTimeline != null && value != null) { + dispatch(timelineActions.showTimeline({ id: insertTimeline.timelineId, show: false })); + handleOnTimelineChange( + insertTimeline.timelineTitle, + insertTimeline.timelineSavedObjectId, + insertTimeline.graphEventId + ); + dispatch(setInsertTimeline(null)); + } + }, [insertTimeline, dispatch, handleOnTimelineChange, value]); + return { cursorPosition, handleCursorChange, diff --git a/x-pack/test/security_solution_cypress/es_archives/case_and_timeline/data.json.gz b/x-pack/test/security_solution_cypress/es_archives/case_and_timeline/data.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..5838d18e1c7dd2c5907cc5f5ef9f55d4cfe2a8d8 GIT binary patch literal 3687 zcmV-t4w&&DiwFP!000026YX7FbK5o+e)q57@~Pc1Y2Gi|m+huW>uKF)Yd76#-H|~O zlu+}c5TIivAn#F*O$VW!u*Q-}z+5J$B6Js>V06}T;4rZACXwJT% zDB7|sE31!ZFoYRQe2D3wKg!lW7u^+m%6WM6!wLKMaB}jgKLha$_;3`!5abcrg>{>^ z$){&CS+cdmF003DGX@a*8JdxTlz25oSfGE&6bj`o1pd@N#Ja=~23`b%HxQ6KYY?lE zqXQEV^W}b9GGt}J6<3Ru?acNPw+_SY0RZbNimke?W*WAkxUTxV;9RulGj?=>F(9m0 zQ#3_V3`w!CRApqiBgK?0L;ZbsuEt=7B?2QDLz2x$&96IwyE#LHNq~?Hc{zdjEzB;! ztkGL&32@10#IMpkA+3Hk!yV(Xa@uT-!F^XNVlq-{jSuw^B8>4n=#w=25zMJbyC^mv zh}gI*%GSPP544Mu4w8k8-ck<3EWy7aGW`ywZxD=vY7MoCxsa|>0-~-?3<*KW1h=Jw zr|AbOw08*SAVMGcS>W8D149sD*tsr(V)zQom<|a#A*?F0Yg@nXd}2BwyZz)e$nJ7- zz-$&FpL1cHV=#eZl7R#RpS6H!WWgYGO+%4v#|tD?g+Ov`)uEpp;A)m@8^GA*fNi1M zI&rhvd115=jO*&1!it^4pOZ^JgOEW?lTE{P^yMF;uH^IBgW1pFD@yv7$wSjxGR6eN zvu;kivSzru3wYaxdrO?I!&DC_@lY{b7n+}q#1&a}sLVHsS?MQbQyZC2H0~g|=vRZ5 zp;@tNg*^FnsrU0I>%KMZWsYH1tZsEfX@(}K4fGefscuO!E4yM(`Lf5wg)c9zmK|4x zDB669$+9$CeXLma>Eq_27mzWZZ#>D;e1^v}DibP~JLJQ4BsNY~gXvzTq(=z(d)jQ8 zcqu<%lRZsX?g~G8TZPVuSFXc5FJuOiJxxUgxXX?1{ZC=|cCF>lo=n7S6QdlHO3!ZAw5-kk82YKpU?-o#T1V!VE5 zY7fVOz|yl_VOkt@YNYXBsfWj1oaiP|NsFKUE+tR-O%h1jzz`?tO4DE94y-m}$paX! zv?pl*|H3QHvpJb^Xf~U(+?#ha#!@jww|fKd?QWW#r0H!J^kO!h(Np|uOyB0owZ5~j zzkzHcp}IXuaI?_iZEhuC^(0WLgE76iKAKTxZyIZl(AIq@J!#Cclx_o4;|%Y1kh!k+ zRVZuC*k(-imB(EXJ$v;0eXd-)`yTNQ5d_7SUB`KXr@=flDMu1>tRj@ak)m3fuj<@^ zgnM)#w}p&b>u5(dqdZQU@n4}WyJFDy5Clbpj4doli4>xUKo+rG{ zI+O_WC=W}7A4{2fuyB%66di$9dBZv=rD=+Z%stA3k|{7NqXPpWplG88jj`?Ew3tV` z%DK)Iil0HIXqY)L9ZNGUM==%CSRL`7IhTF}=r(+@nO_5Mv-A$pC9Y4^Fm?6|NG9!D zsR)xgaox5ow`mjz$Os={Sh}HGnqjNm_n}sPe}aM2O6a6bKbyrfEaqzFjF z<#~Dj`a+-{5vhx`w8C|Kx|-{(j%Xf^^=Zhqwd)sI)JLW(>xynO0&MIc~AHMDzHH^|3OvbHM?DG~- zEowoww$nvZ>t;{{d{=9uHnPST8Mc~=Z~V||6% zGx6SH?z1J{_aNjJ{wwH@nf41_)B(_regKxdDYh*w^W~$9^dpKQaAwMi@YMzIQ9@EY z{g-f_5Eu!xLHPMn_@AIEW7Qa2Ukl$*fy3XR_X8qlrf$o+C4BY6_g5D`o(d7Vg~E5x zzfHduUQIKK<1>e1Rn}d{mQ_m^EMsabj*k!zluj^&a%XV_HkP0lF5^s&65s zonLC;*ysoJLV||!RG@QHMrxde=$9mJ>yD_Tnc!7mHmWMj)nyVJ5eE0Sod~ zjzgMJh4mrLsJ5E&+`{&Z)q&vC>R&`~sn?ZE#DjpVIqfeo`)d&bM>bDTAJ$Uw!WtRj*T!Kla1(; zV(43XS0U?{^usT7F;rD&=fV0s!i%lc+jn{1XH`Ro;<)hm8~LCTomJSopzBhMX|`iZ znqvi$0X-;rj$=qF&=e<7RTbFErdgK1^Ge-Nv$r@IT`8#VU_$xr3qL{fA9x^!_E9IiHsLShDiPp$sZ#5e=BA4O$(!SB~yp$CVf+b z+!mIDjG3zCw1dLgS>YT)c|2WbloEdNs^cZd_H8Lr6l@ZUp&42)o*z)3JwpO$h~z&f z;8_HJ&)ols^SxjJ$lL{3<_D7^*7667%nhgID>g?ObF-zx>`ye z`O~7BhF;ZDHc)=13`c*NKO+BOe)KRe)rvp(h!x5=fqEX_5zl@A|q098=#Z z4qkUepB4o#esi15<+d#vE`C}Leh}Bk#WzjaKPFzM^<|_*49hVCpc#^G`t+L~>XPGX zfux46W(8honwGN*9F7M7s)lAMS}AMXN%$LNzaY~~?m_cEe0WkCgq}{#Q5?frn+8!W zRngdpAq_I5L3T}p1fFhLrlw1w>RXcGtD)rBx++;dG-2RE(^l;xr9oboXYCPr5c`?( zAo{-ZAdsHrArCU-L54iYkOvv^AVVHx$b$@dkRcB;IIIhCIlS2YJps zh%@9tj+F;_`r7~-oZ?4>zFS)w5+57oUqn*6@=?c@{&FQx$$8cHbNqKehUCYP{5V4L z!?vKQX@)7;rs_(DW~!2_0#DK`E%a2N+M44ZDfv;|== Date: Tue, 15 Sep 2020 16:47:09 -0500 Subject: [PATCH 5/9] [Enterprise Search] Add flag to restrict width of layout (#77539) * [Enterprise Search] Add flag to restrict with of layout This PR adds a boolean flag to restrict the width of the Enterprise Search layout file for use in Workplace Search. * Add tests --- .../public/applications/shared/layout/layout.test.tsx | 9 ++++++++- .../public/applications/shared/layout/layout.tsx | 7 +++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.test.tsx index 4053f2f4bb613..623e6e47167d2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.test.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { EuiPageSideBar, EuiButton } from '@elastic/eui'; +import { EuiPageSideBar, EuiButton, EuiPageBody } from '@elastic/eui'; import { Layout, INavContext } from './layout'; @@ -15,6 +15,13 @@ describe('Layout', () => { const wrapper = shallow(); expect(wrapper.find('.enterpriseSearchLayout')).toHaveLength(1); + expect(wrapper.find(EuiPageBody).prop('restrictWidth')).toBeFalsy(); + }); + + it('passes the restrictWidth prop', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiPageBody).prop('restrictWidth')).toEqual(true); }); it('renders navigation', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.tsx index b4497140b65b7..e122c4d5cfdfa 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.tsx @@ -14,6 +14,7 @@ import './layout.scss'; interface ILayoutProps { navigation: React.ReactNode; + restrictWidth?: boolean; } export interface INavContext { @@ -21,7 +22,7 @@ export interface INavContext { } export const NavContext = React.createContext({}); -export const Layout: React.FC = ({ children, navigation }) => { +export const Layout: React.FC = ({ children, navigation, restrictWidth }) => { const [isNavOpen, setIsNavOpen] = useState(false); const toggleNavigation = () => setIsNavOpen(!isNavOpen); const closeNavigation = () => setIsNavOpen(false); @@ -54,7 +55,9 @@ export const Layout: React.FC = ({ children, navigation }) => { {navigation} - {children} + + {children} + ); }; From 8b001dc913b2f5ac415f442c441e224f4ad6b316 Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Tue, 15 Sep 2020 18:26:05 -0400 Subject: [PATCH 6/9] [Maps] Add DynamicStyleProperty#getMbPropertyName and DynamicStyleProperty#getMbPropertyValue (#77366) --- x-pack/plugins/maps/common/constants.ts | 4 ++ .../maps/public/classes/sources/source.ts | 4 +- .../dynamic_color_property.test.tsx | 6 +- .../properties/dynamic_icon_property.test.tsx | 4 +- .../dynamic_orientation_property.ts | 22 +++++--- .../properties/dynamic_size_property.test.tsx | 4 +- .../properties/dynamic_size_property.tsx | 4 +- .../properties/dynamic_style_property.tsx | 55 ++++++++++++++++++- .../properties/dynamic_text_property.ts | 18 +++--- .../vector/properties/style_property.ts | 15 +++-- .../classes/styles/vector/vector_style.tsx | 29 ++++------ 11 files changed, 110 insertions(+), 55 deletions(-) diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index 3589ec41e4db3..d72d04d2a1843 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -263,3 +263,7 @@ export enum MB_LOOKUP_FUNCTION { GET = 'get', FEATURE_STATE = 'feature-state', } + +export type RawValue = string | number | boolean | undefined | null; + +export type FieldFormatter = (value: RawValue) => string | number; diff --git a/x-pack/plugins/maps/public/classes/sources/source.ts b/x-pack/plugins/maps/public/classes/sources/source.ts index 4a050cc3d7d19..7e7a7bd8f049d 100644 --- a/x-pack/plugins/maps/public/classes/sources/source.ts +++ b/x-pack/plugins/maps/public/classes/sources/source.ts @@ -12,7 +12,7 @@ import { Adapters } from 'src/plugins/inspector/public'; import { copyPersistentState } from '../../reducers/util'; import { IField } from '../fields/field'; -import { MAX_ZOOM, MIN_ZOOM } from '../../../common/constants'; +import { FieldFormatter, MAX_ZOOM, MIN_ZOOM } from '../../../common/constants'; import { AbstractSourceDescriptor } from '../../../common/descriptor_types'; import { OnSourceChangeArgs } from '../../connected_components/layer_panel/view'; @@ -37,8 +37,6 @@ export type PreIndexedShape = { path: string; }; -export type FieldFormatter = (value: string | number | null | undefined | boolean) => string; - export interface ISource { destroy(): void; getDisplayName(): Promise; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.test.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.test.tsx index de8f3b5c09175..c9188a0a19b0d 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.test.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.test.tsx @@ -15,7 +15,7 @@ import { shallow } from 'enzyme'; import { Feature, Point } from 'geojson'; import { DynamicColorProperty } from './dynamic_color_property'; -import { COLOR_MAP_TYPE, VECTOR_STYLES } from '../../../../../common/constants'; +import { COLOR_MAP_TYPE, RawValue, VECTOR_STYLES } from '../../../../../common/constants'; import { mockField, MockLayer, MockStyle } from './__tests__/test_util'; import { ColorDynamicOptions } from '../../../../../common/descriptor_types'; import { IVectorLayer } from '../../../layers/vector_layer/vector_layer'; @@ -28,7 +28,7 @@ const makeProperty = (options: ColorDynamicOptions, style?: MockStyle, field?: I field ? field : mockField, (new MockLayer(style ? style : new MockStyle()) as unknown) as IVectorLayer, () => { - return (value: string | number | undefined) => value + '_format'; + return (value: RawValue) => value + '_format'; } ); }; @@ -273,7 +273,7 @@ describe('supportsFieldMeta', () => { null, (new MockLayer(new MockStyle()) as unknown) as IVectorLayer, () => { - return (value: string | number | undefined) => value + '_format'; + return (value: RawValue) => value + '_format'; } ); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.test.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.test.tsx index 06987ab8bcc48..2f9e4709c1c0b 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.test.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.test.tsx @@ -13,7 +13,7 @@ jest.mock('../components/vector_style_editor', () => ({ })); import React from 'react'; -import { VECTOR_STYLES } from '../../../../../common/constants'; +import { RawValue, VECTOR_STYLES } from '../../../../../common/constants'; // @ts-ignore import { DynamicIconProperty } from './dynamic_icon_property'; import { mockField, MockLayer } from './__tests__/test_util'; @@ -33,7 +33,7 @@ const makeProperty = (options: Partial, field: IField = mock field, mockVectorLayer, () => { - return (value: string | number | undefined) => value + '_format'; + return (value: RawValue) => value + '_format'; } ); }; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_orientation_property.ts b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_orientation_property.ts index dd976027a50f2..192fa1f4db6e0 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_orientation_property.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_orientation_property.ts @@ -5,25 +5,29 @@ */ import { Map as MbMap } from 'mapbox-gl'; -import { DynamicStyleProperty } from './dynamic_style_property'; -import { getComputedFieldName } from '../style_util'; -import { VECTOR_STYLES } from '../../../../../common/constants'; +import { DynamicStyleProperty, getNumericalMbFeatureStateValue } from './dynamic_style_property'; import { OrientationDynamicOptions } from '../../../../../common/descriptor_types'; +import { RawValue } from '../../../../../common/constants'; export class DynamicOrientationProperty extends DynamicStyleProperty { syncIconRotationWithMb(symbolLayerId: string, mbMap: MbMap) { if (this._field && this._field.isValid()) { - const targetName = this._field.supportsAutoDomain() - ? getComputedFieldName(VECTOR_STYLES.ICON_ORIENTATION, this.getFieldName()) - : this._field.getName(); - // Using property state instead of feature-state because layout properties do not support feature-state - mbMap.setLayoutProperty(symbolLayerId, 'icon-rotate', ['coalesce', ['get', targetName], 0]); + const targetName = this.getMbPropertyName(); + mbMap.setLayoutProperty(symbolLayerId, 'icon-rotate', [ + 'coalesce', + [this.getMbLookupFunction(), targetName], + 0, + ]); } else { mbMap.setLayoutProperty(symbolLayerId, 'icon-rotate', 0); } } - supportsMbFeatureState() { + supportsMbFeatureState(): boolean { return false; } + + getMbPropertyValue(rawValue: RawValue): RawValue { + return getNumericalMbFeatureStateValue(rawValue); + } } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.test.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.test.tsx index c5298067f6cbe..b4244cf7829c4 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.test.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.test.tsx @@ -15,7 +15,7 @@ import { shallow } from 'enzyme'; // @ts-ignore import { DynamicSizeProperty } from './dynamic_size_property'; -import { VECTOR_STYLES } from '../../../../../common/constants'; +import { RawValue, VECTOR_STYLES } from '../../../../../common/constants'; import { IField } from '../../../fields/field'; import { Map as MbMap } from 'mapbox-gl'; import { SizeDynamicOptions } from '../../../../../common/descriptor_types'; @@ -48,7 +48,7 @@ const makeProperty = ( field, (new MockLayer(mockStyle) as unknown) as IVectorLayer, () => { - return (value: string | number | undefined) => value + '_format'; + return (value: RawValue) => value + '_format'; }, false ); diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.tsx index 35c830f3cb5e3..4e75a61539ad9 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.tsx @@ -7,7 +7,7 @@ import _ from 'lodash'; import React from 'react'; import { Map as MbMap } from 'mapbox-gl'; -import { DynamicStyleProperty, FieldFormatter } from './dynamic_style_property'; +import { DynamicStyleProperty } from './dynamic_style_property'; import { OrdinalLegend } from '../components/legend/ordinal_legend'; import { makeMbClampedNumberExpression } from '../style_util'; import { @@ -16,7 +16,7 @@ import { SMALL_MAKI_ICON_SIZE, // @ts-expect-error } from '../symbol_utils'; -import { MB_LOOKUP_FUNCTION, VECTOR_STYLES } from '../../../../../common/constants'; +import { FieldFormatter, MB_LOOKUP_FUNCTION, VECTOR_STYLES } from '../../../../../common/constants'; import { SizeDynamicOptions } from '../../../../../common/descriptor_types'; import { IField } from '../../../fields/field'; import { IVectorLayer } from '../../../layers/vector_layer/vector_layer'; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx index f6ab052497723..2bc819daeea90 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx @@ -15,6 +15,8 @@ import { SOURCE_META_DATA_REQUEST_ID, STYLE_TYPE, VECTOR_STYLES, + RawValue, + FieldFormatter, } from '../../../../../common/constants'; import { OrdinalFieldMetaPopover } from '../components/field_meta/ordinal_field_meta_popover'; import { CategoricalFieldMetaPopover } from '../components/field_meta/categorical_field_meta_popover'; @@ -28,6 +30,7 @@ import { IField } from '../../../fields/field'; import { IVectorLayer } from '../../../layers/vector_layer/vector_layer'; import { IJoin } from '../../../joins/join'; import { IVectorStyle } from '../vector_style'; +import { getComputedFieldName } from '../style_util'; export interface IDynamicStyleProperty extends IStyleProperty { getFieldMetaOptions(): FieldMetaOptions; @@ -46,9 +49,16 @@ export interface IDynamicStyleProperty extends IStyleProperty { pluckOrdinalStyleMetaFromFeatures(features: Feature[]): RangeFieldMeta | null; pluckCategoricalStyleMetaFromFeatures(features: Feature[]): CategoryFieldMeta | null; getValueSuggestions(query: string): Promise; -} -export type FieldFormatter = (value: string | number | undefined) => string | number; + // Returns the name that should be used for accessing the data from the mb-style rule + // Depending on + // - whether the field is used for labeling, icon-orientation, or other properties (color, size, ...), `feature-state` and or `get` is used + // - whether the field was run through a field-formatter, a new dynamic field is created with the formatted-value + // The combination of both will inform what field-name (e.g. the "raw" field name from the properties, the "computed field-name" for an on-the-fly created property (e.g. for feature-state or field-formatting). + // todo: There is an existing limitation to .mvt backed sources, where the field-formatters are not applied. Here, the raw-data needs to be accessed. + getMbPropertyName(): string; + getMbPropertyValue(value: RawValue): RawValue; +} export class DynamicStyleProperty extends AbstractStyleProperty @@ -313,7 +323,7 @@ export class DynamicStyleProperty }; } - formatField(value: string | number | undefined): string | number { + formatField(value: RawValue): string | number { if (this.getField()) { const fieldName = this.getFieldName(); const fieldFormatter = this._getFieldFormatter(fieldName); @@ -345,4 +355,43 @@ export class DynamicStyleProperty /> ); } + + getMbPropertyName() { + if (!this._field) { + return ''; + } + + let targetName; + if (this.supportsMbFeatureState()) { + // Base case for any properties that can support feature-state (e.g. color, size, ...) + // They just re-use the original property-name + targetName = this._field.getName(); + } else { + if (this._field.canReadFromGeoJson() && this._field.supportsAutoDomain()) { + // Geojson-sources can support rewrite + // e.g. field-formatters will create duplicate field + targetName = getComputedFieldName(this.getStyleName(), this._field.getName()); + } else { + // Non-geojson sources (e.g. 3rd party mvt or ES-source as mvt) + targetName = this._field.getName(); + } + } + return targetName; + } + + getMbPropertyValue(rawValue: RawValue): RawValue { + // Maps only uses feature-state for numerical values. + // `supportsMbFeatureState` will only return true when the mb-style rule does a feature-state lookup on a numerical value + // Calling `isOrdinal` would be equivalent. + return this.supportsMbFeatureState() ? getNumericalMbFeatureStateValue(rawValue) : rawValue; + } +} + +export function getNumericalMbFeatureStateValue(value: RawValue) { + if (typeof value !== 'string') { + return value; + } + + const valueAsFloat = parseFloat(value); + return isNaN(valueAsFloat) ? null : valueAsFloat; } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_text_property.ts b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_text_property.ts index d55a6e1cfb444..ec79d71eb7587 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_text_property.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_text_property.ts @@ -6,18 +6,18 @@ import { Map as MbMap } from 'mapbox-gl'; import { DynamicStyleProperty } from './dynamic_style_property'; -import { getComputedFieldName } from '../style_util'; import { LabelDynamicOptions } from '../../../../../common/descriptor_types'; +import { RawValue } from '../../../../../common/constants'; export class DynamicTextProperty extends DynamicStyleProperty { syncTextFieldWithMb(mbLayerId: string, mbMap: MbMap) { if (this._field && this._field.isValid()) { - // Fields that support auto-domain are normalized with a field-formatter and stored into a computed-field - // Otherwise, the raw value is just carried over and no computed field is created. - const targetName = this._field.supportsAutoDomain() - ? getComputedFieldName(this._styleName, this.getFieldName()) - : this._field.getName(); - mbMap.setLayoutProperty(mbLayerId, 'text-field', ['coalesce', ['get', targetName], '']); + const targetName = this.getMbPropertyName(); + mbMap.setLayoutProperty(mbLayerId, 'text-field', [ + 'coalesce', + [this.getMbLookupFunction(), targetName], + '', + ]); } else { mbMap.setLayoutProperty(mbLayerId, 'text-field', null); } @@ -34,4 +34,8 @@ export class DynamicTextProperty extends DynamicStyleProperty { isDynamic(): boolean; isComplete(): boolean; - formatField(value: string | number | undefined): string | number; + formatField(value: RawValue): string | number; getStyleName(): VECTOR_STYLES; getOptions(): T; renderLegendDetailRow(legendProps: LegendProps): ReactElement | null; @@ -53,9 +53,14 @@ export class AbstractStyleProperty implements IStyleProperty { return true; } - formatField(value: string | number | undefined): string | number { - // eslint-disable-next-line eqeqeq - return value == undefined ? '' : value; + formatField(value: RawValue): string | number { + if (typeof value === 'undefined' || value === null) { + return ''; + } else if (typeof value === 'boolean') { + return value.toString(); + } else { + return value; + } } getStyleName(): VECTOR_STYLES { diff --git a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx index 956522524a2eb..1244c53afe9a6 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx @@ -29,7 +29,7 @@ import { import { StyleMeta } from './style_meta'; import { VectorIcon } from './components/legend/vector_icon'; import { VectorStyleLegend } from './components/legend/vector_style_legend'; -import { getComputedFieldName, isOnlySingleFeatureType } from './style_util'; +import { isOnlySingleFeatureType } from './style_util'; import { StaticStyleProperty } from './properties/static_style_property'; import { DynamicStyleProperty } from './properties/dynamic_style_property'; import { DynamicSizeProperty } from './properties/dynamic_size_property'; @@ -82,11 +82,6 @@ const POINTS = [GEO_JSON_TYPE.POINT, GEO_JSON_TYPE.MULTI_POINT]; const LINES = [GEO_JSON_TYPE.LINE_STRING, GEO_JSON_TYPE.MULTI_LINE_STRING]; const POLYGONS = [GEO_JSON_TYPE.POLYGON, GEO_JSON_TYPE.MULTI_POLYGON]; -function getNumericalMbFeatureStateValue(value: string) { - const valueAsFloat = parseFloat(value); - return isNaN(valueAsFloat) ? null : valueAsFloat; -} - export interface IVectorStyle extends IStyle { getAllStyleProperties(): Array>; getDynamicPropertiesArray(): Array>; @@ -618,21 +613,17 @@ export class VectorStyle implements IVectorStyle { for (let j = 0; j < dynamicStyleProps.length; j++) { const dynamicStyleProp = dynamicStyleProps[j]; - const name = dynamicStyleProp.getFieldName(); - const computedName = getComputedFieldName(dynamicStyleProp.getStyleName(), name); - const rawValue = feature.properties ? feature.properties[name] : undefined; + const targetMbName = dynamicStyleProp.getMbPropertyName(); + const rawValue = feature.properties + ? feature.properties[dynamicStyleProp.getFieldName()] + : undefined; + const targetMbValue = dynamicStyleProp.getMbPropertyValue(rawValue); if (dynamicStyleProp.supportsMbFeatureState()) { - tmpFeatureState[name] = getNumericalMbFeatureStateValue(rawValue); // the same value will be potentially overridden multiple times, if the name remains identical + tmpFeatureState[targetMbName] = targetMbValue; // the same value will be potentially overridden multiple times, if the name remains identical } else { - // in practice, a new system property will only be created for: - // - label text: this requires the value to be formatted first. - // - icon orientation: this is a lay-out property which do not support feature-state (but we're still coercing to a number) - - const formattedValue = dynamicStyleProp.isOrdinal() - ? getNumericalMbFeatureStateValue(rawValue) - : dynamicStyleProp.formatField(rawValue); - - if (feature.properties) feature.properties[computedName] = formattedValue; + if (feature.properties) { + feature.properties[targetMbName] = targetMbValue; + } } } tmpFeatureIdentifier.source = mbSourceId; From e9a4555623ad55120085a1f9bca3774116eef07b Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Tue, 15 Sep 2020 18:27:39 -0400 Subject: [PATCH 7/9] [Lens] Remove dynamic names in telemetry fields (#76988) Co-authored-by: Elastic Machine --- .../editor_frame_service/editor_frame/suggestion_panel.tsx | 1 - .../dimension_panel/dimension_editor.tsx | 3 --- 2 files changed, 4 deletions(-) diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx index f1dc3fa306d15..e6503cb793a8e 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx @@ -338,7 +338,6 @@ export function SuggestionPanel({ if (lastSelectedSuggestion === index) { rollbackToCurrentVisualization(); } else { - trackSuggestionEvent(`position_${index}_of_${suggestions.length}`); setLastSelectedSuggestion(index); switchToSuggestion(dispatch, suggestion); } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx index 98e9389a85819..153757ac37da1 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx @@ -213,9 +213,6 @@ export function DimensionEditor(props: DimensionEditorProps) { previousColumn: selectedColumn, }); - trackUiEvent( - `indexpattern_dimension_operation_from_${selectedColumn.operationType}_to_${operationType}` - ); setState( changeColumn({ state, From 0752de7b1091cebb250ff2519a8631555abde47e Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 15 Sep 2020 16:44:06 -0700 Subject: [PATCH 8/9] Skip flaky Events Viewer Cypress test Signed-off-by: Tyler Smalley --- .../cypress/integration/events_viewer.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/events_viewer.spec.ts b/x-pack/plugins/security_solution/cypress/integration/events_viewer.spec.ts index 5f2de69689865..d193330dc54ff 100644 --- a/x-pack/plugins/security_solution/cypress/integration/events_viewer.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/events_viewer.spec.ts @@ -47,7 +47,8 @@ const defaultHeadersInDefaultEcsCategory = [ { id: 'destination.ip' }, ]; -describe('Events Viewer', () => { +// https://github.com/elastic/kibana/issues/70757 +describe.skip('Events Viewer', () => { context('Fields rendering', () => { before(() => { loginAndWaitForPage(HOSTS_URL); From e667f2fa8d637e76bb726109afa2731b1990a394 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 15 Sep 2020 18:28:38 -0700 Subject: [PATCH 9/9] [UBI] Copy license to /licenses folder (#77563) Requirement for OpenShift certification https://redhat-connect.gitbook.io/partner-guide-for-red-hat-openshift-and-container/program-on-boarding/technical-prerequisites#licenses-requirements Signed-off-by: Tyler Smalley --- .../tasks/os_packages/docker_generator/templates/Dockerfile | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/Dockerfile b/src/dev/build/tasks/os_packages/docker_generator/templates/Dockerfile index d235bfe9d6fbc..24649a52b729b 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/Dockerfile +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/Dockerfile @@ -85,7 +85,6 @@ RUN groupadd --gid 1000 kibana && \ useradd --uid 1000 --gid 1000 \ --home-dir /usr/share/kibana --no-create-home \ kibana -USER kibana LABEL org.label-schema.build-date="{{dockerBuildDate}}" \ org.label-schema.license="{{license}}" \ @@ -115,8 +114,13 @@ LABEL name="Kibana" \ release="1" \ summary="Kibana" \ description="Your window into the Elastic Stack." + +RUN mkdir /licenses && \ + cp LICENSE.txt /licenses/LICENSE {{/ubi}} +USER kibana + ENTRYPOINT ["/usr/local/bin/dumb-init", "--"] CMD ["/usr/local/bin/kibana-docker"]